智能体记忆需要区分事实与获知时间
文章以联系人迁居为例,说明智能体记忆应同时记录事实何时生效、系统何时获知。它进一步区分现实状态变化与历史录入错误,指出仅靠两个时间字段仍不足以表达完整的修订语义。
AI 深度解读
联系人 10 月 15 日搬到 Berlin,团队 11 月 1 日才知道。此时再问 AI「他 10 月 20 日住在哪」,你要的是后来补齐的事实,还是团队当时掌握的信息?如果 CRM 只存最后一次保存时间,这两种问题就会被混成一个。
Palo Alto AI Research Lab 的 AI 联合创始人 Mycroft 分享了一次用 SQLite 改造 Agent 记忆的实践。结论是:想解释 AI 为什么昨天和今天回答不同,必须同时保存事实生效时间、系统获知时间,并明确标记哪些旧记录属于错误。
两个时间轴,才能分清事实和当时的认知
双时态数据(bitemporal data)记录两件事:一个事实在现实中何时成立,系统又在何时知道它。
放到搬家案例里,10 月 15 日是事实生效时间,11 月 1 日是获知时间。按照后来掌握的信息,联系人 10 月 20 日已经住在 Berlin;按照团队 10 月 25 日掌握的信息,当时仍会回答旧城市。两次查询条件不同,答案可以不同。
这个设计并不新,SQL:2011 已包含相关能力,XTDB 也围绕它构建。作者指出,很多 Agent 记忆方案仍然只存其中一个时间维度。
但加上两个日期还没完。Codex 在审查作者的初版设计时追问:如何区分「世界变了」和「之前记错了」?
客户从有意向变成冷淡,旧状态曾经成立,只需结束它的有效期。如果一开始就录错状态,旧值从未成立,需要撤回(retraction)。旧记录仍要保留,否则无法回答「纠正之前,我们相信什么」。
更新描述事实的变化,撤回描述认知的纠错。
作者参考了两种实现。Zep / Graphiti 为事实保存四个时间戳,分别描述事实生效、失效、图谱获知和图谱停止采用的时间;它还保存支撑事实的消息、文档等 episodes,并用 reference_time 将「上周」对齐到消息日期。
作者阅读的 Mem0 开源版则保存旧值、新值、操作和写入时间。这能追踪编辑,却缺少事实有效期,无法同时按事实日期 D 和获知日期 K 查询。它的抽取步骤值得借鉴:比较新事实与相似记忆,再选择 ADD、UPDATE、DELETE 或 NOOP。
最终,作者采用了 Zep 的双时间轴和证据关联方式,以及 Mem0 的写入决策入口,存储仍然使用 SQLite。
区块链能保留历史,为什么还不够
区块链的追加日志、状态根、按区块高度查询和回滚机制,看起来很适合保存记忆。但对链内交易来说,「发生」与「被记录」通常是同一件事:交易进入区块才成立。区块时间戳由出块者在允许范围内设置,也不等于现实事件发生的时间。
「15 日搬家,1 日才得知」属于链外事实。即使通过预言机写入,搬家日期也只是载荷中的字段,链本身不会验证它。
作者还指出,追加一笔纠正交易,并不自动区分事实变化与旧值错误;最终确认前的链重组,也不会天然保留某个节点此前相信的历史。更贴近日常开发的类比是 Git:提交同时有 author date 和 committer date。不过,两类日期只是入口,完整查询仍需额外的数据规则。
因此,作者只借用了三种存储习惯:历史日志只追加,由触发器阻止 UPDATE 和 DELETE;每条历史记录有确定性 ID,重复回放成为空操作;当前状态作为日志之上的快照,让现有读取保持快速。
他们的前提是「一个写入者、一个文件」,没有引入共识、Merkle trees 或 hash chains,磁盘损坏交给 integrity_check 和备份处理。
SQLite 怎么补历史,又不改坏现有读取
作者没有给所有业务表添加 valid_from、valid_to,以免影响主键和现有读取代码,而是在同一数据库、同一事务中,用触发器把字段变化写进 outbox。
下面的触发器负责捕获客户状态变化,并优先采用写入者提供的事实生效时间:
CREATE TRIGGER leads_hist_u_status AFTER UPDATE OF status ON leads
WHEN old.status IS NOT new.status BEGIN
INSERT INTO object_history(subject, field, value_json, old_json, valid_from, operation)
VALUES ('leads:' || new.lead_slug, 'status', json_quote(new.status), json_quote(old.status),
coalesce((SELECT valid_from FROM history_ctx LIMIT 1),
strftime('%Y-%m-%dT%H:%M:%fZ','now')), 'U');
END;
重点看 coalesce:通话解析器或 CRM 操作者知道真实日期,就在事务内写入 history_ctx;不知道时,默认采用当前写入时间。没有证据就不补造过去。
夜间 relay 再把 outbox 转入统一事件日志:值变化就关闭旧有效期、开启新有效期,删除就结束有效期,相同值重复写入则不产生变化。
触发器捕获不了另一类信息:补充过去,却不改变现在。例如现在住在 Berlin,后来才知道 10 月 1 日至 14 日住在 Prague。当前城市没有更新,必须显式写入历史片段:
segment --subject people:ivan --field city --value Prague --from 2026-10-01 --to 2026-10-15
注意 --to 2026-10-15 对应原文所说的截至 14 日。该操作将原有效期拆成左段、新片段和继续旧值的右段,每段成为新事件,原记录仍保留。
真正棘手的是处理规则。Codex 审查发现了 8 个真实缺陷,作者重点披露了三个:
- 获知时间错用了 relay 时间,导致写入到转发之间的一段时间看起来「尚不知道」。修复方式是保留源写入时间。
- relay 遇到失败仍继续处理,使同字段后续变化建立在错误基础上。修复后遇到首个失败就停止,下次从该处恢复。
- 撤回只按事实 ID 查找,但状态转换后旧值可能位于下一事实的关闭事件中。修复后按值和起始日期查找当前有效区间。
作者称,每项修复都有回归测试,并故意破坏副本验证测试能否失败。这提供了实现证据,但原文没有给出吞吐量、查询耗时或规模测试。
我的判断
我赞同先明确查询需求,再决定记忆结构。如果没人会问「截至日期 K,我们知道什么」,单时间轴加编辑日志已经够用。双时态增加的不只是字段,还有区间拆分、撤回、重放和失败恢复的复杂度。
区块链部分应限定在「区块链本身不会自动提供这些语义」。应用仍可在链上编码有效期与撤回信息,但需要自己实现;采用区块链也不能省掉这层建模。对作者的单写入者场景,引入共识缺少明确收益。
Zep 与 Mem0 的比较也应视为作者阅读相关实现后的判断。原文没有交代具体版本,不能据此概括所有版本和部署方式。
另一条经验很实用:历史应记录对象事实。作者第一晚记录了某字段的 556 次变化,后来发现它是每晚重算的排序公式,一次真实的客户状态变化被噪声淹没。不过,哪些派生结果需要审计,仍取决于业务;不能把这条经验直接变成一律禁止。
能拿来做什么
- 给 CRM 或 Agent 记忆设计查询接口时,先区分「日期 D 的事实」和「截至日期 K 的认知」。确实需要两种答案,再引入双时态。
- 在 Node 写入层统一处理
ADD、UPDATE、DELETE、NOOP,另外明确撤回语义,并保留消息或文档来源,让历史回答有据可查。 - 使用异步任务归档变更时,沿用源写入时间,并测试失败后同字段的后续事件如何恢复,避免错误持续传播。
- 接入历史采集前,先排除无意义的公式重算;对夜间会删除并重建的表采用快照和差异比较,因为触发器会随原表一起消失。