Files
Agent-System-Backend/internal/rag/hash_embed.go
T
weichen 5d5cef9fcd
ci / vet-and-test (pull_request) Successful in 56s
Add model identity and in-database vector top-k via pgvector (U6 slice 3)
Why: plan U6 vector slice (KTD11 correctness step) - same-dimension vectors from different models must never mix, and PG tiers should run filtered exact top-k in the database instead of scanning every row in-process.

Evidence: full suite green on SQLite and real PostgreSQL 18.6 with pgvector (35 packages each tier) - pgvector_test covers model-identity and dimension isolation, update/delete atomicity (BLOB and vec_pg swap together), shared top-5 with score monotonicity, Recall@10 = exact-scan parity, deny-all predicate leakage; storage-migrate tests green after excluding the derived vec_pg column.

Impact: v25 migration adds model/content_hash columns and (PG, best-effort DO block) the pgvector extension plus vec_pg column - absence degrades to in-process scan; UpsertKnowledgeVectorExt dual-writes BLOB (cross-tier authority) and vec_pg; SearchKnowledgeVectorsModel/SharedModel filter by model identity and use SQL exact top-k (ORDER BY vec_pg <=> q) with automatic degradation; embedders expose Model() and the rag pipeline threads it through indexing and both retrieve paths; storage-migrate excludes vec_pg as a derived column.

Refs: proposals/2026-10-11-collaboration-storage-and-distribution.md U6 slice 3, KTD11; R9.
2026-10-11 16:31:12 +00:00

75 lines
1.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package rag
import (
"context"
"fmt"
"hash/fnv"
"unicode/utf8"
)
const hashDim = 32
// HashEmbedder 是零依赖的本地嵌入档。语义弱于真实模型,只保证同文本稳定、近文本可比较。
// 由 settings models.embedding.provider=hash 显式打开;未设置时仍按 D23 退化为全文。
type HashEmbedder struct{}
// Model 本地哈希嵌入的身份(U6-3:同源写入/检索,禁止与真实模型混查)。
func (HashEmbedder) Model() string { return "hash:" + fmt.Sprint(hashDim) }
func (HashEmbedder) Embed(_ context.Context, texts []string) ([][]float32, error) {
out := make([][]float32, len(texts))
for i, text := range texts {
out[i] = hashVec(text)
}
return out, nil
}
func hashVec(text string) []float32 {
v := make([]float32, hashDim)
for _, r := range text {
h := fnv.New32a()
var buf [4]byte
n := utf8.EncodeRune(buf[:], r)
_, _ = h.Write(buf[:n])
idx := int(h.Sum32() % hashDim)
v[idx] += 1
}
var norm float32
for _, x := range v {
norm += x * x
}
if norm == 0 {
return v
}
inv := float32(1)
// 粗略归一,避免余弦被长度主导。
for norm > 1 {
norm = norm / 4
inv = inv / 2
}
for i := range v {
v[i] *= inv
}
return v
}
// SwitchingEmbedder 在 provider=hash 时走本地档,否则走 HTTP。
type SwitchingEmbedder struct {
HTTP *HTTPEmbedder
Provider func() string
}
func (e SwitchingEmbedder) Embed(ctx context.Context, texts []string) ([][]float32, error) {
if e.Provider != nil && e.Provider() == "hash" {
return HashEmbedder{}.Embed(ctx, texts)
}
if e.HTTP == nil {
return nil, fmt.Errorf("rag: embedding 模型未配置(settings.models.embedding)")
}
return e.HTTP.Embed(ctx, texts)
}
func (e SwitchingEmbedder) Offline() bool {
return e.Provider != nil && e.Provider() == "hash"
}