package memory import ( "math" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/embedding" ) // 本文件收拢语义打分的辅助件:内存余弦相似度与语义路径的 // 一次性告警(回退发生、回退超预算)。它们的用量小但分散, // 独立成文件是为了让 retrieve.go 保持可读(文件 ≤500 行,AGENTS §10)。 // cosineSim 计算两个向量的余弦相似度,由调用方夹到 [0,1]。 // 长度为零或不一致的向量得 0。 func cosineSim(a, b embedding.Vector) float64 { if len(a) == 0 || len(b) == 0 || len(a) != len(b) { return 0 } var dot, na, nb float64 for i := range a { dot += float64(a[i]) * float64(b[i]) na += float64(a[i]) * float64(a[i]) nb += float64(b[i]) * float64(b[i]) } if na == 0 || nb == 0 { return 0 } return dot / (math.Sqrt(na) * math.Sqrt(nb)) } // warnSemanticFallback 报告一次「接入了向量索引但在候选集内无命中, // 已改走内存余弦」。它每个检索器只报一次:该状态在一次运行里 // 基本不变(索引尚未写入或 embedder 维度不匹配),逐次 Retrieve // 重复告警只会淹没有用日志。索引报错本身由 vectorIndexScores 以 // Warn 记录,这里用 Debug 区分「已知会回退」与「真的出错了」。 func (r *Retriever) warnSemanticFallback() { r.seedMu.Lock() defer r.seedMu.Unlock() if r.fallbackWarned { return } r.fallbackWarned = true r.log.Debug("memory: vector index had no candidate hit, using in-memory cosine fallback") } // warnFallbackBudget 报告一次「内存余弦回退没能在预算内把候选正文 // 向量化完,本轮语义分只有一部分」。它是 Warn 而不是 Debug:语义分 // 少了一部分会静默改变排名,运维必须看得见(AGENTS §10 日志带来源)。 // 同样每个检索器只报一次 —— 该状态在一次运行里基本不变(索引缺向量 // 或 embedder 慢),逐次告警只会把日志刷满;条数由日志字段给出, // 需要细看时看 candidates/embedded。 func (r *Retriever) warnFallbackBudget(candidates, embedded int) { r.seedMu.Lock() defer r.seedMu.Unlock() if r.budgetWarned { return } r.budgetWarned = true r.log.Warn("memory: in-memory cosine fallback exceeded its embed budget, semantic scores partial", "candidates", candidates, "embedded", embedded, "budget", r.embedBudget()) }