Skip to content

上下文压缩:当窗口不够用时

本章摘要

压缩的本质是**"为继续工作而丢弃过去"**——四层策略从无感到有感逐步升级:落盘 → 微压缩 → 完整压缩 → 反应式压缩。核心不是尽量少字,而是用更少的 token 保住继续工作的连续性。时机关键:自然断点做,最小化缓存损失

上一章我们看到了上下文的全貌:系统提示词、消息历史、工具定义,每轮 API 调用都在重新发送这一切。这带来一个不可避免的问题——消息历史只增不减

每轮对话都在追加内容:用户输入、Agent 回复、工具调用、工具结果……而上下文窗口有硬上限,没有任何商量余地。当历史膨胀到接近上限时,Agent 要么压缩,要么停止工作。

这不是一个可选的优化,而是 Agent 持续运行的硬约束。更要命的是,Agent 越能干,上下文消耗越快——它会读更多文件、跑更多命令、做更多搜索。能力越强,越需要压缩机制来维持持续运行。

四层渐进压缩策略

Agent 系统不是等撞到上限才临时处理,而是从一开始就维护一套渐进策略——从最轻量的操作到最重的操作,逐步升级:

text
单个工具输出太大

  第 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 准备开始下一个),缓存损失的影响最小。

压缩如何融入主循环

主循环现在同时维护两件事:任务推进上下文预算。两者之间需要平衡——过于激进的压缩会丢失关键信息,过于保守则可能撞到硬上限。

text
每轮 API 调用前:

  清理旧工具结果(微压缩)

  检查上下文大小 ──> 超过阈值 ──> 执行完整压缩
        ↓ 未超过
  调用 API,处理响应

  收到"提示词过长" ──> 反应式压缩,重试
        ↓ 正常响应
  进入下一轮(任务完成则结束)

这个设计让压缩对用户尽量透明:前两层(落盘和微压缩)完全无感,第三层(完整压缩)有感知但系统自动处理,只有第四层(反应式压缩)才意味着"出了问题"。

本章要点

  • 压缩的本质是**"为继续工作而丢弃过去"**,不是尽量少字
  • 四层渐进策略:落盘(静默)→ 微压缩(静默)→ 完整压缩(有感知)→ 反应式压缩(紧急)
  • 完整压缩由独立 Agent 在独立上下文中生成摘要,避免污染主工作上下文
  • 压缩后关键上下文会专门恢复(最近文件、技能正文、计划状态)
  • 缓存代价是隐形成本——压缩后第一轮会明显更慢更贵
  • 时机关键:自然断点做,任务中途不做

延伸:还有什么机制参与上下文管理

压缩是上下文管理最核心的机制,但不是唯一的。还有两个值得注意的实验性机制:

上下文折叠(Context Collapse):普通完整压缩将整个对话历史一次性替换为摘要,而折叠采用按消息组逐步压缩的策略——当上下文占用达到约 90% 时触发,将消息按组替换为摘要。原始消息落盘到文件系统中,发送给模型的是摘要文本。这种方式的好处是:如果后续需要恢复,可从文件系统还原完整历史。

基于记忆压缩(Session Memory Compact):开启该功能后,在每次发起模型调用的同时,如果判断新增 ≥ 3 次工具调用,或者最后一个 assistant 回合没有工具调用,也即一个自然对话断点,都会 fork 一个 subagent 来生成 {projectDir}/{sessionId}/session-memory/summary.md。在需要压缩时,这份文件内容会作为压缩后的上下文。它保留最近的 10K–40K token 和至少 5 条文本消息,删除更旧的内容,用已总结的内容填充压缩边界。