Files
AskaEth cb564fe981 fix(memory): 空 query 不再触发语义回退 + 回退有界(修真机 39 秒召回)
真因(agent-197,文件:行):只有附件的消息正文为空 ⇒ queryFromMessages 返回空串 ⇒
retrieve.go 的 vectorIndexScores 在问索引之前就 return ⇒ 上层分不清「没问过索引」与
「索引无命中」⇒ 打出误导的 no-candidate-hit ⇒ 转内存回退,把全库 302 条候选正文塞进
一个 embedding 请求(302 × 约 130ms = 39.3 秒,与实测 duration_ns 吻合)。
索引/维度/表/写入其实全是好的(PG vec_items 302 行全 dim=1024,与 SQLite 302 个 id 集合相同)。
修法:空/纯空白 query 整条语义路径跳过(不问索引、不 embed);回退改为有界
(分批 32 + 1 秒时间预算 + 正文到向量 FIFO 缓存 1024,不再每回合重算)。
文件:retrieve.go / retrieve_semantic.go + 新增 retrieve_fallback.go / retrieve_keyword.go /
retrieve_fallback_test.go(retrieve.go 曾达 501 行,按 ≤500 拆分)。
变异:去掉空 query 门 ⇒ 向量化 43 条文本(want 0);整库单请求 ⇒ 69 条超上限 32;停用缓存 ⇒ 重复嵌入。
2026-10-05 08:04:47 +08:00

62 lines
2.4 KiB
Go

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())
}