⭐ 06 上下文压缩
"上下文总会满,要有办法腾地方" — 三层压缩策略,换来无限会话。
Harness 层:压缩 — 干净的记忆,无限的会话。
[[00-课程概览/综述|← 返回综述]] | [[00-课程概览/教学大纲|📋 教学大纲]]
问题:上下文窗口是有限资源
[[04-子智能体|s04 子智能体]] 解决了同一层级的上下文隔离:子任务在独立 messages[] 中运行,用完即弃。但父智能体自己的上下文仍然在持续增长。
考虑一次真实的编码会话:
- 读一个 1000 行的 Python 文件 ≈ 4000 token
ls、grep、git log的输出每次几百到几千 token- 10 个文件读取 + 20 个 shell 命令 = 轻松突破 100k token
- 模型推理速度随上下文长度平方级下降
- API 成本线性增长(Claude 100k 输入 ≈ $3/次)
不压缩,智能体根本没法在大项目里持续工作。
这呼应了 [[01-智能体循环|s01 提出的循环代价]]:循环跑得越多,上下文越脏,下一条推理越慢越贵。s04 解决了跨层级污染,s06 解决的是时间维度上的上下文膨胀。
解决方案:三层压缩
Harness 工程采用三层压缩策略,激进程度递增,在信息保留和上下文管理之间寻找平衡:
Every turn:
+------------------+
| Tool call result |
+------------------+
|
v
[Layer 1: micro_compact] (静默执行,每轮)
Replace tool_result > 3 turns old
with "[Previous: used {tool_name}]"
|
v
[Check: tokens > 50000?]
| |
no yes
| |
v v
continue [Layer 2: auto_compact]
Save transcript to .transcripts/
LLM 摘要对话
Replace all messages with [summary]
|
v
[Layer 3: compact tool]
模型主动调用 compact
同上摘要机制每一层都在不同的激进程度上工作,它们不是互斥的——而是协同配合:
| 层级 | 触发方式 | 激进程度 | 信息丢失 |
|---|---|---|---|
| 第一层 | 每轮自动 | 温和 | 仅丢弃旧 tool_result 的详细内容 |
| 第二层 | Token 阈值 | 中等 | 整个对话压缩为摘要 |
| 第三层 | 模型按需 | 同第二层 | 同上,但由模型决定时机 |
第一层:micro_compact(静默压缩)
每次 LLM 调用前,将超过 N 轮(默认 3 轮)的旧 tool_result 替换为占位符。这层压缩是无感知的——模型不会察觉,因为消息结构和时间线完整,只丢失了过期的详细输出。
KEEP_RECENT = 3
def micro_compact(messages: list) -> list:
# 收集所有 tool_result 条目
tool_results = []
for msg_idx, msg in enumerate(messages):
if msg["role"] == "user" and isinstance(msg.get("content"), list):
for part_idx, part in enumerate(msg["content"]):
if isinstance(part, dict) and part.get("type") == "tool_result":
tool_results.append((msg_idx, part_idx, part))
if len(tool_results) <= KEEP_RECENT:
return messages
# 向前查找 tool_use_id 对应的 tool_name
tool_name_map = {}
for msg in messages:
if msg["role"] == "assistant":
content = msg.get("content", [])
if isinstance(content, list):
for block in content:
if hasattr(block, "type") and block.type == "tool_use":
tool_name_map[block.id] = block.name
# 清除非最近的 N 条,替换为占位符
to_clear = tool_results[:-KEEP_RECENT]
for _, _, result in to_clear:
if isinstance(result.get("content"), str) and len(result["content"]) > 100:
tool_id = result.get("tool_use_id", "")
tool_name = tool_name_map.get(tool_id, "unknown")
result["content"] = f"[Previous: used {tool_name}]"
return messages关键设计决策:
- 保留最近 3 条完整 tool_result,确保模型能看到最近的工具输出用于决策
- 占位符包含
tool_name,让模型知道曾经执行过什么操作,只是丢失了具体输出 - 只替换长度 > 100 字符的结果——短结果直接保留,进一步减少信息损失
第二层:auto_compact(自动摘要)
当 estimate_tokens() > THRESHOLD(默认 50k)时,触发自动压缩:
def auto_compact(messages: list) -> list:
# 保存完整对话到磁盘,用于恢复
TRANSCRIPT_DIR.mkdir(exist_ok=True)
transcript_path = TRANSCRIPT_DIR / f"transcript_{int(time.time())}.jsonl"
with open(transcript_path, "w") as f:
for msg in messages:
f.write(json.dumps(msg, default=str) + "\n")
# LLM 生成摘要
conversation_text = json.dumps(messages, default=str)[:80000]
response = client.messages.create(
model=MODEL,
messages=[{"role": "user", "content":
"Summarize this conversation for continuity. Include: "
"1) What was accomplished, 2) Current state, 3) Key decisions made. "
"Be concise but preserve critical details.\n\n" + conversation_text}],
max_tokens=2000,
)
summary = response.content[0].text
# 将所有消息替换为压缩后的两条消息
return [
{"role": "user", "content": f"[Conversation compressed. Transcript: {transcript_path}]\n\n{summary}"},
{"role": "assistant", "content": "Understood. I have the context from the summary. Continuing."},
]为什么分层还不够? auto_compact 在 token 超标时触发,但它是一个昂贵的操作(需要一次 LLM 调用生成摘要)。micro_compact 的低成本让它可以在每一轮执行——两者互补,不是替代。
第三层:compact tool(手动触发)
当模型自己觉得上下文太臃肿时,可以调用 compact 工具主动触发压缩。这给了模型元认知能力——它知道自己何时需要"忘掉过去"才能继续前进。
{"name": "compact", "description": "Trigger manual conversation compression.",
"input_schema": {"type": "object", "properties": {
"focus": {"type": "string", "description": "What to preserve in the summary"}
}}}三层整合
def agent_loop(messages: list):
while True:
micro_compact(messages) # Layer 1: 每轮压缩
if estimate_tokens(messages) > THRESHOLD:
messages[:] = auto_compact(messages) # Layer 2: 自动压缩
response = client.messages.create(...)
# ... 工具执行 ...
if manual_compact:
messages[:] = auto_compact(messages) # Layer 3: 手动压缩注意 messages[:] = 的语法——它在原地替换列表内容,确保所有引用同步更新。
⭐ 核心设计原则
Transcript 即真相
auto_compact 的关键原则:压缩不是删除,是归档。 完整的历史通过 transcript 保存在磁盘上的 .transcripts/ 目录中。信息没有真正丢失,只是移出了活跃上下文。
这在调试时尤其重要——如果压缩丢失了关键信息,可以重放 transcript 进行恢复。
Micro 是 MVP
三层压缩中最重要的是第一层 micro_compact。它 几乎零成本(纯数组操作,无 LLM 调用),无感知(模型不察觉),无副作用(不改变消息结构)。
一个常见的误区是认为"等上下文满了再压缩也不迟"。但实际上,模型在上下文饱满时的推理质量已经下降了。micro_compact 让上下文始终保持苗条,而不是满了再做减肥。
与 s04 子智能体的协同
[[04-子智能体|s04]] 和 s06 解决的是同一个问题的两个维度:
| s04 子智能体 | s06 上下文压缩 | |
|---|---|---|
| 解决的问题 | 跨任务上下文污染 | 时间线上的上下文膨胀 |
| 方式 | 丢弃子任务的完整历史 | 压缩父任务的过时信息 |
| 粒度 | 整体丢弃 | 逐层渐进式压缩 |
| 信息保留 | 仅摘要文本返回父智能体 | Transcript 磁盘归档 |
两者结合使用:子智能体保证执行过程不污染主脑,上下文压缩保证历史对话不拖慢当前推理。
相对 s05 的变更
| 组件 | 之前 (s05) | 之后 (s06) |
|---|---|---|
| Tools | 5(基础 + task) | 5(基础 + task + compact) |
| 上下文管理 | 无 | 三层压缩 |
| Micro-compact | 无 | 旧结果 → 占位符 |
| Auto-compact | 无 | Token 阈值触发 |
| Transcripts | 无 | 保存到 .transcripts/ |
从 Harness 工程演进来看:
- [[01-智能体循环|s01]] — 循环骨架
- [[02-工具使用|s02]] — 工具分发
- [[03-待办写入|s03]] — 任务规划
- [[04-子智能体|s04]] — 上下文隔离
- [[05-技能加载|s05]] — 知识注入
- s06 — 上下文压缩,无限会话
试一试
cd learn-claude-code
python agents/s06_context_compact.py试试这些 prompt(英文 prompt 对 LLM 效果更好,也可以用中文):
Read every Python file in the agents/ directory one by one(观察 micro-compact 替换旧结果)Keep reading files until compression triggers automaticallyUse the compact tool to manually compress the conversation
观察要点
- 第一组
Read agents/*.py时,注意后面的 tool_result 内容是否被替换为[Previous: used read_file]占位符 - 持续读文件,观察 token 计数何时触发 Threshold(50k)
- auto_compact 后,检查
.transcripts/目录中是否生成了 transcript 文件 - 使用 compact 工具后,模型是否还能从摘要中准确回忆之前的信息
挑战
- 如果把
KEEP_RECENT从 3 改为 0,micro_compact 会做什么?模型还能正常工作吗? - 为什么 auto_compact 的 summary prompt 要包含 "What was accomplished, Current state, Key decisions made" 三个维度?
- 如果要支持增量压缩(在旧摘要基础上追加新信息而不是重新摘要全部对话),代码需要怎么改?
本课小结
- 上下文窗口是有限的——不压缩,Agent 无法在大项目中持续工作
- micro_compact 是 MVP——零成本替换旧 tool_result,每轮无感知执行
- auto_compact 在 token 超标时触发——保存 transcript 后用 LLM 摘要代替全部历史
- compact tool 让模型拥有元认知——自己决定何时需要压缩
- Transcript 归档确保信息没有真正丢失——压缩 = 存档,不是删除
- 三层压缩是时间维度上对 [[04-子智能体|s04 空间维度]] 上下文管理的补充
下节预告
上下文压缩让 Agent 无限会话成为可能,但单体 Agent 终究有天花板。[[07-任务系统|s07 任务系统]] 将引入任务抽象——把大任务分解为可追踪、可恢复、可并行的单元,走向真正的工程化 Agent。
⭐ 核心路径
s01 [智能体循环] → s02 [工具使用] → s03 [待办写入] → s04 [子智能体]
→ s05 [技能加载] → s06 [上下文压缩] → s07 [任务系统] → s09 [智能体团队][[05-技能加载|← Lesson 05:技能加载]] ⭐ | [[07-任务系统|Lesson 07:任务系统 →]] ⭐
延伸阅读
- Learn Claude Code 项目 — 练习代码和完整文档
- [[00-课程概览/教学大纲|教学大纲]] — 查看完整课程地图
- [[00-课程概览/综述|课程综述]] — Agent Harness 工程导论
- [[术语表#Token估计|Token 估计]] — 快速计算上下文使用量
- [[讨论课/压缩策略比较|讨论课:压缩策略比较]] — 不同压缩方式的优劣分析
「让模型学会遗忘,它才能记住真正重要的东西」