☰
生产级 Agent 的记忆该怎么设计:两类记忆与三种接法
2026/10/24 2:22:05 网站建设 项目流程

生产级 Agent 的记忆该怎么设计:两类记忆与三种接法

原文:MemCurrent: Designing Persistent Memory for Production Agents(https://www.databricks.com/blog/memcurrent-designing-persistent-memory-production-agents)

先说结论

记忆不是"把历史全塞进 prompt"。原文开头把矛盾说得很直白:开发者常被迫在质量和效率之间做取舍,而很多技术提升一个维度就要牺牲另一个。记忆是少数能同时改善两者的手段,但更多历史不等于更好的记忆——全量上下文既贵,又会带入过时、冲突、无关的内容。

真正的难点是三个问题:留下什么、怎么维护、怎么衡量它有用。这篇文章讲的 MemCurrent 就是围绕这三问设计的。

两类记忆:原始轨迹与提炼结果

episodic memory存原始会话轨迹,只追加不修改,由 plugin 在会话运行时逐步写入。

derived memory存从轨迹里提炼出的结论,以文件形式放在结构化知识库里,是一张模拟分层文件系统的可变表;顶层有一个自动生成的 memory.md 索引,说明目录结构和每个文件装了什么。

两者都放在能支撑多用户、低延迟的生产级数据库里。这个选择很实际:记忆要是存在对象存储里,每次读都多几百毫秒,agent 会倾向于不读。

为什么不能只要一种?原文的消融实验给了答案:只给原始轨迹,提升有限,因为 agent 不知道自己有什么、什么时候该去翻;只给提炼结果,会丢掉细节,而原始轨迹恰好能把这些细节捞回来。

三种接法:plugin、tool、pipeline

记忆作为触发方计算时机优点缺点
Plugin自动在线更新立即生效;行为可预测运行时成本和延迟高;不灵活(不需要记忆时也在烧 token)
ToolAgent在线更新立即生效;agent 有灵活性运行时成本和延迟高;质量受 agent 能力限制
Pipeline定时或事件触发离线运行时成本和延迟低(异步批量)更新有延迟

读的时候是分层的:会话开始时 plugin 注入索引,让 agent 先知道自己知道什么;会话过程中给它两个工具,search_memory返回查询的 top matches(可能是过去轨迹里的一步,也可能是某个记忆文件里的一行),fetch_memory按标识符(session ID 或文件路径)取回完整条目。

写的时候也分两路:显式请求走 agent 工具add_memory/update_memory/delete_memory,比如用户说"别在 /tmp 下写脚本";剩下的隐式经验由离线的 dreaming 管线在后台从新会话里蒸馏进相关文件,同时承担整体维护——拆分过大的文件、合并重复、解决冲突。

拆分、合并、解决冲突这三件事,才是记忆系统真正难的地方。写入容易,维护才贵。冲突不解决,记忆就会自相矛盾,agent 反而更容易答错。

数字怎么看

内部基准的构造值得单独说:从 12K+ 会话里挖出 3K 条含"值得记"信号的候选,配上后续问题和评分标准,用对抗式 judge 按 7 条标准筛,最后人工复核,留下 114 个测试用例、29 个用户、2233 条历史会话。

结果:相比无记忆,通过率从 22.3% 提到 68.4%(约三倍),快 1.35 倍,便宜 1.44 倍,token 用量 418K。作为上限的 gold injection 是 78.6%,也就是关掉了八成以上的差距。

开源基准(LoCoMo、LongMemEval、ConvoMem)上平均 88.1%,和全量上下文的 87.8% 打平,但每题输入 token 从 103K 降到 8.4K,约 12 倍差距。在历史最长的两个基准上甚至超过了全量上下文——把一切都塞进 prompt 反而会干扰 agent。原文还提到这类测试的延迟基本在 4–7 秒/题,因为多数问题一跳就能答完。

消融实验更能说明结构:

  • 只有 episodic:35.8%。原始历史有用,但 agent 只在大约三分之一的用例里去搜它;一旦搜了,通过率约 60%。
  • 只有 derived:66.1%。会话开始时注入索引,解决了"不知道自己有什么"这个前提问题。
  • 两者叠加:68.4%。增益不大,但它说明原始轨迹能捞回蒸馏丢掉的细节。
  • 只做离线 dreaming:63.5%,比只给在线工具(53.9%)高近 10 个点;两者结合是 66.1%。

两个真实例子的账

例一,记忆阻止了幻觉:用户要某产品近两个月的 top 20 用户。agent 写了正确的 SQL,但没去读落在文件里的执行结果,直接编了一张表。用户反馈后它把教训写进记忆;下次查询跑完,先读结果再答,答案对了。

例二,记忆省掉了重复劳动:用户问"top 5 SDP 客户"。第一次 agent 从零搞清 SDP 指什么、用哪张表、怎么 join 和过滤,花掉 11 次工具调用、8 个子 agent、将近 6 分钟,然后把步骤存成一个 workflow 文件。后来同类问题,2 次工具调用、1 个子 agent、41 秒。

这两个例子的价值在于:它们不是"记忆让模型更聪明",而是"记忆省掉了重复探索"。前者不可控,后者可以工程化。

自己搭一个的最小骨架

官方入口是 Agent Bricks Memory 和 Genie,下面是我按原文语义写的示意接口,不是官方 API:

fromdataclassesimportdataclass@dataclassclassEpisode:session_id:strstep:introle:strcontent:strclassMemory:def__init__(self,episodic_store,derived_store):self.episodic=episodic_store# 只追加,写进去就不再改self.derived=derived_store# 可变,按文件路径组织# 会话开始:把索引注入 system prompt,成本固定且可预估defindex(self)->str:returnself.derived.read("memory.md")# 会话中:先搜再取,两级读,避免一次吃满上下文defsearch(self,query:str,top_k:int=5):hits=self.episodic.search(query,top_k)\+self.derived.search(query,top_k)return[{"id":h.id,"snippet":h.snippet}forhinhits]deffetch(self,ref:str)->str:# 自拟约定:带斜杠的当文件路径,否则当会话 idreturnself.derived.read(ref)if"/"inrefelseself.episodic.get(ref)# 显式写入:用户或 agent 明确要求记住某事defwrite_file(self,path:str,text:str):self.derived.upsert(path,text)

index() 在会话开始时调一次,返回索引文本,塞进 system prompt。search() 接查询返回 top_k 条带 id 和摘要的命中,注意它只返回摘要不给全文,这是为了压住单次检索的 token 开销。fetch() 接 id 或路径返回完整条目,那个"带斜杠就是路径"的判断是我自己的约定,正式实现里应该用显式类型字段。write_file() 走 upsert,同一路径重复写就是更新,天然支持覆盖旧结论。

调用链是固定的:注入索引 → agent 决定搜不搜 → 搜到 id → 取全文 → 用完可能写回。中间任何一环省掉都会掉分,消融里那三个数字就是证据。

怎么验证它在你自己的场景里有用

别只看别人的百分比,原文的实验设计给了三条基线,直接照搬:

  1. 无记忆,作为下限。
  2. 全量上下文,作为上限;历史长到塞不进时改成 gold injection(只注入标准答案记忆)。
  3. 你自己的方案。

然后把 token 和延迟一起画上去,看是否落在成本-准确率的 Pareto 前沿。只看准确率,很容易做出一个又贵又慢、但数字好看的系统。

值得记住的取舍

三种接法的代价是明摆着的:plugin 和 tool 都在线计算,更新即时但贵且慢,plugin 还不灵活,不需要记忆的时候照样烧 token;tool 的质量则受 agent 能力限制。pipeline 便宜、异步、能批量,代价是更新有延迟。

我的判断是:会话内会变的东西走在线,跨会话沉淀的东西走离线。前者要即时生效(“别再做这件事”),后者可以等(“这类任务的标准流程”)。

原文最后还提了一个方向:记忆不该绑死在某个 agent 或某个模型上,它可以跟着用户迁到新 agent、新模型,甚至沉淀成组织级知识。这部分他们仍在探索,还没给出实现细节,以官方文档为准。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询