s01 → s02 → s03 → s04 → s05 → s06 → s07 → s08 → s09 → s10 → ... → s19
"上下文总会满, 要有办法腾地方" — 四层压缩策略, 便宜的先跑贵的后跑。
Harness 层: 压缩 — 干净的记忆, 无限的会话。
Agent 跑着跑着,不动了。
手里有 bash、有 read、有 write,能力是够的。但它读了一个 1000 行的文件(~4000 token),又读了 30 个文件,跑了 20 条命令。每条命令的输出、每个文件的内容,全都堆在 messages 列表里。
上下文窗口是有限的。满了之后,API 直接拒绝——prompt_too_long。
不压缩,Agent 根本没法在大项目里干活。
s07 的循环、技能加载、子 Agent 全部保留。唯一的变动:每轮 LLM 调用前插入三层预处理器(0 API),token 仍超阈值时触发 LLM 摘要(1 API),API 报错时应急裁剪。
核心设计就一句话:便宜的先跑,贵的后跑。
Agent 跑了 80 轮对话,messages 攒了 160 条。最前面的"帮我创建 hello.py"和当前工作几乎无关了,但全占着位置。
消息数超过 50 条 → 保留头部 3 条(初始上下文)和尾部 47 条(当前工作),中间裁掉:
def snip_compact(messages, max_messages=50):
if len(messages) <= max_messages:
return messages
keep_head, keep_tail = 3, max_messages - 3
snipped = len(messages) - keep_head - keep_tail
placeholder = {"role": "user",
"content": f"[snipped {snipped} messages from conversation middle]"}
return messages[:keep_head] + [placeholder] + messages[-keep_tail:]裁掉了整条消息,但剩下的消息里 tool_result 内容仍在累积——第 34 条消息里可能躺着 30KB 的旧文件内容。→ L2。
Agent 连续读了 10 个文件。第 1-7 次的完整内容还躺在上下文里,早就不需要了,但占着大量空间。
只保留最近 3 条 tool_result 的完整内容,更旧的替换为一行占位符:
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旧结果清掉了,但单条新结果可能就有 500KB——一个 cat 大文件的输出就能打满上下文。→ L3。
模型一次读了 5 个大文件,单条 user 消息里所有 tool_result 加起来 500KB。
统计最后一条 user 消息里所有 tool_result 的总大小。超过 200KB → 按大小排序,从最大的开始落盘到 .task_outputs/tool-results/,上下文里只留 <persisted-output> 标记 + 前 2000 字符预览。模型看到标记后知道完整内容在磁盘上,需要时可以重新读。
def tool_result_budget(messages, max_bytes=200_000):
last = messages[-1]
blocks = [(i, b) for i, b in enumerate(last["content"])
if b.get("type") == "tool_result"]
total = sum(len(str(b.get("content", ""))) for _, b in blocks)
if total <= max_bytes:
return messages
ranked = sorted(blocks, key=lambda p: len(str(p[1].get("content", ""))), reverse=True)
for idx, block in ranked:
if total <= max_bytes:
break
block["content"] = persist_large_output(block["tool_use_id"], str(block["content"]))
total = recalculate_total(blocks)
return messages前三层都是纯文本/结构操作——0 API 调用,但也无法"理解"对话内容。上下文可能仍然太大。→ L4。
前三层全跑完了,但在超大项目中连续工作 30 分钟后,token 仍然超过阈值。
三步流程:
- 保存 transcript:完整对话写入
.transcripts/,JSONL 格式。信息没有丢失,只是移出了活跃上下文。 - LLM 生成摘要:把对话历史发给 LLM,要求保留当前目标、重要发现、已改文件、剩余工作、用户约束等关键信息。
- 替换消息列表:所有旧消息被替换为一条摘要。摘要后自动重新附加最近几个文件的内容,确保模型不会丢失当前文件上下文。
def compact_history(messages, state):
transcript_path = write_transcript(messages) # 先保存完整对话
summary = summarize_history(messages) # LLM 生成摘要
state.has_compacted = True
return [{"role": "user",
"content": f"[Compacted]\n\n{summary}"}]熔断器:连续失败 3 次后停止重试,防止死循环浪费 API 调用。
有时候 API 还是返回 prompt_too_long(413)——上下文增长速度快于压缩触发速度时。
这时触发 reactive_compact:比 compact_history 更激进,从尾部回退,以字节级精度裁剪到 API 可接受的大小,只保留最后 5 条消息 + 摘要。
def reactive_compact(messages):
transcript = write_transcript(messages)
summary = summarize_history(messages)
tail = messages[-5:]
return [{"role": "user",
"content": f"[Reactive compact]\n\n{summary}"}, *tail]def agent_loop(messages, state):
while True:
# 三个预处理器(0 API 调用)
messages[:] = snip_compact(messages) # 裁中间
messages[:] = micro_compact(messages) # 旧结果占位
messages[:] = tool_result_budget(messages) # 大结果落盘
# 还不够?LLM 摘要(1 API 调用)
if estimate_token_count(messages) > THRESHOLD:
messages[:] = compact_history(messages, state)
try:
response = client.messages.create(...)
except PromptTooLongError:
messages[:] = reactive_compact(messages) # 应急
continue
# ... 工具执行 ...顺序不能换。 便宜的先跑,贵的后跑。应急只在报错时才触发。
| 组件 | 之前 (s07) | 之后 (s08) |
|---|---|---|
| 上下文管理 | 无(上下文无限膨胀) | 四层压缩管线 + 应急 |
| 新函数 | — | snip_compact, micro_compact, tool_result_budget, compact_history, reactive_compact |
| 工具 | bash, read_file, write_file, edit_file, glob, todo_write, task, load_skill (8) | bash, read_file, write_file, task, list_skills, load_skill, compact (7) |
| 循环 | LLM 调用 → 工具执行 | 每轮前跑三层预处理器 + 阈值触发 compact_history |
| 设计原则 | — | 便宜的先跑,贵的后跑 |
cd learn-claude-code
python s08_context_compact/code.py试试这些 prompt:
Read the file README.md, then read code.py, then read s01_agent_loop/README.md(连续读多个文件,观察 L2 压缩旧结果)Read every file in s08_context_compact/(一次性读大量内容,观察 L3 落盘)- 反复对话 20+ 轮,观察是否出现
[auto compact]或[reactive compact]
观察重点:每次工具执行后,旧 tool_result 是否被压缩?连续对话后 token 超阈值时,是否自动触发了摘要?
上下文压缩让 Agent 能跑很久不会崩。但每次压缩后,用户之前告诉它的偏好、约束也跟着丢了。能不能让 Agent 有选择地记住重要的事?
s09 Memory → 三个子系统:选择记什么、提取关键信息、整理巩固。跨压缩、跨会话。
深入 CC 源码
以下基于 CC 源码
compact.ts(1705 行)、autoCompact.ts(351 行)、microCompact.ts的完整分析。
| 维度 | 教学版 | Claude Code |
|---|---|---|
| 执行顺序 | snip → micro → budget → auto | 完全一致(query.ts:379-543) |
| snip_compact | 保留头 3 + 尾 47 | 同,CC 仅主线程启用 |
| micro_compact | 文本占位符替换 | API cache_edits(不破坏 prompt cache) |
| micro_compact 白名单 | 按位置(最近 3 条) | Read/Bash/Grep/Glob/WebSearch/WebFetch/Edit/Write |
| tool_result_budget | 200KB 字符 | 200,000 字符(~50K token),完全一致 |
| compact_history 阈值 | 字符数估算 | 精确 token:contextWindow - maxOutputTokens - 13_000 |
| 摘要要求 | 5 类信息 | 9 个部分 + <analysis>/<summary> 双标签 |
| 压缩 prompt | 简单 prompt | 首尾双重防呆禁止调工具 |
| reactive_compact | 有(简化) | 字节级精度回退群组 |
| 后压缩恢复 | 无 | 自动重新读取最近文件 |
| 熔断器 | 3 次 | 3 次(遥测驱动设计) |
| 常量 | 值 | 源文件 |
|---|---|---|
AUTOCOMPACT_BUFFER_TOKENS |
13,000 | autoCompact.ts:62 |
MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES |
3 | autoCompact.ts:70 |
MAX_OUTPUT_TOKENS_FOR_SUMMARY |
20,000 | autoCompact.ts:30 |
POST_COMPACT_TOKEN_BUDGET |
50,000 | compact.ts:123 |
POST_COMPACT_MAX_FILES_TO_RESTORE |
5 | compact.ts:122 |
POST_COMPACT_MAX_TOKENS_PER_FILE |
5,000 | compact.ts:124 |
| 时间 micro_compact 间隔 | 60 分钟 | timeBasedMCConfig.ts:32 |
| PTL 重试次数 | 3 | compact.ts:227 |
| 流重试次数 | 2 | compact.ts:131 |
CC 源码中还有两个机制本教学版没有展开:
- contextCollapse:一个独立的上下文管理系统,启用时会完全替代 compact_history。它依赖 CC 的内部编译模块,教学版不展开。
- sessionMemoryCompact:compact_history 之前,CC 会先尝试用已有的 session memory(s09 会讲到)做轻量摘要,不调 LLM。这个机制等学完 s09 之后回头看会更清楚。
CC 的压缩 prompt 有两个硬性要求:
- 绝对禁止调用工具:开头就是
CRITICAL: Respond with TEXT ONLY. Do NOT call any tools.,末尾还会再 REMINDER 一次 - 先分析再总结:模型需要先在
<analysis>标签里理清思路,然后在<summary>标签里输出正式摘要。analysis 在格式化时被剥离
- micro_compact 用文本占位 → 我们没有 API 层的
cache_edits权限 - token 用字符数估算 → 精确 tokenizer 不在教学范围内
- 两个辅助机制不展开 → 属于 10% 的细节
核心设计思想——便宜的先跑,贵的后跑——完整保留。