Files
ai-gateway-go/internal/workbench/service.go
T
superidou b536672000 feat(m8): P2 pgvector + Ollama 向量化与语义检索
- PostgreSQL 切换 pgvector/pgvector:pg17 镜像;迁移 000024 建 vector 扩展、
  knowledge_chunks.embedding vector(1024) + HNSW 余弦索引,retrieval_mode 放宽三态
- OllamaEmbedder 本地 bge-m3 批量嵌入,404 惰性 pull 重试,维度/超时校验,可整体关闭
- SemanticRetriever/HybridRetriever + NewRetriever 按 retrieval_mode 分发,缺 embedder 回退 FTS
- 文档入库同步批量向量化;Ollama 故障降级入库 + embedding_failed 事件
- 修复 pgx CopyFrom 对 vector 列二进制编码误读:COPY 基础列后同事务 unnest 批量回填
- 修复降级路径 embeddings=nil 索引越界 panic(Add 与 Reprocess)
- 知识库列表 vectorized_chunk_count + 前端三态检索模式选择与向量化覆盖率
- 单测 embedder/retrievers + 集成 TestKnowledgeVectorLifecycle 全绿

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-12 15:16:32 +08:00

79 lines
2.0 KiB
Go

package workbench
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
platformid "aigateway.local/core/internal/platform/id"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
type Service struct {
pool *pgxpool.Pool
embedder Embedder
}
func NewService(pool *pgxpool.Pool) *Service { return &Service{pool: pool} }
// SetEmbedder 注入向量化器(M8 P2)。为 nil 时知识库退回纯 FTS,不生成 embedding。
func (s *Service) SetEmbedder(embedder Embedder) { s.embedder = embedder }
// Embedder 返回当前向量化器(可能为 nil)。
func (s *Service) Embedder() Embedder { return s.embedder }
func newUUID() (string, error) { return platformid.NewUUID() }
func emit(ctx context.Context, tx pgx.Tx, eventType, aggregateType, aggregateID, actorID string, values map[string]any) error {
eventID, err := newUUID()
if err != nil {
return err
}
if values == nil {
values = make(map[string]any)
}
values[aggregateType+"_id"] = aggregateID
values["actor_id"] = actorID
payload, err := json.Marshal(values)
if err != nil {
return err
}
_, err = tx.Exec(ctx, `INSERT INTO gateway.outbox_events(event_id,event_type,event_version,aggregate_type,aggregate_id,payload) VALUES($1,$2,1,$3,$4,$5)`, eventID, eventType, aggregateType, aggregateID, payload)
return err
}
func normalizeStrings(values []string, maximum int) ([]string, error) {
seen := make(map[string]struct{}, len(values))
result := make([]string, 0, len(values))
for _, value := range values {
value = strings.TrimSpace(value)
if value == "" {
continue
}
if len(value) > 255 {
return nil, errors.New("列表项过长")
}
if _, ok := seen[value]; ok {
continue
}
seen[value] = struct{}{}
result = append(result, value)
}
if len(result) > maximum {
return nil, fmt.Errorf("列表最多允许 %d 项", maximum)
}
return result, nil
}
func mapNotFound(err error) error {
if errors.Is(err, pgx.ErrNoRows) {
return ErrNotFound
}
return err
}
func rollback(ctx context.Context, tx pgx.Tx) { _ = tx.Rollback(ctx) }