feat(memory): P2 读路径打分 + 桌面端记忆面板(看/改/删)
读路径(Generative Agents 公式的 Recency+Importance 两项,Relevance 待 P3): - memory.Get 改为 rankProfiles:Score=0.4·Recency+0.6·Importance,按分降序、截断 top-30。 Recency=0.98^天 指数衰减;未评分行用兜底 importance=5(不被不公平遗忘)。纯函数 + 单测。 - 新增 Store.List(结构化、不截断)+ memory_list 工具。 桌面端记忆面板: - gateway GET /memory(列表) + DELETE /memory?key=(软删),受保护组。 - api listMemory/deleteMemory;MemoryView 右侧从占位 → 真列表:按分排序展示 key/value/重要度/最近时间,可内联编辑(PUT)与删除(软删);左侧登记后自动刷新。 实测:PUT 两条 → GET 返回带 importance/last_seen 的有序列表(较新者靠前)→ DELETE 一条 (软删)→ 再 GET 已消失。rank/recency/兜底 importance 单测过;前端 tsc+构建通过。 至此记忆:召回(打分) + 历史 + 自动对账(P1) + 用户可管控(面板) 闭环。Relevance(Milvus) 留 P3。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
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)
|
||||
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); 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)
|
||||
want := wRecency*1.0 + wImportance*(defaultImportance/10)
|
||||
if got != want {
|
||||
t.Errorf("未评分行应用兜底 importance: got %v want %v", got, want)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user