记忆不能给自己的猜测盖章

题记:记住一句话不难,难的是别把“我推断”存成“你说过”。
1. 最危险的幻觉,是下次还能搜到的那种⌗
设想一个虚构场景:用户问“这种缓存策略是不是不合适?”Agent 分析后认为,缓存应该关闭。
如果系统把这轮讨论压成“用户要求禁用缓存”,错误就换了身份。它不再是一条可以继续争论的分析,而是一条下次会被检索出来、带着引用出现的用户要求。
向量检索越准,反而越容易稳定地找回它。
这类问题不只是摘要质量差。它混淆了三个命题:
| 命题 | 需要的依据 |
|---|---|
| 用户说过某句话 | 对应的人类消息 |
| 用户认可某个 Agent 结论 | 结论所在的过程,以及用户认可的证据 |
| 结论在客观上正确 | 相应的验证材料 |
三者不能互相代替。用户认可不等于技术验证;测试成功不等于用户认可;用户问了一个问题,更不等于已经作出决定。
这次记忆改造值得讨论的地方,不是又加了一位模型来“审核事实”。恰好相反:即时写入撤掉了 LLM 准入审核,收窄成一个确定性的原话检查;复杂判断留给读取完整过程的后台抽取。
2. 即时写入:先证明这是本轮说的⌗
主动记忆工具的入口变成了:
remember(quote, content, category?, replaces?, project?)
quote 是逐字引文,content 是脱离当前上下文也能读懂的改写。执行时,工具不去整段历史里搜索一个看起来相似的句子,而是只查运行时提供的本轮人类消息集合。
匹配规则非常朴素。下面是等价简化,不是完整接口:
const normalize = (text: string) => text.replace(/\s+/g, ' ').trim()
const source = currentHumanMessages.find(message =>
normalize(message.text).includes(normalize(quote))
)
if (!source) reject()
空引文在前置检查中被拒绝。允许的变化只有空白归一化,不接受同义替换,也不允许把两条消息拼成一条“原话”。匹配成功后,出处指向命中的消息,而不是随手挂到最近一条消息上。
这里真正重要的是集合怎么来。
运行时把本轮真实人类输入与已经送达的插话交给工具。任务结果、定时触发、唤醒通知,即使在消息协议里占着 user 角色,也不能因此变成人类证词。识别过程还排除了组件模板和隔离待处理输入等来源。
这堵住了一个很实际的缝隙:消息角色是传输格式,不是作者身份证。
没有本轮原话时,工具仍然存在,但执行会失败。后台任务不会因为“工具列表里有 remember”,就自然获得替用户作证的资格。纠正旧事实的 replaces 参数也不例外,必须先过同一道原话检查。
3. 这道门证明的,比它看起来少⌗
必须说清楚:这不是语义正确性验证器。
假设当前消息里确实出现“先比较两个方案”,Agent 把它作为合法引文,却把 content 写成“用户永久禁止第二个方案”。字符串检查照样可能通过。
它确定性证明的是:
这段引文存在于本轮合格的人类消息中。
它没有证明:
改写由引文蕴含,且没有扩大适用范围。
同样,逐字匹配本身不会识别所有“把提问改成决定”的错误。工具契约要求不这样做,但即时写入的这条执行路径没有调用语义审核模型来逐条判定。
这是取舍,不是遗漏了一个万能组件:确定性的来源门负责拦住没有当前原话的调用;调用模型仍负责忠实改写。后台路径另外承担上下文判断。
因此,不能把这次改动宣传成“事实终于可信了”。更准确的说法是:一类没有来源的即时写入,现在可以直接拒绝,不必靠另一个模型猜。
4. Agent 的结论不消失,只是不准自己晋升⌗
如果即时工具只收用户原话,Agent 验证出的知识怎么办?
答案不是删掉,也不是换个文件偷偷写进去。先留在对话里,等后台读取稳定的讨论过程。
后台抽取区分两种事实来源。实现中的标签沿用系统命名,含义如下:
| 标签 | 含义 | 需要的消息指针 |
|---|---|---|
max-stated | 用户亲口陈述 | source 指向陈述消息 |
max-accepted | Agent 提出、用户认可的结论 | source 指向引出结论的人类消息,acceptedBy 指向认可消息 |
unknown | 没有已确认的来源类型 | 不冒充前两种,旧数据默认归此类 |
抽取提示词要求:沉默、换话题、单纯提问、机器事件,以及 Agent 自己宣布验证成功,都不算认可。未获认可的探索可以进入 episode,也就是对话过程摘要,但不能按这条路径成为已认可的 fact。
确定性验证随后检查消息指针:来源必须是合格的人类消息;认可指针不能指向 Agent、框架事件、纯提问或不存在的位置;来源与认可还必须属于同一会话代。
不过,一条人类消息是不是在认可这个特定结论,仍由模型解释。 指针合法不代表语义关联正确。当前数据结构也没有把“引出问题—Agent 结论—认可范围”完整编码成一条可机械证明的链。
这套设计的价值,是把“谁说的”和“谁认可的”变成不同字段,而不是声称已经消除了自然语言歧义。
它也有明确代价:无人跟进的技术发现,即使看起来正确,也可能只留在可检索的过程摘要中,不能立即成为结构化事实。这里优先保护的是归因,不是最大化 facts 的产量。
5. 先读完,再提交:不要把第一版猜测抢跑入库⌗
完整过程可能装不进一次模型调用,所以抽取必须分段。
一个很自然、也很危险的实现是:读完一段,抽几条事实,马上存。后面发现前面的结论错了,再想办法修。
这次实现走另一条路:每段输出的是累计候选的完整新状态。后段可以改写或删除前段候选,最后才一次提交。
第一段 ──► 候选集 A
│
第二段 ──► 修正后的候选集 B
│
最后段 ──► 最终候选集 C
│
证据指针检查
│
▼
facts + episode + 消息标记
同一事务提交
extraction.test.ts 用脚本化模型构造了多段修订:前段给出待确认候选,最后一段才返回被认可的结论。断言不仅要求最后留下正确候选,还检查了中间调用时,消息尚未被标记为已抽取。
另一个测试让后续分段抛错,要求结果是:没有临时事实落库,消息也仍未消费。
事务里还会检查抽取期间是否出现了新的待处理消息,以及目标消息是否已被另一个抽取者占用。对应测试覆盖了对话恢复和并发抽取的情形。
这些检查保护的是同一个边界:模型看过一部分,不等于系统已经有资格宣布整段讨论定论了。
这里的“稳定”是可以提交的处理边界,不是永远不会再被纠正的哲学保证。
6. 合并不能给证据洗白⌗
入口做对了,夜间整理仍然可能把它做坏。
想象两条虚构事实:一条来源不明的旧推断,一条有直接人类陈述支持的新规则。整理模型觉得它们相似,把两条合成一条流畅的表述,再沿用较强来源。
旧推断就被顺带认证了。
本次实现为此引入了一个保守顺序:
unknown < max-accepted < max-stated
这是归因强度的顺序,不是客观真实性的排名。用户亲口说的技术判断未必比 Agent 实测的结论更正确;但若要声称“这是用户说的”,直接陈述确实比认可别人的结论更强。
weakestFactEvidence 在合并时取较弱来源。多事实替换会检查参与合并的事实,继承其中最弱的来源类型及对应指针;整理路径的单条替换也有防止来源升级的处理。
夜间整理的测试专门构造了 unknown 与 max-stated 合并的情况,要求结果仍保留较弱证据。数据库迁移也没有把历史事实批量补成“用户说过”,而是将新增类型字段默认设为 unknown。
这是整个改动里我最赞同的一点:
文本可以越整理越短,证据不能越整理越强。
当然,取最弱来源会损失精度。合并后的大句子里,某些分句可能有直接证据,另一些没有;单一标签只能整体降级。若未来需要精确表达这些差别,应保留更细的命题与证据关系,而不是直接挑最强标签。
7. 测试守住的是边界,不是模型的理解力⌗
这次改动的测试很适合按“拒绝了什么”阅读:
| 测试组 | 明确要求的行为 |
|---|---|
| 原话入口 | 空引文、语义近似、跨消息拼接都不能通过 |
| 来源身份 | 任务结果和其他机器事件不能冒充人类输入 |
| 即时纠正 | replaces 不能绕过原话检查 |
| 后台证据 | Agent 消息、纯提问、越界指针不能充当认可证据 |
| 分段抽取 | 后段能撤回前段候选;后段失败不留下半批事实 |
| 夜间整理 | 混合来源不能被合并成更强证据 |
需要避免过度解读。使用脚本化模型的测试可以证明:当模型给出缺失或非法的证据指针时,程序会拒绝它;不能证明真实模型总能正确理解“继续”是在认可哪一个结论。
文中没有线上错误率、召回率或幻觉降低比例。读到的代码与测试足以解释机制,不足以编出效果数字。
8. 记忆系统不该只问“要不要记”⌗
如果把记忆设计成“模型提炼一句话,然后存下来”,最容易丢失的恰恰是那句话的身份:它是提问、假设、观察、认可,还是决定?
这次改造提供了一个更好的分工:
- 即时入口问:本轮真有这段人类原话吗?
- 后台抽取问:完整讨论最终支持保留什么?
- 存储层问:来源类型和证据指针有没有一起保存?
- 整理层问:合并有没有偷偷抬高来源强度?
它不是一套事实真伪裁判。用户可以说错,Agent 可以误读认可,合法引文也可能被过度改写。这些剩余问题都应该明说。
但相比给摘要器多写一句“请确保准确”,这里已经把一部分责任变成了可执行的边界。
长期记忆最重要的能力,不是把每句话都记下来,而是在下一次引用时,仍然知道自己凭什么这样说。
代码依据:落地版本 8cc2236 的 skills/memory/tools.ts、skills/memory/SKILL.md、src/write-routing/max-quote.ts、src/runtime/turn-runner.ts、src/memory/extraction.ts、src/memory/fact-evidence.ts、src/memory/fact-store.ts 与迁移 0550_fact_evidence.sql。测试依据为 remember-quote.test.ts、fact-evidence.test.ts、extraction.test.ts、night-consolidation.evidence.test.ts;本文描述的是这些测试规定的断言,不是另行执行所得的成绩。