记忆系统 v2:解决上下文膨胀问题
我们的 AI 智能体面临两个问题:上下文窗口因工具输出而爆炸(上下文的 82.5% 是工具结果),以及 /new 命令会清除所有工作状态而没有短期记忆交接。我们构建了积极的上下文修剪、将 MEMORY.md 压缩了 97%,并编写了一个会话交接钩子,在每次会话重置前自动更新记忆文件。
在我们之前关于构建持久记忆系统的文章中,我们描述了 MEMORY.md 膨胀问题:经过六周后,该文件增长到了 700 多行,我们通过从内联内容切换到基于指针的条目来修复它。这个修复是有效的。MEMORY.md 变得紧凑,会话启动改善,一切正常。
然后它再次膨胀了。
四周后,MEMORY.md 又回到了 92,000 字符和 790 行。组织者管道继续内联写入新的事实,而不是推迟到按主题的文件。我们的字节大小限制没有得到一致的执行。原来的修复只是缓解了症状,而不是根本原因。
更令人担忧的是,我们开始注意到会话在 MEMORY.md 受控的情况下仍然在任务中途达到上下文限制。智能体会读取几个文件,运行搜索,然后停顿——不是因为它的记忆用完了,而是因为它的上下文窗口被同一会话早期的工具输出填满了。
还有第三个问题我们一直在容忍:每当我们运行 /new 来启动新的会话时,智能体就会失去对它刚才在做什么的所有认识。我们的长期记忆系统(v1)很好地处理了事实、偏好和项目知识。但是短期工作状态——正在进行的任务是什么、刚刚做出了什么决定、下一步是什么——完全消失了。用户必须手动提醒智能体在重置前更新其记忆文件,否则就会失去上下文。
三个问题,一个主题:在任何时间尺度上都没有系统的上下文生命周期。
修复前的测量
在更改任何内容之前,我们编写了 session-stats.py 来分析过去 15 个会话,并理解上下文实际上流向何处。输出很有启发性。
Session context breakdown (15 sessions, chars):
┌──────────────────┬───────────┬───────────┬────────────┐
│ Category │ Total │ % of ctx │ Avg/session│
├──────────────────┼───────────┼───────────┼────────────┤
│ Tool results │ 1,842,300 │ 82.5% │ 122,820 │
│ System prompt │ 268,100 │ 12.0% │ 17,873 │
│ Assistant text │ 64,700 │ 2.9% │ 4,313 │
│ User input │ 55,900 │ 2.5% │ 3,727 │
└──────────────────┴───────────┴───────────┴────────────┘
最极端的会话:159,000 字符的工具结果,1,500 字符的用户输入和助手文本合计。实际对话在自己的上下文窗口中几乎是看不见的。
系统提示在每个会话平均为 17K 字符。我们知道 MEMORY.md 是在启动时加载的,但看到它在所有会话中占总上下文的 12%,包括与记忆无关的会话,使得这个数字变成了具体的。智能体在每个会话上都要支付 17K 字符的上下文税,不管它在做什么。
这两个问题现在是可测量的:工具结果在会话内膨胀,MEMORY.md 在会话间膨胀。两者都是可解决的,而且我们有数字来评估解决方案。
解决方案 1:上下文修剪
会话内的问题是工具输出积累。智能体读取一个文件——那是 8K 字符的上下文。运行搜索——另外 4K。编辑文件,看到差异——2K。读取测试输出——6K。在适度复杂的任务之后,上下文主要是来自智能体不再需要引用的早期步骤的工具输出。
OpenClaw 的 contextPruning 功能通过基于 TTL 的方法处理这个问题:在可配置的时间窗口之后,最近转向之外的工具输出被替换为占位符。内容从活跃上下文中消失,但智能体可以看到发生了什么。
我们的配置:
contextPruning:
mode: cache-ttl
ttl: 30
minPrunableToolChars: 100
hardClearRatio: 0
使用 ttl: 30,任何超过 30 秒的工具结果在下一转都符合修剪条件。minPrunableToolChars: 100 防止替换成本几乎为零的微小工具输出。hardClearRatio: 0 意味着我们永远不做完整擦除——我们保持最近转的完整。
这样的效果是智能体使用最近工具上下文的滑动窗口而不是完整的累积历史进行操作。对于涉及重复文件读取或搜索迭代循环的任务,这是在第 8 步达到上下文限制和完成任务之间的区别。
我们有一个顾虑:修剪会破坏智能体参考早期工作的能力吗?实际上,不会。对于大多数任务,智能体要么需要最近工具调用的输出,要么需要应该在记忆中而不是在临时工具结果中的一般事实。如果智能体需要重新读取它已经处理过的文件,这通常是事实应该被写入记忆而不是缓存在上下文中的标志。
解决方案 2:MEMORY.md 结构压缩
92K → 紧凑迁移需要面对我们第一次避免过的一个设计问题:MEMORY.md 究竟应该包含什么?
我们 v1 的答案是”最近活动、活跃项目、关键联系和基础设施注释”,带有字节大小上限以保持其可管理性。这是错误的。字节大小上限是压缩内容的激励,但它不能防止积累——它只是使每个条目在你用完空间并开始弯曲规则之前更短。
正确的答案是 MEMORY.md 应该包含指针,而不是内容。如果你能用”它包含 X”来回答问题”这个文件的目的是什么?“,那么 MEMORY.md 不应该包含 X——它应该包含”查看 memory/X.md 了解 X”。MEMORY.md 是一个告诉智能体去哪里看的索引,而不是包含智能体知道什么的文档。
根据这个定义,目标结构变得显而易见:
## Users
| handle | role | notes |
| --- | --- | --- |
| @orange | owner | ... |
## Projects
| name | status | detail file |
| --- | --- | --- |
| claw-stack | active | memory/entities/project-claw-stack.md |
| info-pipeline | active | memory/entities/project-info-pipeline.md |
## Infrastructure
| service | notes | detail file |
| --- | --- | --- |
| CF Workers | edge compute | memory/infra/cloudflare.md |
## Behavior rules
See AGENTS.md for current rules.
## Recent (last 5)
- 2026-03-09: ...
用于结构化事实的表格(用户、项目、基础设施)。其他一切的指针。最近活动限制在五个条目,滚动。总目标:5,000 字符以下。
迁移后,MEMORY.md 从 92,000 字符降至 2,900 字符——减少了 97%。会话启动从 ~23K tokens 的 MEMORY.md 上下文变为 ~700 tokens。之前在 MEMORY.md 中的所有内容仍然可以通过 QMD 向量搜索查找;它只是现在在按主题的文件中而不是内联的。
迁移脚本本身大约 150 行 Python:读取当前的 MEMORY.md,使用 Claude Haiku 按类别提取事实,将事实写入适当的按主题的文件,生成新的基于指针的 MEMORY.md。运行它花了 20 秒。
解决方案 3:会话交接钩子
上下文修剪和 MEMORY.md 压缩解决了技术膨胀问题。还有第三个问题我们一直在容忍:当你运行 /new 来启动新的会话时,你会失去当前会话的所有工作上下文。你在编辑什么文件?下一步是什么?你刚刚发现了你在调试的错误的什么信息?
传统的回应是”写更好的笔记”。我们想自动化它。
OpenClaw 支持在特定命令上触发的钩子。我们编写了一个 command:new 钩子,在新会话开始前运行会话总结管道:
# Triggered on /new
def session_handoff(transcript):
summary = claude_haiku(
system=open("MANIFEST.md").read(), # file map for the memory system
prompt=f"Summarize this session. Extract: current work state, "
f"decisions made, lessons learned, entities updated. "
f"Format as structured updates for memory files.\n\n{transcript}"
)
apply_memory_updates(summary) # updates MEMORY.md, TODO.md, entities, etc.
钩子以 20 秒超时同步运行,然后如果抄本太长则回退到异步。实际上,大多数会话在 8-12 秒内处理。
关键部分是 MANIFEST.md,一个描述记忆系统结构的文件:哪些文件存在、每个文件包含什么,以及什么样的更新去哪里。没有它,Haiku 不知道项目更新应该进入 memory/entities/project-X.md 而不是直接进入 MEMORY.md。MANIFEST 是维护记忆的智能体的模式文档。
在交接钩子之后,/new 仍然启动新的上下文,但 MEMORY.md 现在反映当前会话的结果。下一个会话启动时知道你在哪里停下来。
衰减防止规则
在重建系统两次后,我们在 AGENTS.md 中写入了明确的规则,以防止相同的问题再次发生:
硬限制:
- MEMORY.md 必须保持在 5,000 字符以下。如果更新会使其超过限制,请写入按主题的文件并添加指针。
- 永远不要将提交哈希、代码片段或原始错误消息写入 MEMORY.md。这些要么是临时的(提交哈希、错误),要么属于按主题的文件(代码)。
禁止的内容:
- 超过 5 个项目的列表(使用按主题的文件)
- 已经存在于另一个记忆文件中的事实(无重复)
- “临时”笔记(写入 TODO 文件,而不是 MEMORY.md)
定期维护:
- 在任何涉及超过 3 个文件的会话之后,检查按主题的文件是否需要更新
- 当项目状态改变时,更新实体文件,而不是 MEMORY.md 表
写入 AGENTS.md 的规则成为系统提示的一部分,这意味着组织者管道和交接钩子都能看到它们。它们不是由代码强制执行的,但上下文中明确的规则比非正式的约定要好得多。
测量结果
部署 v2 更改后的即时结果:
| 指标 | 之前 | 之后 |
|---|---|---|
| MEMORY.md 大小 | ~92K 字符 (~23K tokens) | ~2.9K 字符 (~700 tokens) |
| 会话启动上下文税 | ~23K tokens | ~700 tokens |
| 工具结果在上下文中的比例 | 82.5% | 30 秒后修剪 |
| 跨 /new 保留的工作状态 | 否 | 是(自动化) |
MEMORY.md 的减少是 97% 的削减。每个新会话现在启动时开销减少了 22K tokens,这意味着实际任务有更多空间。上下文修剪配置意味着超过 30 秒的工具结果被替换为占位符,防止在多步骤任务中导致停顿的会话内累积。
交接钩子是否一致地产生正确的记忆更新是我们在几周的使用后才会知道的事情。架构是正确的——问题是 Haiku 关于要更新什么的判断在规模上是否成立。我们会回报。
我们对记忆的了解
v1 博客文章将膨胀问题框架为一个技术问题,具有技术修复:执行字节大小限制,使用指针而不是内联内容。这个框架是正确的但不完整。
真正的问题是记忆管理是一个信息架构问题,而不是存储问题。每次我们说”这个事实可能稍后相关,所以把它放在 MEMORY.md 中”时,我们都在做出一个坏的索引决策。MEMORY.md 被用作一个通用工具而不是架构中的特定层。
v2 系统之所以有效,不是因为我们有更好的执行机制(虽然 TTL 修剪和大小限制有帮助),而是因为我们更清楚每一层是什么:
- 活跃上下文:当前会话的工作状态。临时的。积极修剪。
- MEMORY.md:会话方向。启动会话所需的最小上下文。仅指针。
- 按主题的文件:特定主题的深度。按需加载。内容所在的地方。
- 向量搜索:跨所有记忆的回退检索。用于不知道去哪里看的查询。
当一个新事实到达时,问题不是”我应该记住这个吗?“而是”这个属于哪一层?“大多数事实不属于 MEMORY.md。正确地解决这个架构问题是防止膨胀的原因。
智能体开发者的实际建议
如果你正在构建类似的东西,我们两次犯过的错误是值得知道的:
在写入时执行索引/内容分离,而不是事后执行。 MEMORY.md 上的字节大小限制不能防止膨胀——它只是使膨胀在你超过它之前更小。真正的限制是:索引中没有内容,只有指针。检查每一次写入。
在优化前测量上下文分布。 我们假设 MEMORY.md 是主要问题。它是一个问题。工具结果是更大的问题。运行 session-stats 花了一天来编写,并立即浮出了更大的问题。先测量。
基于 TTL 的上下文修剪风险低、回报高。 我们担心它会破坏智能体行为。它没有。对于大多数任务,旧工具结果是噪音,而不是信号。修剪它们。
交接钩子比完美的笔记更值得。 要求人类(或智能体)可靠地写出会话结束笔记是一个失败的策略。自动化它。即使是花 10 秒的粗糙提取也比没有写的手动笔记更好。
为使用它的智能体记录记忆系统的模式。 MANIFEST.md 模式——一个解释事物去处的文件——是使自动记忆更新实际上把事物放在正确位置的原因。没有它,每次更新都会变成关于文件放置的临时决定。
AI 智能体的记忆系统仍然年轻到没有既定的实践。这些是对我们规模有效的模式。你的规模、访问模式和智能体的任务分布会产生不同的限制。但底层原则是成立的:智能体记忆是信息架构。在构建基础设施之前先把架构搞对。