Files
sundynix-agentix/sundynix-mcp-go/internal/memory/rank_test.go
T
Blizzard 0edfc948ba 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>
2026-06-22 14:48:11 +08:00

51 lines
1.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}
}