智能体记忆该拆成几层:笔记、记忆表示与注入上下文
原文:Anthropic Research - 《The components of agent memory》(https://www.anthropic.com/research/components-of-agent-memory)
做 Agent 做到第二个月,大概率会撞上同一个问题:上下文窗口塞不下,于是有人提议"给它加个记忆"。加完之后你会发现,真正难的不是存,而是存什么、什么时候写、写错了怎么办。
Anthropic 11 月 6 日那篇 engineering 文章把这几个问题拆得比较细。我按自己的理解重排一遍,重点放在能照着做的部分。
一、先把边界划清楚
原文把 agent memory 定义成"Claude 从经验里获得、并在会话之间保留的知识,以它能读到的上下文形式保存",然后明确排除三类东西:
| 被排除的 | 为什么不算 |
|---|---|
| 权重变化 | 训练时写进参数,不属于运行期经验 |
| in-context learning | 只在单次对话内有效,换会话就没了 |
| compaction | Claude 自己写的摘要,目的是腾窗口继续干,不是跨会话知识 |
这个划分有用。很多人说"记忆"时把 compaction 摘要也算了进去,那是同一会话里的工作记忆,跟跨会话知识库是两码事。混在一起,后面所有设计都会糊。
二、四层结构
原文给的是自下而上的四层:
- transcript:可搜索的事件日志,最底层,原始素材
- memory notes:Claude 自己写下的显著性观察
- memory representations:比如 wiki,把笔记组织成可导航的结构
- injected context:每次会话预加载的内容,最顶层
dreaming 在图里的位置是关键:箭头从 transcript 指向上面每一层,相邻层之间还有双向流动。也就是说它不是"再写一遍笔记",而是拿原始日志去校正、重构上面几层。
这个词借的是睡眠记忆巩固的类比:海马体快速记录当下经验,只有一部分在离线时被重放、蒸馏、整合进新皮层。对应到 Agent 上,Letta 和 UC Berkeley 那篇 sleep-time compute 讲的是同一件事------用交互之间的空闲时间重组上下文。
三、有记忆和没记忆,差多少
CL-Bench 1.0 的数据库任务:每个问题都开新会话,唯一能带过去的是持久化的 memory 目录。答完会告诉它正确答案,它可以写记忆帮助后面几题。
| 模型 | 无记忆 | 有记忆 |
|---|---|---|
| Opus 5.5 | 0.655 | 0.838 |
| Sonnet 5.5 | 0.644 | 0.833 |
| Opus 5 | 0.564 | 0.828 |
| Opus 4.7 | 0.383 | 0.700 |
| Sonnet 5 | 0.306 | 0.610 |
| Sonnet 4.6 | 0.330 | 0.364 |
表格最后一行值得多看两眼。Sonnet 4.6 拿到记忆几乎没涨,说明有记忆不等于会用记忆,这一层能力是模型自己长出来的,不是 harness 给得起的。
比分数更有意思的是行为演进,四代模型留下四个阶段:
- Sonnet 4.6:停在"提出猜测",目录里散着几个文件,每个列几条没验证过的假设
- Opus 4.7:开始把笔记合并成单一参考文档,并标注哪些已验证、哪些没确认
- Sonnet 5:被评分器检查过之后才把答案标成 confirmed,答错时写更正
- Opus 5:第一个形成闭环的,条目里带规则、带"我怎么知道这条规则成立"、带对踩过的坑的明确警告
这个演进说明一件事:记忆的价值不在条数,在有没有"验证"这一步。没有验证的记忆只是把猜测固化了。
四、什么该写,什么不该写
Pokémon Red 那组对比最能说明显著性问题。同样是约一万步:
| Sonnet 3.7 | Fable 5 | |
|---|---|---|
| 规模 | 413 文件 / 225 目录 / 977,930 字符 | 10 文件 / 2 目录 + global.md / 43,406 字符 |
| 徽章 | 1 | 4 |
Sonnet 3.7 在约一万步时开了第四个 master 记忆目录,接下来 17 步里创建 6 个文件、删除 9 个文件,游戏里一步没动。
Fable 5 的原则只有一句:只记屏幕上读不到的东西。金钱数量、当前位置每回合都能读到,存下来就是冗余;策略、计划、验证过的规则不写就消失。
它的目录结构可以抄:global.md 当仪表盘(当前目标、预先写好的道馆攻略、自己的记忆使用规则),locations 放路线导航计划,gameplay 放对机制的理解。一条真实笔记长这样:
If 3+ balls fail in a row, VARY the timing (insert extra 'wait' presses between menu steps) - the catch landed right after doing this.
这是连续七个球没抓到 Magikarp 之后写的。注意它记的不是"我失败了七次"(那是日志),而是"失败七次之后改了什么、结果如何"(那是规则)。
五、引导的尺度:说目的,不说方法
原文有一句我觉得是全篇最实用的:告诉 Claude 记忆是干什么的,但不要告诉它怎么用。
证据是反过来的,固定规则会掉分。设一条"只记录失败",记忆增益变成 Opus 5 是 −0.234、Opus 4.7 是 −0.150、Sonnet 4.6 是 −0.020。
而引导对老模型帮助最大。Sonnet 4.6 经常压根不读记忆、也不测试猜测,提示它"回答前先读记忆文件,然后显式测试你的猜测",分数从 0.330 到 0.641,接近翻倍。作为对比,Sonnet 5 不提示也有九成概率先读记忆,Sonnet 4.6 不到一半。
所以实践上的分界线是:harness 负责提供能力和工具,判断交给模型。你要做的是把 memory 目录挂上去、告诉它有这个东西、在它明显不用时加一句流程提示,而不是写死"每次先读 errors.md"。
六、dreaming 干的四件事
1. 纠错:Mt Moon
这个案例很典型。Mt Moon 洞穴入口在西、出口在东,入口附近有个单向陡坎会把人摔回原路。Claude 拿入口的 PokéCenter 当地标,判断要不要下坎取决于它认为 PokéCenter 在山哪一侧。
它错记成东侧,出洞后宣布 Mt Moon is behind me,把笔记更新成 DONE,然后走下坎被摔回原路。又因为导航工具不显示落差,它误判成"I overshot far west",一路退回 Pewter City,最后没到 Cerulean。
dreaming 的修法是:把含错误地标的那个目录拿去,让它对着 session transcript 逐条核对笔记、发现错误、重写。transcript 在这里是 ground truth。
修完之后它说"Now I need to find the Mt Moon cave entrance to re-enter",直接重新进洞,第 1,935 个动作到达 Cerulean。
这里有个隐含前提值得单独拎出来:能做纠错的前提是原始日志还在。如果你的 harness 只留摘要、不留 transcript,dreaming 就失去了对照物,只能让模型自己回忆,那等于让它再猜一次。
2. 建 wiki:从笔记到可导航
wiki 的定义是"由 Claude 自己写和维护的页面集合,把一个主题下它知道的东西聚起来,并链接到其他笔记"。链接的作用是给模型一张定位地图。
Stainless 语料那组实验:35 万文件、约 4.2 亿 token,25 个问题,给搜索工具。
| 配置 | 25 题得分 |
|---|---|
| dreaming 从 2 会话扩到 128 会话 | 13% → 48% |
| wiki + 搜索工具 | 54% → 61% |
| 只有搜索工具 | 持平 53% |
| 只用 wiki 答题 | 最高 48% |
规模变化:2 会话后 103 页 / 766 链接,128 会话后 222 页 / 12,893 链接。wiki 加搜索同时用,每题省约 2 万 token。
61% 和 53% 的差距不算大,但省下的 token 是实打实的。这也符合直觉:wiki 帮你少翻文件,最终答案还得回原文核。wiki 不是语料替代品,它只是索引加导航。
原文还举了个反面。某任务里 wiki 比原始语料还大:10 个文档 115 KB,产出 68 页、255 链接、203 KB 的 wiki,1.77 倍于语料,有些页面彼此没链接。作者的原话是 effectively abstracted nothing,等于做了个更贵的副本。
Harvey LAB 那组能看出适用边界:24 个并购问题里多数要跨多文档找信息、还要判断和计算,用 wiki 87%、只用搜索 73%;而多数标准能从单个 deal overview memo 里搜到的任务,wiki 没有明显增益。
结论很清楚:跨文档、需要综合的任务,wiki 值;单文档定位类任务,搜索就够,建 wiki 是纯开销。
3. 注入:顶层要小
dreaming 可以把笔记"浮"到顶层,让 harness 每次会话开始就预加载。好处是模型不用搜,甚至不用知道有东西可搜。代价是两条:
- 占 token,而且是每个会话都占
- 一条错的笔记会被带进之后的每一个会话
原文还引了 Claude Code 文档里的观察:指令文件越长,遵循度越低。所以顶层必须保持小。Claude Code 的 auto memory 做法是只加载 MEMORY.md 的前 200 行或 25 KB,把这段头部当索引------告诉模型"你知道什么、去哪找",而不是把内容全塞进去。
4. 修结构:从 72% 到 100%
LAB 收购合同任务,多个 agent 同时往共享的条款表里写,互相覆盖,最后 brief 只包含 57 个事实里的 41 个,也就是 72%。
dreaming 的修法不是加更多 agent,而是改结构:每份合同单独一页、由单个 agent 写、逐字引用条款而不是汇总成共享表。改完 57/57。
同一批实验里还有个细节:建 wiki 时 Claude 会链到还没创建的页面。第二轮 dreaming 在某个任务里找到 9 条指向缺失页面 escrow-agreement 的链接(来自 6 个页面),全部重指向已存在的 buyer-form-escrow-agreement。
启示是:多 agent 共享写同一份结构,冲突率极高;分片所有权比并发更重要。
七、写一个最小 dreaming 循环
下面这段是自拟的示意实现,不是官方代码,只保留原文讲清楚的三个动作:对着 transcript 校错、按预算裁剪注入层、把已验证的规则排在前面。
python
# dream_cycle.py ------ 会话结束后跑一轮记忆维护
import re
import pathlib
MEM_DIR = pathlib.Path("memory")
INDEX = MEM_DIR / "MEMORY.md"
INJECT_BUDGET = 25 * 1024 # 对应 Claude Code 的 25KB 上限
MAX_LINES = 200 # 行数上限,两个条件谁先到算谁
def load_notes():
"""读出所有笔记,跳过索引本身,返回 [(path, text)]"""
return [(p, p.read_text(encoding="utf-8"))
for p in MEM_DIR.rglob("*.md") if p != INDEX]
def verify(note_text, transcript):
"""拿 transcript 当 ground truth:笔记里每条断言都要能在
日志里找到落点,否则标成 unverified。这里是关键字匹配的
粗糙版本,真实实现应当交给模型逐条判断。"""
out = []
for claim in re.findall(r"^[-*]\s*(.+)$", note_text, re.M):
key = re.sub(r"\W+", " ", claim)[:40].strip().lower()
state = "verified" if key and key in transcript.lower() else "unverified"
out.append((state, claim))
return out
def rebuild_index(notes):
"""只写链接和状态,不搬正文;到预算就停,剩下的靠点进去看"""
lines, size = ["# MEMORY", ""], 0
for path, _ in notes:
entry = "- [{}]({})".format(path.stem, path.as_posix())
size += len(entry.encode("utf-8"))
if size > INJECT_BUDGET or len(lines) > MAX_LINES:
break
lines.append(entry)
INDEX.write_text("\n".join(lines), encoding="utf-8")
if __name__ == "__main__":
transcript = pathlib.Path("session.log").read_text(encoding="utf-8")
notes = load_notes()
for p, text in notes:
facts = verify(text, transcript)
# 关键一步:把验证结论写回笔记,让下个会话知道自己信的是什么
p.write_text("\n".join("- {}: {}".format(s, c) for s, c in facts),
encoding="utf-8")
rebuild_index(notes)
参数和调用链:
- verify 的 transcript 是唯一可信源。缺了它,这个函数就退化成模型自评,正好踩上原文说的"错误的笔记会被信任"。
- INJECT_BUDGET 和 MAX_LINES 是两个并列上限,对应"顶层必须小"。超出部分只留链接,不搬内容。
- rebuild_index 只产出索引,不做摘要,避免每轮 dreaming 都把正文复制一遍,越长越臃肿。
- 调用顺序是:会话结束 → verify → rebuild_index → 下个会话读 INDEX。注意 verify 会原地重写笔记,正式环境得留版本或备份,否则一次错误匹配就把笔记洗掉了。
八、四个还没答案的问题
原文最后自己列了局限,对做产品的人比结论更有用:
- 什么时候写。会话内写能抓住最鲜活的细节,但会分散主任务注意力;推到后台,输出就不能立刻用。
- 怎么保持更新。环境和需求都会变,旧记忆会漂移;全量持续校验的算力不现实,只能聚焦"可能会变的信息",或按访问频率调 dreaming 的节奏。
- transcript 搜索能不能当记忆底座。表示加搜索有时有效,但要花算力维护,防止幻觉累积。
- 显著性判断的根本局限。没有模型能真正知道未来会发生什么。所以 dreaming 的价值不是预测未来,是回头看------用算力把过去整理好,让未来更好取。
九、可以照着做的三件事
如果你的 Agent 现在只有会话历史、没有记忆层,我建议按这个顺序加:
第一,先把 transcript 完整留下来。它是后面所有纠错的地基,成本最低、收益最确定。
第二,加一层 note,但只记"屏幕上读不到的东西"。判断标准可以直接用 Fable 5 那句:这条信息下一轮能不能自己观察到?能就不写。
第三,顶层索引卡死预算。200 行、25 KB 这种数字不是为了好看,是防止注入层慢慢长成第二个上下文窗口。
最后提醒一句:上面的分数都来自特定模型版本和 Anthropic 自建基准,换模型、换任务不一定复现。记忆的收益高度依赖任务是否存在"可积累的经验",纯一次性任务加记忆只会增加干扰。具体接口和参数以官方文档为准。