Context Compact --- 上下文总会满,要有办法腾地方
参考资料:https://github.com/shareAI-lab/learn-claude-code
核心思想:便宜的先跑,贵的后跑 --- 四层压缩策略,从零成本操作到按需调用LLM,再到紧急兜底。
问题背景
Agent在大型项目中工作时,会不断累积工具调用结果、文件内容、命令输出到messages列表中:
- 读一个1000行文件 → ~4000 token
- 读30个文件 + 跑20条命令 → 大量token累积
- 上下文窗口有限 → API返回
prompt_too_long
不压缩,Agent根本无法在大项目里持续工作。
四层压缩管线(执行顺序)
实际执行顺序是:budget → snip → micro → auto
| 层级 | 名称 | 触发方式 | API成本 | 信息损失 | 作用 |
|---|---|---|---|---|---|
| L3 | tool_result_budget |
每轮自动 | 0 | 中(落盘) | 大工具结果落盘,留预览 |
| L1 | snip_compact |
每轮自动 | 0 | 中(裁对话) | 裁掉无关的旧对话中间部分 |
| L2 | micro_compact |
每轮自动 | 0 | 低(占位) | 旧工具结果替换为简短占位符 |
| L4 | compact_history |
超阈值触发 | 1次API | 高(摘要) | LLM全量摘要压缩 |
L1: snip_compact --- 裁掉无关的旧对话
Agent跑了80轮对话,最早的"帮我创建hello.py"和当前工作几乎无关。
- 消息数超过50条 → 保留头部3条 + 尾部47条
- 特殊保护:不能把
assistant(tool_use)和后面的user(tool_result)拆开
python
def snip_compact(messages, max_messages=50):
if len(messages) <= max_messages:
return messages
head_end, tail_start = 3, len(messages) - (max_messages - 3)
# 保护 tool_use ↔ tool_result 完整性
if _message_has_tool_use(messages[head_end - 1]):
while head_end < len(messages) and _is_tool_result_message(messages[head_end]):
head_end += 1
# ... 插入占位符
L2: micro_compact --- 旧工具结果占位
Agent连续读了10个文件,第1-7次的完整内容早就不需要了。
- 只保留最近3条
tool_result的完整内容 - 更旧的替换为一行占位符:
"[Earlier tool result compacted. Re-run if needed.]"
python
KEEP_RECENT_TOOL_RESULTS = 3
def micro_compact(messages):
tool_results = collect_tool_result_blocks(messages)
if len(tool_results) <= KEEP_RECENT_TOOL_RESULTS:
return messages
for _, _, block in tool_results[:-KEEP_RECENT_TOOL_RESULTS]:
if len(block.get("content", "")) > 120:
block["content"] = "[Earlier tool result compacted. Re-run if needed.]"
return messages
L3: tool_result_budget --- 大结果落盘
模型一次读了5个大文件,单条tool_result加起来500KB。
- 统计最后一条user消息里所有
tool_result总大小 - 超过200KB → 按大小排序,从最大的开始落盘到
.task_outputs/tool-results/ - 上下文只留
<persisted-output>标记 + 前2000字符预览
python
def tool_result_budget(messages, max_bytes=200_000):
# 统计最后一条user消息中所有tool_result
# 超过阈值 → 落盘最大的,替换为预览
L4: compact_history --- LLM全量摘要
前三层跑完后token仍然超阈值 → 触发LLM摘要:
- 保存transcript :完整对话写入
.transcripts/(JSONL格式) - LLM生成摘要:保留当前目标、重要发现、已改文件、剩余工作、用户约束
- 替换消息列表:所有旧消息 → 一条摘要消息
python
def compact_history(messages):
transcript_path = write_transcript(messages) # 先保存完整对话
summary = summarize_history(messages) # LLM 生成摘要
return [{"role": "user",
"content": f"[Compacted]\n\n{summary}"}]
熔断器:连续失败3次后停止重试,防止死循环浪费API调用。
应急: reactive_compact
当API返回prompt_too_long(413错误)时触发:
- 比
compact_history更激进 - 从尾部回退,但仍要避免留下孤立的
tool_result - 重试上限:默认1次
python
def reactive_compact(messages):
transcript = write_transcript(messages)
summary = summarize_history(messages)
tail_start = max(0, len(messages) - 5)
# 保护 tool_result 完整性
if _is_tool_result_message(messages[tail_start]) and _message_has_tool_use(messages[tail_start - 1]):
tail_start -= 1
return [{"role": "user",
"content": f"[Reactive compact]\n\n{summary}"}, *messages[tail_start:]]
执行顺序(为什么不能换)
CC源码query.ts中的真实顺序:
applyToolResultBudget(L3)--- 先处理大结果,确保完整内容落盘snipCompact(L1)--- 裁中间消息microcompact(L2)--- 旧结果占位contextCollapse(上下⽂折叠,由 Feature Flag控制的实验性可选功能):将已完成的⼯具调⽤结果折叠为⼀⾏摘要。⼀个返回了3000 ⾏⽂件内容的 read_file 调⽤,如果后续已经基于这个⽂件完成了编辑,就折叠为 已读取并编辑 config.yaml 。autoCompact(L4)--- LLM全量摘要
顺序不能换的原因 :L3(budget)必须在L2(micro)前面,因为micro_compact会把旧的tool_result替换成一行占位符,budget必须在那之前把完整内容落盘。
在源码中,Context Collapse 作为独⽴模块存放在src/services/contextCollapse/ ⽬录下(包含 index.ts 、 operations.ts 、 persist.ts 三个⽂件)。从 autoCompact.ts 中的引⽤关系可以看到,当 Context Collapse 功能启⽤时,它会取代⾃动压缩成为主要的上下⽂管理策略------在90% 上下⽂占⽤时开始提交折叠,95% 时阻⽌新的⼦ Agent ⽣成。这意味着 Context Collapse 的"精确⼿术⼑"优先于 AutoCompact 的"全⾯摘要",只有当 Collapse ⽆法处理时,才退回到传统的 LLM 摘要压缩。
⚖️ 核心区别对比
| 特性 | Context Collapse (上下文折叠) | MicroCompact (微压缩) |
|---|---|---|
| 核心策略 | 折叠与投影:将早期对话"折叠"成摘要投影 | 清理与去重:移除冗余、过时的"垃圾"信息 |
| 操作对象 | 有价值的完整对话轮次 | 冗余的重复消息、旧工具结果等 |
| 触发时机 | 上下文压力达到高水位(90%/95%)时 | 每一次 API 调用前都会执行 |
| 信息可逆性 | 可恢复。原始数据保留,只是创建了"投影" | 不可逆。被识别为冗余的信息会被直接移除 |
| 主要目的 | 系统性管理上下文窗口,为重要信息腾出空间 | 通过低成本方式清除"垃圾",优化上下文质量 |
总结一下,MicroCompact 是流水线中靠前的、非常廉价的清理步骤;而 Context Collapse 则是更靠后、更具战略性的管理手段。当 Context Collapse 启用时,它会取代 AutoCompact 成为主要的上下文管理策略,体现了系统"先做便宜的事,再做昂贵的事"的设计哲学。
整体流程
python
def agent_loop(messages):
reactive_retries = 0
while True:
# 三个预处理器(0 API 调用)
messages[:] = tool_result_budget(messages) # L3: 大结果落盘
messages[:] = snip_compact(messages) # L1: 裁中间
messages[:] = micro_compact(messages) # L2: 旧结果占位
# 还不够?LLM摘要(1 API 调用)
if estimate_token_count(messages) > THRESHOLD:
messages[:] = compact_history(messages)
try:
response = client.messages.create(...)
except PromptTooLongError:
if reactive_retries < MAX_REACTIVE_RETRIES:
messages[:] = reactive_compact(messages) # 应急
reactive_retries += 1
continue
raise
# ... 工具执行 ...
核心设计原则
- 便宜的先跑,贵的后跑:三层零API操作先执行,仍不够才调用LLM摘要
- 分层降级防御:从无损到有损再到紧急丢弃
- 保护消息完整性 :不能拆开
tool_use和tool_result配对 - 有损压缩不可逆:压缩时先保存transcript备份,但Agent不会主动翻旧档案
局限性
- 有损压缩不可逆:摘要质量取决于LLM判断,重要细节可能被丢弃
- 需要独立记忆系统补偿:压缩管的是"当前会话台面整洁度",跨会话长期知识由Memory系统负责