Appearance
上下文压缩策略
上下文压缩要处理的是一个很现实的工程限制:上下文窗口总有上限。
平时做一两轮问答,你不太会撞到这个限制。但只要任务开始变长,问题就会马上出现。比如一个 Agent 连续工作几十步,先读仓库文件,再跑命令,再看网页,再回头改代码。每一步的历史、工具调用和返回结果都会继续留在上下文里。窗口再大,也经不起这样一直堆。
这就是上下文压缩存在的原因。当历史已经很长时,怎样尽量保留任务连续性、让 Agent 还能接着干——这比"把旧消息删掉"要复杂得多。
本章目标
- 理解上下文窗口的限制和影响
- 掌握两种主要压缩策略:全量摘要压缩和微压缩
- 了解 Claude Code 的自动压缩触发机制
- 知道压缩会带来什么信息损失,以及怎么减少损失
为什么上下文长度是个工程问题
很多人第一次听到上下文窗口,注意力只放在"最多能塞多少内容"。但在真实系统里,它至少牵涉三件事:成本、速度和质量。
先说成本。Token 是计费单位,每次推理都要把当前上下文重新送给模型。任务跑得越久,历史越长,每一轮调用就越贵。一个已经跑了 50 步的 Agent,每一轮都要为前面 49 步的历史买单。
再说速度。上下文越长,模型处理越慢。前几轮响应很快,后面越来越拖——每次推理都背着越来越重的包袱。
更容易被忽略的是质量。很多人直觉上觉得"带的信息越多越安全",可长上下文并不总会带来更好的结果。相反,历史消息一多,重要信息会被稀释。模型可能抓住早期某个已经被证伪的方向,也可能忽略刚刚才出现的关键约束。
所以,上下文压缩对长任务来说就是系统设计的一部分。你要考虑的是"什么时候压、压哪些、压完之后怎么避免丢关键东西"。
一个很常见的撞墙场景
假设你让一个 Coding Agent 修一个线上 bug。它可能会这样工作:
- 先读报错日志
- 再搜索相关文件
- 连续读取多个模块的代码
- 跑测试,得到一大段终端输出
- 改完之后再看 diff,再继续测试
问题在于,这些步骤里最占上下文的,往往不是对话本身,而是工具返回的大段内容。几十次文件读取、几次长日志输出,再加上网页正文或者搜索结果,很快就会把窗口吃掉。
工程上不能只盯着"聊天消息有多少轮"。真正把上下文挤爆的,经常是工具结果。
为什么不能简单丢掉最早的消息
很多人第一次想到的方案都很自然:窗口满了,那就像队列一样把最早的历史删掉不就行了?
问题在于,对话历史不是一串等价的数据包。早期消息里经常藏着后面一直要依赖的状态:
- 最初的任务目标和限制条件
- 用户明确说过的禁止事项
- 已经验证过的错误路径
- 某次工具调用后得出的关键结论
如果直接 FIFO 式删除,系统看起来像是"保留了最新消息",其实很可能把任务骨架删掉了,只留下最近几轮操作碎片。模型会突然不知道自己为什么在做这件事,也不知道哪些方向其实已经试过并失败。
真正难的地方在于:删掉之后,任务状态还能不能成立。
两种主要压缩策略
全量摘要压缩(Compact)
全量摘要压缩的思路很直接:既然历史太长,那就不要原样带着跑了,先把这段历史总结成一份更短的"任务摘要",再拿摘要继续往下做。
它通常会这样工作:
- 系统发现上下文已经接近阈值
- 把当前对话历史交给模型做总结
- 保留摘要,移除大部分原始消息
- 之后的推理都基于这份摘要继续进行
它的好处也很明显:清空间最狠。一旦成功,往往能立刻腾出大量 Token,足够任务继续往前跑很久。
代价同样明显。摘要一定是有损的。原始工具输出、具体文件内容、某次试错时的细节,很容易在压缩后消失。更麻烦的是,如果摘要本身概括错了,后面所有步骤都会建立在一个失真的中间记忆上。
全量摘要压缩更像一次"大换气"。上下文真的已经重到影响运行时,用它来腾空间很有效。但别指望它能做到无损——它做不到。
微压缩(MicroCompact)
微压缩走的是另一条路。它不碰整段对话结构,而是优先对最占空间的历史工具结果下手。
例如,Agent 半小时前读过一个 500 行文件,当时有用,现在已经不重要了。微压缩不会去重写整段历史,而是把那条超长工具返回替换成一个简短标记,告诉模型:"这里之前读过内容,但正文已经清掉了。"
这样做的好处是,对话主干还在。系统仍然知道自己读过哪个文件、跑过哪个命令、查过哪个网页,只是把最肥的原始内容删薄了。
Claude Code 的微压缩只对特定工具的返回结果做处理(来自 services/compact/microCompact.ts),包括:
FileReadTool(文件读取,可能返回大段代码)BashTool、PowerShellTool(命令输出,可能有大量日志)GrepTool、GlobTool(搜索结果,可能很长)WebFetchTool、WebSearchTool(网页内容,通常非常长)
微压缩的信息损失通常比全量摘要小,但它也没法解决所有问题。如果任务已经特别长,只清理一些旧工具结果,可能还是不够。这时系统往往会先做微压缩,实在腾不出足够空间,再上全量摘要。
实际使用中,这两种策略通常是分层配合的:
- 先做局部清理,尽量保住原始对话结构
- 还不够,再做整段摘要,把历史整体压缩一遍
压缩本质上是在重写状态表示
换个角度理解:上下文压缩在做的事情,是把一份冗长的运行历史重写成更紧凑的状态表示。
原始对话里包含了很多层东西:用户目标、过程性尝试、工具调用、失败日志、中间结论、临时噪音。压缩做的事情,是尽量把其中真正决定后续行为的部分留下来,把只是曾经出现过但现在不再关键的部分缩掉。
这也是为什么压缩一定会有损。因为"哪些信息决定后续行为"这件事,本身就带有判断。系统不可能完美知道未来哪一个细节还会被重新用到,它只能基于当前状态做近似选择。
好的压缩策略追求的是"在有限空间里,保留最有决策价值的状态",而不只是把体积压到最小。
Claude Code 的自动压缩机制
Claude Code 的上下文压缩是自动触发的,用户通常不需要自己盯着 Token 数看(来自 Claude Code v2.1.88 services/compact/autoCompact.ts)。
触发阈值
Claude Code 对每次推理的 Token 用量做实时监控,设置了多个警戒线:
- 自动压缩阈值:Token 用量超过有效上下文窗口减去 13,000 Token 时,触发自动压缩
- 预留摘要空间:自动压缩时,会给摘要生成预留最多 20,000 Token 的输出空间(根据 p99.99 分位数的实际摘要长度确定)
这里最重要的不是具体数字,而是设计思路:它不会等到窗口真的满了才动手。原因很简单,摘要本身也要占空间。如果已经把窗口塞死了,连"生成摘要"这一步都做不了。
提前预留 buffer 是为了保证压缩机制自己还能工作。很多稳定性设计都有类似逻辑:表面看起来保守,实际是在避免系统进入不可恢复的状态。
连续失败的熔断机制
如果自动压缩连续失败 3 次(比如历史已经复杂到连摘要都不好生成),系统会停止继续尝试,避免无意义地重复调用。
源码注释里有一段真实的工程日志:
"BQ 2026-03-10: 1,279 个 session 有 50+ 次连续失败(最高 3,272 次),每天全球浪费约 25 万次 API 调用。"
这个熔断机制(MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES = 3)解决的是真实线上成本问题。会话一旦进入"怎么压都压不动"的状态,继续死磕只会烧钱。
手动触发
Claude Code 也支持用户主动触发压缩(/compact 斜杠命令),适合在你自己已经感觉到上下文很乱、想先做一次整理时使用。手动压缩的阈值设置更激进,因为用户主动触发,通常就是希望尽可能多腾出空间。
如果自己实现自动触发
自己做 Agent 系统时,不一定一开始就需要复刻 Claude Code 的完整策略,但至少要有一个主动触发点。最糟糕的做法是等模型 API 报 context length exceeded,再临时想办法补救。那时已经太晚了,因为压缩本身也需要上下文空间。
一个可落地的最小策略是:每次准备调用模型前,先估算当前消息和工具结果的 token 用量;超过安全阈值时,先压缩,再继续执行任务。
python
from dataclasses import dataclass
@dataclass
class ContextBudget:
max_tokens: int = 128_000
output_reserve: int = 8_000
compact_reserve: int = 12_000
@property
def compact_threshold(self) -> int:
return self.max_tokens - self.output_reserve - self.compact_reserve
def should_compact(input_tokens: int, budget: ContextBudget) -> bool:
return input_tokens >= budget.compact_threshold这里的数字不用死记。真正重要的是三块空间要分开想:当前输入会占多少,下一轮模型输出要预留多少,压缩动作本身还要预留多少。只要你把这三者混在一起,系统就很容易在最需要压缩的时候反而没空间压缩。
触发压缩后,建议把压缩过程也记录成一条明确事件,而不是悄悄替换历史:
python
compact_event = {
"type": "context_compacted",
"before_tokens": 104_320,
"after_tokens": 31_600,
"strategy": "tool_result_trim_then_summary",
}以后排查 Agent 为什么突然忘了某个细节时,这条记录很有用。你能看到遗忘发生在压缩前还是压缩后,也能判断是不是某次摘要把关键约束漏掉了。
压缩的信息损失
上下文压缩是有损操作。搞清楚"会丢什么、通常还能保什么",比记住机制名字管用得多。
会丢失的信息:
- 早期工具调用的原始返回结果(大文件的具体内容、详细日志)
- 模型的中间推理过程(思考链的细节)
- 未被摘要者识别为"重要"的细节
通常能保留的信息:
- 任务目标
- 已完成的步骤和结论
- 重要的错误和修正
- 当前状态
压缩之后最常见的情况是:系统记得自己做过什么,但忘了细节。于是你会看到这些现象:
- 它知道自己查过某个方向,却记不清为什么放弃
- 它记得读过某个文件,但已经不记得文件里哪一段关键
- 摘要里如果有错误,后面会一直沿着这个错误继续推
工程上减少损失的办法,说白了就是提前把重要信息外置出来:
- 对关键结论及时落盘,比如写入任务清单、记到规则文件或中间结果文件
- 用结构化记录保存关键状态,而不是只留一句模糊摘要
- 需要长期参考的内容,优先记路径、记文件名、记定位方式,别把希望全押在压缩后的记忆上
压缩能延长对话寿命,但替代不了真正的状态管理。
压缩后的质量验证
压缩完成不代表任务状态还完整。工程上最好做一次轻量验证,检查摘要里是否还保留了继续执行所需的关键字段。
可以把压缩摘要要求成固定结构,而不是让模型自由写一段散文:
text
请把当前任务状态压缩成以下结构:
1. 用户最终目标
2. 必须遵守的限制
3. 已完成的步骤
4. 已经失败或放弃的方向
5. 当前正在处理的文件、接口或数据
6. 下一步最合理的动作拿到摘要后,系统可以做几项简单检查:
用户最终目标不能为空必须遵守的限制不能丢掉用户明确禁止的事项当前正在处理的文件、接口或数据必须包含最近几轮的关键对象下一步最合理的动作不能和已放弃方向冲突
这些检查不需要很复杂。它们拦不住所有问题,但能挡住最常见的压缩事故:摘要变得很通顺,却丢掉了任务真正依赖的状态。
工具结果太大时怎么截断
工具结果是上下文里最容易失控的部分。读一个 10,000 字网页、打印一整段测试日志、读取一个超长文件,都可能让后续几轮推理变慢变贵。
我更建议在工具层先做截断,而不是等结果进入对话后再处理。工具返回可以采用"摘要 + 可定位引用"的形式:
python
def shape_tool_result(content: str, source: str, max_chars: int = 4000) -> dict:
if len(content) <= max_chars:
return {
"source": source,
"truncated": False,
"content": content,
}
return {
"source": source,
"truncated": True,
"content": content[:max_chars],
"note": "结果已截断。如需完整内容,请按 source 重新读取更小范围。",
}这段逻辑看起来简单,但比把完整内容塞给模型可靠。模型至少知道结果被截断了,也知道下一步应该去哪里取更小范围的原文。不要让模型误以为自己已经看完了全部材料。
为什么 CLAUDE.md 这类记忆不会和历史一起丢
很多人容易混淆的一点:上下文压缩压的是对话历史,提示词里注入的固定信息不受影响。
像 Claude Code 里的 CLAUDE.md,来自 Claude Code v2.1.88 utils/claudemd.ts,会在每次会话构建系统提示词时重新加载。它属于运行时注入的记忆层,和对话里临时生成的聊天记录是两码事。
这意味着两件事:
- 对话历史再长、再被压缩,
CLAUDE.md里的项目规则仍然会继续存在 - 这类持久规则和"本轮任务过程中得到的中间结论"不是一回事,前者稳定,后者仍然需要你自己妥善保存
压缩和持久化记忆是协同的。一个负责让本轮别爆,一个负责让跨轮规则别丢。
上下文长度的工程设计建议
如果你自己在做 Agent 或长任务系统,有几条具体建议。
1. 控制工具返回的数据量
如果工具会返回大量数据,在工具层就要做截断和筛选,只把当前真正相关的部分送回模型。文件、日志、网页正文都是上下文杀手,别指望后面再补救。
2. 任务分段
真正长的任务,不要默认一个会话从头跑到尾。更稳的做法是阶段化:每一段结束时提炼关键结论,再进入下一段。这样你是在主动管理记忆,而不是等窗口报警。
3. 不要依赖压缩后的历史
重要的中间结果要即时记录,不能指望"上下文里应该还记得"。一旦发生压缩,这部分记忆要么被缩短,要么被误概括。系统如果没有外部状态,很容易在长任务里反复兜圈。
4. 监控 Token 用量
长任务里要监控每轮 Token 消耗。你不一定需要做特别复杂的可视化,但至少要知道是谁在吃上下文,是文件读取太重,还是某个循环在不断复制历史。
容易踩的坑
把这一章只理解成"窗口满了就做摘要",做长任务系统时还是会出问题。
最该压缩的通常是大块工具输出,文件、日志和网页才是上下文杀手,聊天语句反而占不了多少。能外置的状态就别留在上下文里,压缩保的是连续性,扛不住长期记忆的职责。
摘要也别当真相看。它只是当前时刻对历史的一种近似重写,遗漏细节很正常。预留 buffer 同样值得注意——没有 buffer,连压缩这一步本身都可能做不出来。
成熟的系统会主动分段、记录、归档,然后在干净的上下文里重新启动,而不是把所有希望都押在一个超大窗口上。
你应该怎么理解这章和前后章节的关系
- Agent 基础原理:那一章告诉你 Agent 为什么会不断计划、行动、观察;这一章补上另一个现实问题,Agent 跑久了之后怎么活下来
- 持久化记忆系统:压缩解决"当前会话太长",持久化记忆解决"跨会话还要记住什么",各管各的
- LLM 基础概念:Prompt Cache 解决的是重复上下文带来的成本问题;上下文压缩解决的是历史已经太长、再带下去就跑不动的问题
学完本章后,你应该能回答
- 上下文窗口满了为什么不能简单地"丢掉最早的消息"?
- 全量摘要压缩和微压缩各自的取舍是什么?什么情况下选哪种?
- Claude Code 的自动压缩为什么不等到窗口真正满了才触发?
- 压缩之后哪些信息最容易丢失?工程上怎么缓解这个问题?
- 为什么源码里需要熔断机制(连续失败 3 次就停止)?
下一章
继续阅读 AI 应用系统设计。前面几章讲的是一个个零件:Agent Loop、工具系统、记忆、压缩。系统设计这一章要处理的,是怎么把这些零件装成一个能长期维护的应用——跑通 demo 只是第一步。