mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-03 07:47:45 +00:00
① 场景召回是每轮常驻注入,此前按「场景权重→时间倒序」排,与 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
73 lines
2.8 KiB
Go
73 lines
2.8 KiB
Go
package memory
|
||
|
||
// 场景召回必须按**块向量余弦**与当前 query 相关,而不是按历史热度/时间。
|
||
//
|
||
// 现场(2026-10-27):群里聊平板/机型,注入的 1833 字记忆里九成是
|
||
// `rt 标签 / rtchild / inputch` 这类老图库高频三元组 —— 模型"忘事"就是
|
||
// 这么来的。块里本来就存着向量,所以相关性只是一次余弦比较。
|
||
|
||
import (
|
||
"math"
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
func TestDenseCosineBasics(t *testing.T) {
|
||
if got := denseCosine([]float64{1, 0}, []float64{1, 0}); math.Abs(got-1) > 1e-9 {
|
||
t.Fatalf("同向余弦应为 1,实际 %v", got)
|
||
}
|
||
if got := denseCosine([]float64{1, 0}, []float64{0, 1}); math.Abs(got) > 1e-9 {
|
||
t.Fatalf("正交余弦应为 0,实际 %v", got)
|
||
}
|
||
if got := denseCosine([]float64{0, 0}, []float64{1, 0}); got != 0 {
|
||
t.Fatalf("零向量余弦应为 0,实际 %v", got)
|
||
}
|
||
}
|
||
|
||
// fakeEmbedder 把文本映射成固定向量:包含 "平板" ⇒ 与 query 同向;
|
||
// "rt" ⇒ 正交。用来验证"闸门按语义而不是字面"。
|
||
type fakeEmbedder struct{}
|
||
|
||
func (fakeEmbedder) Loaded() bool { return true }
|
||
func (fakeEmbedder) Dim() int { return 2 }
|
||
func (fakeEmbedder) Fingerprint() string { return "fake" }
|
||
func (fakeEmbedder) VectorizeDense(text string) ([]float64, error) {
|
||
if hasSub(text, "平板") || hasSub(text, "机型") {
|
||
return []float64{1, 0}, nil
|
||
}
|
||
return []float64{0, 1}, nil
|
||
}
|
||
func (fakeEmbedder) Vectorize([]string) [][]float64 { return nil }
|
||
|
||
// EmbedImageDense / Close 满足 vector.MultimodalEmbedder 的完整接口
|
||
// (测试只用文本路;图片与生命周期在真空间里才有意义)。
|
||
func (fakeEmbedder) EmbedImageDense([]byte, string) ([]float64, error) { return nil, nil }
|
||
|
||
func (fakeEmbedder) Close() {}
|
||
|
||
func hasSub(s, sub string) bool {
|
||
return strings.Contains(s, sub)
|
||
}
|
||
|
||
func TestRankSceneRelationsByVectorDropsUnrelated(t *testing.T) {
|
||
idx := NewIndexer(nil)
|
||
idx.SetDenseSpace(fakeEmbedder{})
|
||
rels := []Relation{
|
||
{SourceName: "rt标签", TargetName: "内核", SourceVec: []float64{0, 1}, TargetVec: []float64{0, 1}},
|
||
{SourceName: "平板", TargetName: "promax", SourceVec: []float64{1, 0}, TargetVec: []float64{0.9, 0.1}},
|
||
}
|
||
kept := idx.rankSceneRelationsByVector("群友在聊平板机型", rels)
|
||
if len(kept) != 1 || kept[0].SourceName != "平板" {
|
||
t.Fatalf("只应保留与 query 语义相关的三元组,实际 %+v", kept)
|
||
}
|
||
}
|
||
|
||
func TestRankSceneRelationsByVectorNoEmbedderKeepsOrder(t *testing.T) {
|
||
idx := NewIndexer(nil) // 未注入稠密空间
|
||
rels := []Relation{{SourceName: "a"}, {SourceName: "b"}}
|
||
kept := idx.rankSceneRelationsByVector("随便问问", rels)
|
||
if len(kept) != 2 || kept[0].SourceName != "a" {
|
||
t.Fatalf("无稠密空间时应保留原顺序(不能用字面匹配冒充语义),实际 %+v", kept)
|
||
}
|
||
}
|