真因(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;停用缓存 ⇒ 重复嵌入。
62 lines
2.4 KiB
Go
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())
|
|
}
|