path: e1 → e2 → e5 → e6 → e7
- n1养猫,名叫 Mochi← e1· ⊂ path→ 可见
- n2下月搬去成都← e3· e3 ∉ path→ 隐藏
- n3摘要:养猫 Mochi,下月搬去成都← e1,e2,e3· e3 ∉ path→ 连带隐藏
- n4下月搬去杭州← e5· ⊂ path→ 可见
- n5待办:给 Mochi 订航空箱← e7,e1· ⊂ path→ 可见
对话和 Agent 的历史不是日志,是树。用户重新生成一次回复、改一条之前的消息,或者 Agent 框架从检查点分叉,旧的那条线就变成了兄弟分支。只看原始消息的话,按当前分支过滤一下就够了;麻烦在长期记忆:摘要、事实、画像、关系图的边,都是在旧分支还是「当前」的时候生成的,分支被放弃以后它们还留在库里。用户把「搬去成都」改成了「搬去杭州」,记忆里还写着成都。
branchmem 的规则只有一条:派生记忆只有当它的全部来源事件都在当前锚点的祖先路径上时才可见。可见性不看 branch_id 或 inactive 这类标签——标签会过时,来源事件 ID 不会。只要有一条来源在兄弟分支上,整条记忆就隐藏。切回旧分支只移动锚点,不标记、不删除、不改写任何东西,旧分支的记忆自然回来;没有锚点时严格模式什么都不返回,绝不默认放行整棵树。
规则简单,难的是规则管不到的地方。一是全局失效和分支冲突:用户说「以后别再提这件事」,这条作废应该只在当前分支生效,还是对整棵树生效?作废和被作废的记忆落在不同分支上时,以谁为准?这部分还没做完。二是生产里的静默失效:来源事件缺了、旧数据没有拓扑、请求里没带锚点,任何一环没接上,规则都不会报错,只会安静地多放或多挡。所以严格模式下缺信息就一律不放行,兼容模式放行旧数据,但每一条都计数、单独报告。
配套的 branchbench 有 30 个合成中文对话、98 道题,覆盖编辑用户消息、重新生成、从检查点分叉、切回旧分支、分支内改口五种分叉方式。测试集 20 个场景、69 道题,冻结后用 SHA-256 锁住,正式结果只报测试集;评分只看关键词,不用 LLM 判官,可以复现。同一个回答模型、同一条提示词、温度 0、跑 3 次:全量历史泄漏 52.0%,整段摘要 40.2%,Mem0 开源版 60.8%,branchmem 5.9%,和「只给当前分支原文」的上界持平,也说明现有场景偏短,还没体现派生记忆的必要性;正确率 95.7%,基线在 70–80%。
最能说明问题的是 Mem0 那一组:把喂给它的原文换成只含当前分支的版本,泄漏率只从 60.8% 降到 50.0%,因为兄弟分支的事实早就写进了 Mem0 自己的记忆条目。同样的过滤放在全量历史上能挡住 89%。光过滤原文不够,派生出来的东西也得能追回到来源。
代码从 Autarkia 的记忆层整理出来,和原实现做过差分对照:400 棵随机打乱到达顺序的树上,约 10.7 万次可见性判定结果完全一致。局限也写在仓库里:场景是合成的,leak 题只有 34 道,只用了一个回答模型;有 2 道题的禁止词出现在问题本身,是跑完才发现的,测试集没有改,另附了去掉它们的敏感性分析。