第24章 Agent 应该忘记什么
2026.07.29上下文窗口是有限的,忘记是不可避免的。关键不是"如何记住更多",而是"什么值得保留"。
引子
一个 session 跑了大半天。上下文里塞满了几十次编译报错、五轮调试尝试、三个被否的方案。Agent 开始忽略自己几分钟前做的决定——你明明说了"用方案 B",它又回到了方案 A。
这不是 agent 变笨了。这是上下文窗口满了。
当窗口接近上限时,早期消息在模型的有效利用中趋于不可见,而最新的消息占据注意力。agent 看起来"记不住"了——不是因为它没有上下文,而是因为有用的上下文被无用的信息淹没了。这一现象与公开研究(如 Lost in the Middle: How Language Models Use Long Contexts,Liu 等,TACL 2023)所刻画的位置偏置方向一致:模型对上下文中部信息的利用显著弱于首尾。
问题不是"能不能设计一个无限的上下文窗口"(技术上可以,RAG 和 1M token 窗口已经在做了),问题是:你希望 agent 在有限容量里记住什么? 主动选择遗忘的内容,和让窗口满了随机丢消息,是完全不同的两种情况。
上下文窗口有边界——这是物理约束
1 万个 token 的上下文、10 万个 token 的上下文、甚至 100 万个 token 的上下文——不管多大,它都有边界。一个持续运行数小时甚至数天的 agent session 可以把任何上下文窗口塞满。100 万 token 听上去很多,但如果 agent 每 10 分钟做一次工具调用(每条几百 token),一天就会产生数万 token 的"操作日志",加上每次调用的返回值(读文件可能几千 token),一周就到百万了。
所以这不是一个"够不够大"的问题,这是一个"在有限的容量里放什么"的问题。
更核心的是:把上下文当作"无限"来设计,比把它当作"有限"更危险。当系统假设上下文永远装得下时,它不会设计遗忘策略。而遗忘策略缺席的后果不是"永远不遗忘"——而是被动遗忘:最早的消息被新消息推出窗口,没有任何优先级判断,没有任何保留依据。最古老的但最重要的信息(用户的第一条需求约束)和最古老的最不重要的信息(第一次查目录返回的 200 条文件列表)被同等对待——一起被推出去。
主动遗忘 vs. 被动覆盖——这是选择放弃一些东西,和让东西自己消失,之间的区别。后者不是工程,是听天由命。
遗忘优先级——什么该留、什么该扔
既然遗忘不可避免,那就设计它。
CodeCoder 的压缩策略把上下文内容分为三个优先级:
最高价值——必须保留:
- 当前正在执行的目标和约束
- 未完成的决策和待解决的依赖
- 用户明确表达的偏好("不要用 unwrap"、"优先用 builder 模式")
- 最近的工具调用流(最近 N 轮对话,保持 agent 动作连贯)
这些在任何压缩场景下都不能丢弃。它们是 agent 在当前时刻"正在做什么"和"不能做什么"的核心上下文。
中等价值——可以压缩但不可删除:
- 关键工具调用结果("我读了这个文件,它的结构是……")
- 代理的推理链要点(不保留每个推理步骤,保留推理结论)
- 已修改或已读取的文件路径追踪
这些内容有价值,但不需要保留原始全文。CodeCoder 的 tier-1 压缩对这类内容做占位化处理——把 ToolResult 的冗长正文替换为摘要 + 文件路径,保留"做了什么"但丢弃"返回了什么细节"。
最低价值——可以直接丢弃:
- LLM 生成的 Reasoning token(思考过程的逐字记录,最长、最重复的部分)
- 失败的尝试路径和讨论("试了方案 X,失败了"保留一句话足够了,不需要保留当时的错误输出全文)
- 已经被后续操作覆盖的信息(比如先读了一个目录结构,然后又读了一次——第一次的结果不再需要)
CodeCoder 的 tier-1 压缩首先丢弃所有 Reasoning token。这不是随意的选择——Reasoning 通常占据上下文的相当比例(CodeCoder 项目内的观测,量级在数成上下),而且它的信息密度最低。如果 Agent 需要在之后复现自己的推理过程,它应该在 Memory 中显式保存推理结论,而不是依赖 Reasoning 原文。
tier-2 压缩在 tier-1 之后仍超阈值时触发:对最早的对话段落做结构化摘要,摘要是迭代式合并的——每次只摘要增量部分,并累积文件追踪信息附在摘要末尾。
什么不该遗忘——持久化知识的边界
有些东西不该靠上下文窗口来保留。如果 AGENTS.md 和 CONTEXT.md 的内容需要通过上下文来保持"记忆",那么每次 session 结束它们就丢失了。
但 CodeCoder 的做法是:这些文件在每次 session 开始时重新读入 system prompt。 它们根本不依赖上下文窗口。这就是"文件系统即自我"原则的另一个体现——核心知识不塞进上下文,而是放在磁盘上。上下文窗口里的东西可以丢,磁盘上的文件不会丢。
Memory 工具就是为此设计的。它不是压缩时顺便保存的副产品——是 agent 主动决定"这个信息值得跨 session 记住"时写入的。agent 调用 memory write key=value,系统持久化到 memory/ 目录。
对比三种机制的遗忘特性:
| 存储位置 | 遗忘机制 | 恢复方式 |
|---|---|---|
| 上下文窗口 | 压缩/丢弃 | 不可恢复 |
| Memory 文件 | 永不自动删除 | agent 主动调用 memory read |
| 配置文件(AGENTS.md 等) | 永不自动删除 | session 启动时重读 |
上下文窗口里的东西是最容易丢的——所以只放"当前"需要的东西。Memory 是持久的——放"以后"也需要的东西。配置文件是最稳固的——放"永远"需要的东西。这些配置文件(AGENTS.md、CONTEXT.md)构成 agent 的"自我基因组",见篇 1「文件系统即自我」。
代价与权衡
遗忘策略不是免费的——无论选择什么级别的压缩,都意味着信息丢失。
第一个代价是 tier-2 摘要的信息损失。结构摘要把最早的对话段落压缩为"目标 / 约束 / 进展 / 关键决策 / 下一步"五个字段——但这五个字段不可能完美还原原始对话中所有的细微决策。如果某个关键决策的前提被压缩掉了,agent 在新一轮对话中可能做出与原始意图矛盾的判断。CodeCoder 的迭代式合并策略试图通过累积文件跟踪(在摘要末尾附上 read/modified 文件路径)来缓解这个问题,但路径列表不能替代完整的上下文。
第二个代价是 压缩触发时的性能开销。tier-2 压缩需要调用一次 LLM 来生成结构摘要——这是有成本和延迟的。在交互式 session 中,用户可能正在输入下一句话,而 agent 正在后台压缩上下文,导致响应卡顿。CodeCoder 将 compaction 放在独立线程中运行,但受限于 JSON 文件在压缩期间的一致性问题(agent 可能在压缩的同时写新的消息),实现中使用了写时复制和锁机制——额外的复杂度。
第三个代价是 压缩使得事后审计变得更困难。压缩前的原始上下文在压缩后被替换为摘要,原始细节丢失。如果需要审计员在数周后复现"当时 agent 为什么做了这个决定",他看到的只有 Tier-2 摘要,而不是原始推理过程。CodeCoder 的 Session 文件是追加写入不删除的(压缩用新文件而非原地覆盖),解决了这个问题——但磁盘占用因此翻倍。
第四,Memory 的写入依赖于 agent 的主动性。如果 agent 不知道某个信息应该存到 Memory,或者忘记调用 memory write,这个信息就丢失了。依赖 agent 自发地"决定记住什么"是一种被动记忆策略,与主动的上下文保留完全不同。理论上可以通过定期提示 agent"你有需要记住的东西吗"来缓解,但增加了 prompt 成本和行为的不确定性。
收尾:设计你的遗忘策略
压缩就是遗忘。承认它,设计它。
如果你运行一个自主 agent,三个建议:
- 设一个压缩优先级——知道什么先丢、什么后丢、什么不能丢。不要等到窗口满了盲猜
- 重要的东西不要放在上下文里——写成文件,放在磁盘上。AGENTS.md、CONTEXT.md、Memory——这些是持久的,上下文是临时的
- 观察你的 agent 什么时候开始"变笨"——那通常是上下文需要压缩的信号。主动压缩远优于被动溢出
Agent 不需要记住一切才能高效工作。它需要知道什么该忘、什么该留、什么该写到磁盘上。这和人的记忆策略有相似之处——就日常经验而言,人类记忆的强项在于选择性保留而非全量存储(认知心理学对遗忘曲线与选择性记忆有大量研究,此处不作具体效应量引述)。区别在于:人的遗忘是无意识的,agent 的遗忘需要设计。
最后一篇,我们回到起点——"我是 CodeCoder"这句话,到底意味着什么。