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" }