fix(memory): 场景召回按块向量余弦排序 + 召回 query 可见性 + 对话回执放宽

① 场景召回是每轮常驻注入,此前按「场景权重→时间倒序」排,与 query 无关:
   老图库的 rt/rtchild/inputch 等历史事实常年霸榜,实测 1833 字符里九成是它们,
   刚聊的群聊内容一句没有 —— 这就是"忘事"的机制。
   块里本来就存着向量,相关性就是一次余弦比较:RecallByScene 带出端点/原句块
   向量,Indexer 按 max(cos) 排序并设 0.20 下限,取前 8。
   无稠密空间时保持原顺序(不能用字面匹配冒充语义 —— 既有判据要求
   「措辞与记忆零字面重合时场景路必须兜住」)。

② recall query 可见性:cleaner 缺失/生效/回空三种情况都打日志。此前静默回退
   原文,"召回 query 其实是工具原始回执"这件事在日志里完全看不到。
   实测 qq_get_message: 199 -> 27 bytes(cleaner 生效)。

③ 对话正文类工具(get_message/get_history/list_chats)时间线渲染放宽到 400 字
   (160 字会把群聊上下文切成碎片)。

判据:internal/memory/scene_relevance_test.go
This commit is contained in:
JianFeeeee
2026-10-27 13:25:25 +08:00
parent bd066a627a
commit b2938ed4a7
6 changed files with 249 additions and 5 deletions

View File

@ -391,6 +391,7 @@ func (g *GraphDB) RecallByScene(scenes []string, limit int) (*SceneRecall, error
relQuery := `SELECT e.id, sb.id, tb.id, sb.text_content, tb.text_content,
e.edge_type, e.confidence, COALESCE(e.status, ''), e.session_id,
e.turn_id, e.created_at, COALESCE(srt.text, ''),
COALESCE(sb.vector, ''), COALESCE(tb.vector, ''), COALESCE(srt.vector, ''),
MAX(sr.weight) AS w
FROM scene_refs sr
JOIN scenes s ON sr.scene_id = s.id
@ -398,7 +399,7 @@ func (g *GraphDB) RecallByScene(scenes []string, limit int) (*SceneRecall, error
JOIN memory_blocks sb ON sb.id = e.source_id
JOIN memory_blocks tb ON tb.id = e.target_id
LEFT JOIN (
SELECT c.target_id AS edge_id, MIN(b.text_content) AS text
SELECT c.target_id AS edge_id, MIN(b.text_content) AS text, MIN(b.vector) AS vector
FROM memory_block_edges c
JOIN memory_blocks b ON b.id = c.source_id
WHERE c.edge_type = 'contains' AND c.target_kind = 'edge'
@ -420,13 +421,25 @@ func (g *GraphDB) RecallByScene(scenes []string, limit int) (*SceneRecall, error
// relations 行号,块体系下无意义)。
// ★ DateBucket / SentenceID 不再有来源:旧表退场后边表不存这两列,
// 日期可从 created_at 推,原句 ID 由 contains 边回溯得到。
var srcVecRaw, tgtVecRaw, senVecRaw string
if err := rows.Scan(&rel.ID, &rel.SourceBlockID, &rel.TargetBlockID,
&rel.SourceName, &rel.TargetName,
&rel.RelationType, &rel.Confidence, &rel.Status, &rel.SessionID,
&rel.TurnID, &rel.CreatedAt, &rel.SentenceText, &w); err != nil {
&rel.TurnID, &rel.CreatedAt, &rel.SentenceText,
&srcVecRaw, &tgtVecRaw, &senVecRaw, &w); err != nil {
rows.Close()
return nil, err
}
// 块向量(算场景相关度用):解码失败就当没有,不影响召回本身。
if v, err := decodeVector(srcVecRaw); err == nil {
rel.SourceVec = v
}
if v, err := decodeVector(tgtVecRaw); err == nil {
rel.TargetVec = v
}
if v, err := decodeVector(senVecRaw); err == nil {
rel.SentenceVec = v
}
// 旧表退场后边表不存 date_bucket,从 created_at 推日期桶。
if rel.DateBucket == "" && !rel.CreatedAt.IsZero() {
rel.DateBucket = rel.CreatedAt.Format("2006-01-02")