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;停用缓存 ⇒ 重复嵌入。
This commit is contained in:
AskaEth committed 2026-10-05 08:04:47 +08:00
1 parent 6cb41a8794
commit cb564fe981
5 files changed
+532 -66

No files matched your search

+20 -3
View File
@@ -6,9 +6,9 @@ import (
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/embedding"
)
// 本文件收拢语义打分的辅助件:内存余弦相似度与「索引没答上、改走
// 内存回退」的一次性告警。它们的用量小但分散,独立成文件是为了让
// retrieve.go 保持可读(文件 ≤500 行,AGENTS §10)。
// 本文件收拢语义打分的辅助件:内存余弦相似度与语义路径的
// 一次性告警(回退发生、回退超预算)。它们的用量小但分散,
// 独立成文件是为了让 retrieve.go 保持可读(文件 ≤500 行,AGENTS §10)。
// cosineSim 计算两个向量的余弦相似度,由调用方夹到 [0,1]。
// 长度为零或不一致的向量得 0。
@@ -42,3 +42,20 @@ func (r *Retriever) warnSemanticFallback() {
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())
}