fix(captain): 试验场不查 FAQ 知识库 + 添加常见问题报 400

三个问题修复:

1. 添加常见问题报 400 (assistant_id 类型不匹配)
   - CreateResponseDialog.vue: route.params.assistantId 是字符串,
     传给后端 uint 字段导致 JSON 反序列化失败
   - 修复: Number(route.params.assistantId) 转为数字
   - 同时修复 POST /assistant_responses 无尾部斜杠 307 重定向问题

2. 试验场 playground 不做 RAG 检索
   - generatePlaygroundLLMResponse 只构建 system prompt + 对话历史,
     从不查 FAQ 知识库
   - 新增 retrieveFAQContext(): embed 用户问题 → pgvector 搜索 approved
     FAQ → 注入 system prompt
   - 受 feature_faq 配置开关控制

3. FAQ embedding 无法写入 (pgvector 序列化 + 维度问题)
   - pgvector stub 无 driver.Valuer, GORM Save() 报 SQLSTATE 42804
   - 新增 UpdateEmbedding() 用 ?::vector 原始 SQL 绕过
   - SimilaritySearch 排除 embedding 列 + 手动格式化向量字面量
   - embedding 列从 vector(1536) 改为 vector (跟随模型维度)
   - FAQ 创建/更新时自动索引 embedding (SetRAGService 注入)
This commit is contained in:
Rogee
2026-08-01 19:37:19 +08:00
parent 31238476b1
commit 63df5b8b91
9 changed files with 124 additions and 12 deletions
@@ -2,6 +2,8 @@ package repository
import (
"context"
"strconv"
"strings"
"github.com/gochat/gochat/internal/model"
"github.com/pgvector/pgvector-go"
@@ -45,6 +47,21 @@ func (r *CaptainAssistantResponseRepo) Update(ctx context.Context, resp *model.C
return r.db.WithContext(ctx).Save(resp).Error
}
// UpdateEmbedding updates only the embedding column using raw SQL with explicit
// ::vector cast to avoid GORM/pgvector serialization issue (SQLSTATE 42804).
func (r *CaptainAssistantResponseRepo) UpdateEmbedding(ctx context.Context, id uint, embedding pgvector.Vector) error {
// Format as PG vector literal: [0.1,0.2,...]
strs := make([]string, len(embedding))
for i, v := range embedding {
strs[i] = strconv.FormatFloat(float64(v), 'f', -1, 32)
}
vecStr := "[" + strings.Join(strs, ",") + "]"
return r.db.WithContext(ctx).Exec(
"UPDATE captain_assistant_responses SET embedding = ?::vector WHERE id = ?",
vecStr, id,
).Error
}
func (r *CaptainAssistantResponseRepo) Delete(ctx context.Context, id uint) error {
return r.db.WithContext(ctx).Delete(&model.CaptainAssistantResponse{}, id).Error
}
@@ -80,10 +97,18 @@ func (r *CaptainAssistantResponseRepo) ListByDocument(ctx context.Context, docum
// Reference: Chatwoot Captain::AssistantResponsesSearchService
func (r *CaptainAssistantResponseRepo) SimilaritySearch(ctx context.Context, assistantID uint, embedding pgvector.Vector, limit int) ([]model.CaptainAssistantResponse, error) {
var responses []model.CaptainAssistantResponse
// Format embedding as PG vector literal for the <=> operator (stub has no driver.Valuer).
strs := make([]string, len(embedding))
for i, v := range embedding {
strs[i] = strconv.FormatFloat(float64(v), 'f', -1, 32)
}
vecStr := "[" + strings.Join(strs, ",") + "]"
// Cosine distance (<=>) orders by closest first.
// Omit embedding column from SELECT — the pgvector stub can't scan it back.
if err := r.db.WithContext(ctx).
Select("id, account_id, assistant_id, documentable_id, documentable_type, question, answer, status, edited, created_at, updated_at").
Where("assistant_id = ? AND status = ?", assistantID, model.ResponseStatusApproved).
Order(gorm.Expr("embedding <=> ?", embedding)).
Order(gorm.Expr("embedding <=> ?::vector", vecStr)).
Limit(limit).
Find(&responses).Error; err != nil {
return nil, err