Files
sundynix-agentix/sundynix-dispatcher/internal/eino/memory_extract_test.go
T
Blizzard 1674252d81 feat(memory): P1 长期记忆升级 —— 异步攒批 Consolidate + 软删 + importance/last_seen
把"逐轮盲写抽取"升级为 Mem0 式对账(方案见 memory_industry_analysis.md 落地节):

mcp-go:
- Profile 加 Importance(1~10, poignancy) + LastSeenAt(为 Generative Agents 读路径
  Score=w1·Relevance+w2·Recency+w3·Importance 铺路)。
- Upsert 收 importance + 每次置 last_seen(印证);新增 Delete(软删,BaseModel.DeletedAt
  已具备,失效不物删可审计)+ Touch;memory_upsert 透传 importance、新增 memory_delete 工具。

dispatcher:
- extractMemory → consolidateMemory:一次 LLM 调用同时做 抽取+对账,输出
  [{op:ADD|UPDATE|DELETE|NOOP,key,value,importance}];ADD/UPDATE→upsert、DELETE→软删;
  sanitizeOps 防幻删(DELETE 须命中已有)/夹 importance[1,10]/同key保末个/丢 NOOP。
- 攒批:每 3 轮(per-session 计数)才 consolidate 一次,省成本,对齐 ChatGPT 周期整理。
  从根上解决 exact-key 盲写的记忆腐烂。

验证:parseOps/sanitizeOps/parseProfile 纯逻辑单测;store 集成测试(真 PG)覆盖
importance/last_seen 写入 + 软删(live 0 / 物理 1);dispatcher -race 全过。
(注:完整多轮 LLM consolidate 未做实跑,属构造性验证 + 沿用已证 pool.Chat 模式。)

P2 待做:读路径按 Score(Recency+Importance) 排序/衰减/截断 + 桌面端记忆面板。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 14:27:16 +08:00

58 lines
2.4 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 eino
import "testing"
func TestParseOps(t *testing.T) {
got := parseOps("```json\n[{\"op\":\"ADD\",\"key\":\"称呼\",\"value\":\"Dexter\",\"importance\":7},{\"op\":\"DELETE\",\"key\":\"旧公司\"}]\n```")
if len(got) != 2 || got[0].Op != "ADD" || got[0].Key != "称呼" || got[0].Importance != 7 || got[1].Op != "DELETE" {
t.Fatalf("parseOps 解析错: %+v", got)
}
if parseOps("不是 JSON") != nil {
t.Error("非 JSON 应返回 nil")
}
}
func TestSanitizeOps(t *testing.T) {
existing := map[string]string{"语言": "中文", "旧公司": "A厂"}
in := []memOp{
{Op: "add", Key: "重要事", Value: "X", Importance: 99}, // 大小写规范 + importance 夹到 10
{Op: "UPDATE", Key: "语言", Value: "英文", Importance: 0}, // importance 提到 1
{Op: "ADD", Key: "空值", Value: ""}, // 无 value → 丢
{Op: "DELETE", Key: "旧公司"}, // 命中已有 → 留
{Op: "DELETE", Key: "不存在"}, // 未命中 → 丢(防幻删)
{Op: "NOOP", Key: "语言"}, // NOOP → 丢(不覆盖已收的 UPDATE)
{Op: "ADD", Key: "称呼", Value: "Dexter", Importance: 7},
{Op: "ADD", Key: "称呼", Value: "Dex", Importance: 3}, // 同 key 重复 → 保留末个(值+importance
}
out := sanitizeOps(in, existing)
if len(out) != 4 {
t.Fatalf("应剩 4 条(重要事/语言/删旧公司/称呼),得 %d: %+v", len(out), out)
}
byKey := map[string]memOp{}
for _, o := range out {
byKey[o.Key] = o
}
if byKey["重要事"].Importance != 10 {
t.Errorf("重要事 importance 应夹到 10: %+v", byKey["重要事"])
}
if byKey["语言"].Op != "UPDATE" || byKey["语言"].Importance != 1 {
t.Errorf("语言 应为 UPDATE 且 importance 提到 1: %+v", byKey["语言"])
}
if byKey["旧公司"].Op != "DELETE" {
t.Errorf("旧公司 应保留 DELETE: %+v", byKey["旧公司"])
}
if byKey["称呼"].Value != "Dex" || byKey["称呼"].Importance != 3 {
t.Errorf("称呼 应取末个 opDex/3: %+v", byKey["称呼"])
}
}
func TestParseProfile(t *testing.T) {
m := parseProfile("- 称呼:Dexter\n- 语言: 中文\n\n- 职业:律师")
if m["称呼"] != "Dexter" || m["语言"] != "中文" || m["职业"] != "律师" {
t.Errorf("画像解析错: %v", m)
}
if len(parseProfile("")) != 0 {
t.Error("空画像应得空 map")
}
}