7ae7f7be67
审计三真桩之一。记忆召回此前打分只有 Recency+Importance,缺 Relevance(对当前 任务的语义相关性)——注释写"待接 Milvus",但召回时甚至不知道当前问什么。 关键发现:dispatcher 注入点 fetchMemory(ctx,uid,_) 手上已有当前任务文本(b.query), 只是被 `_` 丢弃了。所以不是"接 Milvus"那么重,把 query 一路传下去 + 缓存嵌入即可。 设计(偏离注释的"接 Milvus"——用户偏好量小,不值当上向量库): - Profile 加 embedding 列(float32 小端打包存 bytea);Upsert 时对 value 向量化缓存 (value 没变不重算,失败留空不阻断)。 - memory 包定义 Embedder 小接口,gateway 注入 rag.Engine(复用同一控制面下发的 embedding 模型),不硬依赖 rag 内部;rag.Engine 加导出 Embed 方法。 - memory_get 工具加可选 query 入参;fetchMemory 停止丢弃 b.query 传下去。 - Get(ctx,uid,query):query 非空且 embedder 就绪 → embed(query) 对每条缓存向量 内存算余弦 → 三项打分 0.25R+0.35I+0.4Rel;否则回落两项(升级前行为)。 - 优雅降级贯穿:无 query/无 embedder/query 嵌入失败/行无向量 → 静默回落,绝不报错。 零 Milvus 依赖、零向量库同步问题、保住"没 embedding 也能跑"。 验证:单测(编解码往返/cosine 截0/三项模式相关性翻转顺序/降级返 nil)+ 端到端 (真 PG:写入即向量化、query=咖啡把低重要度的咖啡记忆翻到运动前面)。migration 加列已 live;embedding 复用 RAG 已验证基建。三模块 build/vet/test 全绿。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
51 lines
1.8 KiB
Go
51 lines
1.8 KiB
Go
package memory
|
||
|
||
import (
|
||
"testing"
|
||
"time"
|
||
)
|
||
|
||
func TestRankProfiles(t *testing.T) {
|
||
now := time.Date(2026, 6, 22, 12, 0, 0, 0, time.UTC)
|
||
rows := []Profile{
|
||
{Key: "高重要近期", Value: "a", Importance: 9, LastSeenAt: now.AddDate(0, 0, -1)},
|
||
{Key: "低重要久远", Value: "b", Importance: 2, LastSeenAt: now.AddDate(0, 0, -60)},
|
||
{Key: "中等", Value: "c", Importance: 5, LastSeenAt: now.AddDate(0, 0, -10)},
|
||
}
|
||
ranked := rankProfiles(rows, now, 0, nil)
|
||
if ranked[0].Key != "高重要近期" || ranked[2].Key != "低重要久远" {
|
||
t.Errorf("应按 Score 降序:高重要近期 > 中等 > 低重要久远,得 %s/%s/%s", ranked[0].Key, ranked[1].Key, ranked[2].Key)
|
||
}
|
||
// 截断 top-N
|
||
if got := rankProfiles(rows, now, 2, nil); len(got) != 2 || got[0].Key != "高重要近期" {
|
||
t.Errorf("top-2 截断错: %d 条 首=%s", len(got), got[0].Key)
|
||
}
|
||
// 原切片不被改动(rankProfiles 应 copy)
|
||
if rows[0].Key != "高重要近期" {
|
||
t.Error("rankProfiles 不应修改入参顺序")
|
||
}
|
||
}
|
||
|
||
func TestRecencyDecay(t *testing.T) {
|
||
now := time.Date(2026, 6, 22, 12, 0, 0, 0, time.UTC)
|
||
if recencyScore(now, time.Time{}) != 1.0 {
|
||
t.Error("无 last_seen 应视为新鲜=1")
|
||
}
|
||
fresh := recencyScore(now, now.AddDate(0, 0, -1))
|
||
old := recencyScore(now, now.AddDate(0, 0, -30))
|
||
if !(fresh > old && old > 0) {
|
||
t.Errorf("越久越低且 >0: fresh=%v old=%v", fresh, old)
|
||
}
|
||
}
|
||
|
||
func TestProfileScore_DefaultImportance(t *testing.T) {
|
||
now := time.Now()
|
||
// importance=0(旧/未评分)应按兜底 5 计,而不是 0(否则被不公平遗忘)。
|
||
p := Profile{Importance: 0, LastSeenAt: now}
|
||
got := profileScore(p, now, nil)
|
||
want := wRecency*1.0 + wImportance*(defaultImportance/10)
|
||
if got != want {
|
||
t.Errorf("未评分行应用兜底 importance: got %v want %v", got, want)
|
||
}
|
||
}
|