上下文压缩:当窗口不够用时
本章摘要:
压缩的本质是**"为继续工作而丢弃过去"**——四层策略从无感到有感逐步升级:落盘 → 微压缩 → 完整压缩 → 反应式压缩。核心不是尽量少字,而是用更少的 token 保住继续工作的连续性。时机关键:自然断点做,最小化缓存损失。
上一章我们看到了上下文的全貌:系统提示词、消息历史、工具定义,每轮 API 调用都在重新发送这一切。这带来一个不可避免的问题——消息历史只增不减。
每轮对话都在追加内容:用户输入、Agent 回复、工具调用、工具结果……而上下文窗口有硬上限,没有任何商量余地。当历史膨胀到接近上限时,Agent 要么压缩,要么停止工作。
这不是一个可选的优化,而是 Agent 持续运行的硬约束。更要命的是,Agent 越能干,上下文消耗越快——它会读更多文件、跑更多命令、做更多搜索。能力越强,越需要压缩机制来维持持续运行。
四层渐进压缩策略
Agent 系统不是等撞到上限才临时处理,而是从一开始就维护一套渐进策略——从最轻量的操作到最重的操作,逐步升级:
单个工具输出太大
↓
第 1 层:落盘持久化 → 写磁盘,留预览(静默)
工具结果累积过多
↓
第 2 层:微压缩 → 删除旧工具结果(静默)
整体历史逼近窗口上限
↓
第 3 层:完整压缩 → 生成摘要替换历史(有感知)
压缩请求本身也超限
↓
第 4 层:反应式压缩 → 紧急裁剪最旧消息(最后防线)四层策略的设计思想是:尽量在用户无感知的情况下释放空间,只在必要时才执行有代价的操作。
第 1 层:大结果落盘
工具输出(尤其是 shell 命令、文件写入等)往往体积大、信息密度低。一份 3000 行的日志,Agent 看过之后需要记住的可能只是"第 142 行有个 ERROR"。与其让它一直占据上下文,不如落盘后只留引用——Agent 真需要回顾全文时,可以随时从磁盘重新读取。
这是最无声的压缩。当工具结果超过一定大小(Claude Code 默认 50K 字符)时,系统自动将完整内容写入磁盘,只在上下文中保留一个引用标记(落盘的位置)加一小段预览。
落盘是给"还没看的大块内容"保留查阅能力——Agent 还没看过这个结果,原文还有被回头查阅的价值。 当然,文件读取工具不受此限制,否则永远无法读取大文件。文件读取工具有自己的长度限制(Claude Code 的实现中最大能读取 256KB 的文件,输出长度限制大约为 25000 tokens)。
第 2 层:微压缩
微压缩发生在每轮 API 调用之前,策略极其简单直接:删除旧的工具结果内容。其主要基于时间触发——当距离上一轮助手消息超过一定时长时(比如一个小时),系统判定之前的工具结果已经"被消化过了",可以安全清理(仅保留最近几个工具结果不动)。此外还可以基于 token 总量的触发机制——当工具结果累积超过一定数量(比如 200k)时触发压缩。
被清理的位置会被替换为一个占位标记,让模型知道"这里曾经有个工具结果,但为了节省空间已被移除"。注意此过程是不可恢复的——与第 1 层的落盘不同,微压缩不会把原文写入磁盘,因为微压缩往往针对读取相关的工具,即便丢失了,仍然可以重新读取。
微压缩与落盘的核心区别是时机:落盘发生在工具结果刚进入上下文时(Agent 还没看),微压缩发生在很久之后(Agent 已经消化)。前者值得保留原文,后者不值得——如果也落盘,只会浪费磁盘。
第 3 层:完整压缩
当微压缩释放的空间不够,上下文仍然逼近上限时(一般不会到完整上限,而是预留一定窗口用于压缩),系统执行完整压缩。这是四层策略中最重的操作,也是用户感知最强的——执行后,对话历史被一份摘要替换。
压缩的核心过程
完整压缩不是"删掉一些消息"这么简单。它是一个精心设计的过程:
1. 独立 Agent 生成摘要
压缩摘要不是由主模型直接在自己的上下文里"一边工作一边总结"。系统会启动一个独立的 Agent 来专门执行压缩任务——它继承主对话的模型配置,使用相同的模型来生成摘要,但在独立的上下文中工作。
这有几重好处:
- 主模型的工作上下文不被压缩过程污染
- 独立 Agent 可以完整地审阅对话历史来生成高质量摘要
- 主模型不需要在自己的上下文窗口中同时处理压缩和当前任务
- 使用同模型来压缩可以有效利用上下文缓存,减少 token 消耗
压缩的核心不是尽量少字,而是用更少的 token 保住继续工作的连续性。
2. 摘要保留什么
一份合格的压缩摘要,至少要保住这些信息:
- 当前任务目标——Agent 需要知道自己在做什么
- 已完成的关键动作——避免重复工作
- 已修改或查看过的文件——保持代码修改的一致性
- 关键决定与约束——防止做出矛盾的选择
- 下一步应该做什么——维持工作的连续性
被丢弃的主要是完整的工具输出原文、中间推理过程、已不再相关的历史细节。
3. 重新注入关键上下文
压缩不是只留一份摘要就结束。摘要只保留了"发生了什么"的叙事,但 Agent 要继续工作还需要更多:文件内容、技能定义、计划状态等。因此压缩后,系统会专门恢复必要的上下文(最近读过的文件、调用过的技能正文、当前计划状态等)。
压缩前,每轮 API 调用发送的内容:
┌─────────────────────────────────────────────┐
│ 系统提示词(API system 参数,不受压缩影响) │
│ --- │
│ <system-reminder> │
│(AGENTS.md + 日期,每轮 prepend,缓存命中) │
│ --- │
│ user: "帮我规划京都 5 日游" │
│ assistant: 查资料 + 分析 │
│ tool_result: 景点表全文(3000 token) │
│ 附件:已调整行程、技能正文、计划状态... │
│ ...(更多轮次的对话) │
└─────────────────────────────────────────────┘
↓ 压缩(清除 AGENTS.md 缓存)
压缩后,当即构建的 API 调用内容:
┌─────────────────────────────────────────────┐
│ 系统提示词(不变) │
│ --- │
│ <system-reminder> │
│(缓存已清除,当即从磁盘重新读取最新 │
│ AGENTS.md + 当前日期) │
│ │
│ 摘要(user 消息,由独立 Agent 生成) │
│ "用户要求规划京都 5 日游,已确定岚山 + │
│ 金阁寺 + 伏见稻荷三个核心景点,交通票 │
│ 已建议用关西周游券。下一步:安排奈良 │
│ 一日往返路线。" │
│ --- │
│ compact_boundary 标记 │
│(system 消息,不发送给 API) │
│ │
│ -- 压缩后专门注入的附件 -- │
│ │
│ 全量重建:延迟工具列表、Agent 列表、 │
│ MCP 指令、计划文件、计划模式状态、 │
│ 后台 Agent 状态、Hook 结果 │
│ │
│ 文件:恢复最近 5 个文件 │
│ ├─ ≤ 阈值 (如:5000 tokens) → 完整内容 │
│ └─ > 阈值 (如:5000 tokens) → 路径引用 │
│ 还需设定总阈值 (如 50,000 tokens) │
│ │
│ 技能:实际调用过的技能正文 │
│ 每个 ≤ 固定阈值 (5,000 tokens) │
│ 总量 ≤ 固定阈值 (25,000 tokens) │
└─────────────────────────────────────────────┘一个值得注意的附带效果:AGENTS.md 的缓存被清除。压缩发生时,正常轮次中缓存的 AGENTS.md 内容被清除,下次重新从磁盘读取最新内容。
第 4 层:反应式压缩(紧急避险)
这是最后的防线。当完整压缩的请求本身也因为上下文太长而失败时,系统进入紧急模式。
反应式压缩的策略很直接:从最旧的消息开始,按 API 轮次分组删除。每丢弃一组,重新尝试压缩。
这个过程就像飞机抛掉不必要的载重来保持飞行——不是优雅的方案,但能防止坠毁。
反应式压缩理论上不应该被触发。如果前三层工作正常,上下文不会增长到连压缩请求都无法发送的程度。它的存在是为了应对极端情况。
压缩后保留什么
压缩完成后,Agent 看到的上下文发生了根本性变化。理解这个变化对设计指令很重要。
典型保留:系统提示词、项目根 AGENTS.md(重新注入)、已调用的技能正文、已读取的文件(过大时降级为路径引用)、计划文件与状态、各种 Agent 列表、MCP 指令等。
缓存代价
压缩还有一个容易被忽略的间接代价:提示缓存完全失效。
主流 LLM API 的缓存按前缀匹配工作。压缩改写了整个消息历史,导致前缀完全不同,下一轮 API 调用必须重新处理全部内容——压缩后的第一轮会明显更慢、更贵。
这也是为什么压缩的时机很关键:在任务间隙做,不要在任务中途做。在一个自然断点压缩(比如修完一个 bug 准备开始下一个),缓存损失的影响最小。
压缩如何融入主循环
主循环现在同时维护两件事:任务推进和上下文预算。两者之间需要平衡——过于激进的压缩会丢失关键信息,过于保守则可能撞到硬上限。
每轮 API 调用前:
清理旧工具结果(微压缩)
↓
检查上下文大小 ──> 超过阈值 ──> 执行完整压缩
↓ 未超过
调用 API,处理响应
↓
收到"提示词过长" ──> 反应式压缩,重试
↓ 正常响应
进入下一轮(任务完成则结束)这个设计让压缩对用户尽量透明:前两层(落盘和微压缩)完全无感,第三层(完整压缩)有感知但系统自动处理,只有第四层(反应式压缩)才意味着"出了问题"。
本章要点
- 压缩的本质是**"为继续工作而丢弃过去"**,不是尽量少字
- 四层渐进策略:落盘(静默)→ 微压缩(静默)→ 完整压缩(有感知)→ 反应式压缩(紧急)
- 完整压缩由独立 Agent 在独立上下文中生成摘要,避免污染主工作上下文
- 压缩后关键上下文会专门恢复(最近文件、技能正文、计划状态)
- 缓存代价是隐形成本——压缩后第一轮会明显更慢更贵
- 时机关键:自然断点做,任务中途不做
延伸:还有什么机制参与上下文管理
压缩是上下文管理最核心的机制,但不是唯一的。还有两个值得注意的实验性机制:
上下文折叠(Context Collapse):普通完整压缩将整个对话历史一次性替换为摘要,而折叠采用按消息组逐步压缩的策略——当上下文占用达到约 90% 时触发,将消息按组替换为摘要。原始消息落盘到文件系统中,发送给模型的是摘要文本。这种方式的好处是:如果后续需要恢复,可从文件系统还原完整历史。
基于记忆压缩(Session Memory Compact):开启该功能后,在每次发起模型调用的同时,如果判断新增 ≥ 3 次工具调用,或者最后一个 assistant 回合没有工具调用,也即一个自然对话断点,都会 fork 一个 subagent 来生成 {projectDir}/{sessionId}/session-memory/summary.md。在需要压缩时,这份文件内容会作为压缩后的上下文。它保留最近的 10K–40K token 和至少 5 条文本消息,删除更旧的内容,用已总结的内容填充压缩边界。