大多数 Agent 内存都是一个无差别的存储库。循环在每一步开始时从中读取以组装上下文,并在每次动作后向其写入 —— 新的观察结果、运行摘要、暂存器编辑。同样的存储,同样的访问路径,没有分离。它在演示中运行良好,但一旦 Agent 运行时间足够长、存储库变大,它就开始腐烂。
它腐烂的原因对于任何扩展过数据库的人来说都很熟悉。一个同时提供读写服务的单一存储库就像是一个没有副本的单主数据库,它继承了该拓扑结构在负载下的每一个问题:写入与读取竞争,更新中途读取到写了一半的记录,且易失性工作集与持久记录之间没有隔离。几十年前,我们就通过读写分离解决了数据库的这个问题。Agent 内存也理应得到同样的对待。
解决方法不是更大的向量索引或更智能的嵌入模型。这是一个架构上的解决方法 —— 认识到 “内存” 是冠以同一名称的两种不同工作负载,并为每种负载提供其真正需要的存储规约。
一个存储库,两种工作负载
拆解 Agent 实际对内存的操作,你会发现这两种操作几乎没有任何共同点。
写入路径是重追加且容忍延迟的。在每次工具调用后,Agent 会记录发生了什么:观察结果、压缩摘要、更新后的计划。这些写入非常频繁,大多是追加而非更新,而且 —— 关键点在于 —— 没有人会因为等待它们而被阻塞。如果摘要在轮次结束后 200 毫秒才进入持久存储库,用户是察觉不到的。
读取路径则恰恰相反。在每个推理步骤开始时,Agent 会查询内存以组装上下文:相关事实、之前的决策、当前的暂存器。此读取操作直接处于轮次的关键路径上。它需要速度快,需要在该轮次持续期间保持一致,而且它是用户唯一能真正感受到的内存操作。
这两种工作负载的需求各不相同。写入路径需要吞吐量和持久性,不在乎延迟。读取路径需要低延迟和稳定的视图,不在乎两秒钟前的写入是否尚未完全传播。强制它们通过同一个存储库意味着每一个设计决策都是一种折中,对两者都没有好处。你为了快速检索而优化索引,写入就会变慢;你为写入吞吐量进行优化,读取就会返回底层正在被修改的存储库的不一致切片。
正是这一认识推动了数据库走向读写分离:将写入路由到主库,将读取路由到副本,并让每一端都能独立进行优化。这种模式在应用架构中也有一个名称 —— 命令查询职责分离(CQRS)—— 它将改变状态的模型与读取状态的模型分离开来。Agent 内存已在悄然间成长为一个大到需要同样分离的系统,而大多数团队尚未察觉。
内存写一半的问题
未分离设计最明显的症状就是读取捕捉到了正在进行的写入。
考虑一个长期运行的 Agent,它在轮次之间运行合并流程:将最后十个观察结果总结为一条简练的笔记并剪除原始条目。这是一个多步变动 —— 先写入摘要,然后删除原始记录。如果下一轮的读取发生在这两个步骤之间的窗口期,Agent 就会同时看到新的摘要和它本应替换掉的过时原始记录。现在它拥有了重复且部分矛盾的上下文,而且它无法知晓这一点。检索到的片段看起来和其他任何检索到的片段别无二致。
这正是 Agent 内存失效变得真正危险的地方。当内存失效时,它很少抛出错误。它会产生一个充满自信的错误响应,将过时的上下文与当前信息混合在一起。Agent 检测不到检索到的事实已经过期 —— 它处理内存注入的方式与处理任何其他提示词文本的方式相同,因此只应用了一半的合并操作就直接成为了推理输入的一部分。更糟糕的是,这种失败是无声的:分页、淘汰和合并策略在不表现出错误的情况下运行失常,因此测试套件依然显示通过,而回答质量却在下降。
在多 Agent 系统中,这个窗口期会变得更宽,损坏情况也会变得更糟。当多个 Agent 并发读写一个共享内存存储库时,同步操作会产生甚至难以重现的内存冲突 —— 一个 Agent 正在写入中的合并操作,就是另一个 Agent 损坏的读取结果。共享存储库已经变成了一个并发 bug 的温床,而原本没有人设计它承载这种复杂性。
主库/副本分离从结构上关闭了这个窗口。写入 —— 包括多步合并 —— 落在写入端,并且只有在整个变动被提交并传播后才对读取者可见。读取端永远不会观察到中间状态,因为它读取的是一个稳定的快照,而不是正在变动中的实时存储库。
最终一致性没问题 —— 除非它出了问题
拆分的反对意见是显而易见的:从库落后于主库,因此读取者可能会错过最近的写入。这就是复制延迟,它是真实存在的。本能反应是因此而拒绝整个设计。
这种本能是错误的,要纠正它,需要精确地确定 哪些 记忆可以容忍延迟,而哪些不能。
长期记忆可以容忍最终一致性。 知识库 —— 积累的事实、学到的偏好、过去任务的片段日志 —— 不需要写入在落地后的同一毫秒内即可读。如果智能体在一小时前学到了用户偏好,那么在检索时晚了 300 毫秒可见是无关紧要的。这个存储库就像 Web 应用背后的只读副本:最终一致,为查询优化,且延迟对体验是不可见的。Box 的工程团队在将读取流量移至从库时正是做了这样的决定 —— 大多数读取根本不需要最新的数据,假装需要只会白白牺牲巨大的可扩展性,而没有任何好处。
工作草稿板(Working scratchpad)不能容忍最终一致性。 智能体在 这 一轮中写下的内容 —— 当前计划、刚刚计算出的中间结果、刚刚确定的子目标 —— 必须在下一步立即可读。这就是“写后读”(read-your-writes)保证,它是不可协商的。一个无法可靠地看到自己最近草稿编辑的智能体,将会陷入循环、自我矛盾,或者重复已经完成的工作。将草稿板路由到延迟的从库,正是导致智能体忘记一步之前决定了什么的原因。
因此,拆分并不是“让记忆变得最终一致”。而是:知识库采用从库语义,工作草稿板保持主库语义。 两个存储库,两种一致性契约,刻意选择。这模仿了标准的数据库逃生舱 —— 大多数读取去往从库,但写入后紧接着的对一致性要求极高的读取则直接路由到主库。智能体的草稿板就是这种对一致性要求极高的读取。其他一切都可以延迟。
在任何一个方向上搞错这种区别,你都会付出代价。把草稿板放在从库上,智能体会丢失自己的思路。把知识库放在主库上,你又回到了单一的竞争存储库,做了拆分的工作却没得到任何好处。
快照读:为每一轮提供稳定的视图
拆分还开启了一个更微妙的一致性属性,它比原始的新鲜度更重要:单一回合应该看到单一、稳定的记忆版本。
如果没有它,一个多次查询记忆的长推理步骤可能会在查询之间观察到不同的状态 —— 因为后台写入或另一个智能体在此期间更改了存储库。智能体的推理随后建立在一个在思考过程中发生变化的记忆之上。这种 Bug 几乎无法重现,因为它取决于后台写入和前台读取的确切交织方式。
数据库通过快照隔离(Snapshot Isolation)解决了这个问题:事务从它开始那一刻的一致数据版本中读取,并且该事务内的每一次读取都会看到那个相同的冻结版本,无论其他地方提交了什么。将这个想法应用于智能体记忆,规则就变成了:当一个回合开始时,它固定知识库的一个快照,该回合中的每一次检索都读取该快照。 写入在整个过程中继续落地到主库;它们只是对正在进行的回合不可见。下一个回合会固定一个新的快照并获取它们。
这为智能体在一步的持续时间内提供了一个连贯的世界。它不会检索到一个事实,据此行动,再次检索,然后发现该事实已更改或消失。快照还使系统具有可调试性:如果一个回合出了问题,你可以准确地重建它所看到的记忆版本,而不是猜测已经发生变化的存储库。
借鉴复制规范
退一步看,整个模式就是将数据库复制规范移植到智能体循环中。
读优化的检索视图就是你的从库:为快速相似性搜索建立了索引,为读路径实际发出的查询构建了结构,并且可以容忍延迟,因为它持有的知识不是回合关键的。写优化的日志就是你的主库:易于追加、持久,是权威记录。整合与总结变成了你的复制管道 —— 后台进程获取原始写入并将其投影为读取侧需要的形状,就像数据库从预写日志(Write-Ahead Log)物化副本一样。
收益是复制一直以来提供的。热路径停止为记忆增长付费。随着持久存储库在长时间会话中积累数千个条目,检索延迟由读取视图的索引决定,而不是由总写入量决定,因为写入不再与读取在同一结构上竞争。存储库可以增长,而智能体的每回合延迟不会随之增长。
成本是诚实的,值得一提。你现在运行两个存储库和它们之间的管道,并且你必须明确哪些记忆具有哪种一致性契约 —— 这在以前是偶然决定的。但这种明确性正是重点。“这次读取是否允许陈旧?”是每个严肃的数据系统都会刻意回答的问题。一个必须在漫长任务中记住事情的智能体是一个严肃的数据系统,它也值得得到一个刻意的回答。
如果你的智能体记忆仍然是一个处理所有事情的存储库,那么你还没有记忆架构。你只有一个单主库数据库,正等着在生产环境中发现,为什么行业的其他公司不再运行这种系统了。
会员专享
余下内容仅对会员开放。
会员可读完整内容 —— 每一个公开发布的观点背后,那些框架、决策与推理的全貌。
- —完整文章,包含未公开存档的部分
- —可落地的工作框架,附带权衡与决策依据
- —新文章抢先看,先于公开发布
随时取消 · 一次订阅,畅读全部