9c19bb44f1
memorize 的 TODO 落地:写回阶段(异步、离热路径)从本轮对话用 LLM 抽取用户 长期稳定偏好 → 与已有画像去重 → memory_upsert 登记。 - extractMemory:模型/工具不可用或输入过短则跳过;复用 llmCtx 超时; 抽取 prompt 只取长期偏好、忽略一次性信息。 - 纯逻辑(可单测):parsePrefs(容忍 json 代码围栏)、parseProfile(把 memory_get 渲染的"- 维度:值"解析回 map,兼容全/半角冒号)、filterNewPrefs(新增/变更才留, 同批同 key 去重)。 - 单测覆盖三者;LLM 抽取调用沿用已验证的 pool.Chat 模式。 至此记忆闭环:召回(memory_get) + 历史写回 + 偏好自动抽取 全通。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
44 lines
1.4 KiB
Go
44 lines
1.4 KiB
Go
package eino
|
|
|
|
import "testing"
|
|
|
|
func TestParsePrefs(t *testing.T) {
|
|
got := parsePrefs("```json\n[{\"key\":\"称呼\",\"value\":\"Dexter\"},{\"key\":\"语言\",\"value\":\"中文\"},{\"key\":\"\",\"value\":\"空\"}]\n```")
|
|
if len(got) != 2 {
|
|
t.Fatalf("应解析出 2 条(过滤空 key),got %d: %v", len(got), got)
|
|
}
|
|
if got[0].Key != "称呼" || got[0].Value != "Dexter" {
|
|
t.Errorf("解析错: %v", got[0])
|
|
}
|
|
if parsePrefs("不是 JSON") != nil {
|
|
t.Error("非 JSON 应返回 nil")
|
|
}
|
|
}
|
|
|
|
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")
|
|
}
|
|
}
|
|
|
|
func TestFilterNewPrefs(t *testing.T) {
|
|
existing := map[string]string{"称呼": "Dexter", "语言": "中文"}
|
|
in := []Pref{
|
|
{"称呼", "Dexter"}, // 已有且相同 → 跳
|
|
{"语言", "英文"}, // 已有但变了 → 留
|
|
{"职业", "律师"}, // 新 → 留
|
|
{"职业", "工程师"}, // 同批重复 key → 跳(保留首个)
|
|
}
|
|
got := filterNewPrefs(in, existing)
|
|
if len(got) != 2 {
|
|
t.Fatalf("应剩 2 条(语言变更 + 新职业),got %d: %v", len(got), got)
|
|
}
|
|
if got[0].Key != "语言" || got[0].Value != "英文" || got[1].Key != "职业" || got[1].Value != "律师" {
|
|
t.Errorf("过滤结果不符: %v", got)
|
|
}
|
|
}
|