真因(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;停用缓存 ⇒ 重复嵌入。
44 lines
1.2 KiB
Go
44 lines
1.2 KiB
Go
package memory
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/embedding"
|
|
)
|
|
|
|
// 本文件收拢排名里的**关键词侧**:TF-IDF 打分与用于匹配的索引文本。
|
|
// 它们与语义侧(retrieve_semantic.go / retrieve_fallback.go)对称,
|
|
// 拆开的目的是让 retrieve.go 保持可读(文件 ≤500 行,AGENTS §10)。
|
|
|
|
// keywordScores 用内存内 TF-IDF 倒排索引(embedding.Index)为每条条目
|
|
// 与 query 打分,复用 embedding 包的分词器。得分
|
|
// 以最大值为基准归一化到 [0,1],以便与语义
|
|
// 得分可比。
|
|
func (r *Retriever) keywordScores(items []MemoryItem, query string) map[string]float64 {
|
|
scores := make(map[string]float64, len(items))
|
|
index := embedding.NewIndex[string]()
|
|
for _, it := range items {
|
|
index.Add(keywordText(it), it.ID)
|
|
}
|
|
hits := index.Search(query)
|
|
if len(hits) == 0 {
|
|
return scores
|
|
}
|
|
max := hits[0].Score
|
|
if max <= 0 {
|
|
return scores
|
|
}
|
|
for _, h := range hits {
|
|
scores[h.ID] = h.Score / max
|
|
}
|
|
return scores
|
|
}
|
|
|
|
// keywordText 是用于关键词匹配的索引文本:content 加 tags。
|
|
func keywordText(it MemoryItem) string {
|
|
if len(it.Tags) == 0 {
|
|
return it.Content
|
|
}
|
|
return it.Content + " " + strings.Join(it.Tags, " ")
|
|
}
|