0edfc948ba
读路径(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>
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)
|
||
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)
|
||
}
|
||
}
|