AI Agent 上下文压缩(三):让 LLM 给自己写摘要 —— Auto Compact 与三级压缩闭环
从 Claude Code 167K 阈值到 9 章节摘要 prompt,再到 tiny-claw 的落地
当 Tool Result Budget 和 Microcompact 都无法阻止 token 逼近上下文窗口上限时,我们需要一种"终极手段":让 LLM 读一遍自己的历史记录,写一份结构化摘要,然后把旧历史全部清空。
🎯 问题:为什么 Microcompact 还不够?
前两篇我们聊了 Tool Result Budget(大结果写磁盘)和 Microcompact(淡化旧工具结果)。这两个机制很轻量,能省下大量 token。
但随着对话不断深入,即使工具结果被压缩了,其他的 token 依然在累积:
- 用户的长提示词和反馈
- Assistant 的思考过程(
<thinking>块) - 调工具的 JSON 结构(
tool_use) - 重新被读回来的文件内容
当这些内容把 200K 的上下文窗口塞满时,API 就会报错 prompt_too_long。此时,我们需要一种能彻底重置上下文大小,同时又保留关键记忆的机制。
这就是 Auto Compact。
🧠 核心思路:让 LLM 给自己写传记
Auto Compact 的思路非常直接:既然上下文快满了,那就花一次 API 调用的钱,让 LLM 把整个对话历史总结成一段精炼的摘要。
整个历史 + 总结指令 → LLM 生成摘要 → 替换所有旧消息
这就像长篇小说连载时,出版社要求作者写一段"前情提要",让新读者(下一次 API 调用)能快速跟上剧情。
🔍 触发时机:167K 是怎么算出来的?
什么时候触发 Auto Compact?太早了浪费钱(多调一次 API),太晚了可能连生成摘要的 token 都不够了。
Claude Code 在 src/services/compact/autoCompact.ts 中定义了三个关键常量:
const MAX_OUTPUT_TOKENS_FOR_SUMMARY = 20_000
export const AUTOCOMPACT_BUFFER_TOKENS = 13_000
export const WARNING_THRESHOLD_BUFFER_TOKENS = 20_000
以 200K 上下文的模型为例,它的计算逻辑是这样的:
- 有效窗口 (
effectiveContextWindow): 200K - 20K (预留给摘要输出) = 180K - 触发阈值 (
autoCompactThreshold): 180K - 13K (Buffer) = 167K
也就是说,当 token 估算达到 167K 时,就会触发 Auto Compact。
此外,还有一条警告线: 180K - 20K = 160K 时,系统可能会在 UI 上提示用户上下文快满了。
🚦 门禁与熔断:避免死循环
Auto Compact 是个重操作,不能随便触发。Claude Code 设置了严格的门禁:
querySource === 'compact': 如果当前正在执行 compact,不能递归触发。- 环境变量
DISABLE_COMPACT或DISABLE_AUTO_COMPACT。 - 用户配置
autoCompactEnabled === false。 feature('CONTEXT_COLLAPSE'): 如果启用了更高级的上下文折叠特性,交由它处理。- 熔断机制 (
consecutiveFailures >= 3)。
重点说说这个熔断机制。源码里有一段非常有说服力的注释:
Stop trying autocompact after this many consecutive failures. BQ 2026-03-10: 1,279 sessions had 50+ consecutive failures (up to 3,272) in a single session, wasting ~250K API calls/day globally.
如果上下文已经处于一种"不可恢复的超限"状态(比如单条消息就超过了 200K),Auto Compact 会一直失败。如果不加熔断,Agent 就会在每一轮都尝试 compact,导致无限死循环,每天浪费几十万次 API 调用。所以,连续失败 3 次后,直接放弃。
📝 9 章节摘要 prompt 的设计
让 LLM 写摘要,不是简单一句 "Summarize the conversation" 就行的。为了保证 Agent 能无缝接手工作,Claude Code 设计了一个非常严密的 9 章节 prompt (src/services/compact/prompt.ts):
<analysis>
[思考过程 - 确保覆盖所有要点, 之后会被 strip 掉]
</analysis>
<summary>
1. Primary Request and Intent: 用户请求与意图
2. Key Technical Concepts: 技术概念、框架
3. Files and Code Sections: 文件与代码片段 (含完整代码)
4. Errors and Fixes: 遇到的错误与修复方法
5. Problem Solving: 问题解决过程
6. All User Messages: 所有用户消息 (非 tool 结果)
7. Pending Tasks: 待办任务
8. Current Work: 当前正在做的工作 (最详细)
9. Optional Next Step: 下一步 (必须与最近请求对齐)
</summary>
这里有两个精妙的设计:
<analysis>草稿区: 强制模型先写一段 analysis。这利用了 LLM 的 Chain-of-Thought 特性,让它先梳理思路,再输出正式的 summary。在最终替换消息时,这段 analysis 会被正则剔除,不占用宝贵的上下文。- 防工具调用结界: Prompt 的开头和结尾都有极度严厉的警告:
CRITICAL: Respond with TEXT ONLY. Do NOT call any tools.Tool calls will be REJECTED and will waste your only turn — you will fail the task.因为执行 compact 的子 Agent 继承了父 Agent 的工具集,如果它手痒调了工具,这次 compact 就废了。
🏛️ Compact Boundary:不删除,只画一条线
生成摘要后,怎么替换旧消息?
Claude Code 的做法不是删除数组里的旧消息,而是在数组末尾插入一条 system 消息作为锚点(Boundary),再跟上包含摘要的 user 消息。
messages = [
... 旧消息 (压缩前) ...
{ type: 'system', subtype: 'compact_boundary' } // ← 锚点
{ type: 'user', content: 'This session is being continued... \n\n Summary: ...' }
... 新消息 ...
]
在发送给大模型 API 前,客户端会在本地调用 getMessagesAfterCompactBoundary(messages) 对数组进行切片。大模型 API 收到的只是一个普通的 messages 数组,它并不知道 boundary 的存在,只会看到锚点之后的新内容(即摘要加上后续的新对话)。旧消息被永远留在了本地,不会再发给大模型。
为什么不直接删除?
- 可追溯性: 本地日志和 Transcript 仍然保留完整历史,用户随时可以查看。
- 链式追踪: 如果发生多次 compact,可以通过边界追溯每一次的压缩状态。
- 安全回滚: 如果 compact 出了严重 bug,本地数据没丢。
这就像 Git 的 commit,我们只是把 HEAD 指针移到了新的位置,但历史记录依然存在。
🧩 Partial Compact 与 Session Memory
除了全量 Auto Compact,Claude Code 还实现了两种变体(虽然在默认流程中可能被 feature gate 隐藏或作为实验性分支):
- Partial Compact: 只总结"最近的"消息,保留更早的历史。Prompt 分为
from(总结这部分)和up_to(总结到这里为止)两个方向。 - Session Memory Compact: 尝试用更轻量的 Session Memory 机制来替代全量摘要。
这些机制展示了上下文管理的复杂性:没有一种银弹能解决所有场景,往往需要多种策略组合。
⚙️ tiny-claw 的简化实现思路
在 Rust 框架 tiny-claw 中,我们实现了一个极简版的 Auto Compact。
1. 核心数据结构
在 AgentLoop 中增加 token 追踪和 compact 状态:
pub struct AgentLoop {
// ... 现有字段 ...
token_tracker: TokenTracker,
compact_failures: u32,
compact_boundary_index: Option<usize>, // 记录锚点位置
}
2. 保留完整的 Prompt 结构
与 Claude Code 保持一致,我们保留了 <analysis> 标签。让大模型在输出最终摘要前,先进行 Chain-of-Thought (CoT) 思考,这对于生成高质量、不遗漏关键细节的摘要至关重要。
pub fn compact_prompt() -> String {
r#"You must respond with TEXT ONLY. Do NOT call any tools.
Your task is to create a detailed summary of the conversation so far.
First, wrap your analysis in <analysis> tags.
Then provide a structured <summary> with these sections:
1. Primary Request and Intent
2. Key Technical Concepts
...
9. Next Step (if applicable)
"#.to_string()
}
3. 应用 Compact
当 API 返回摘要后,我们需要像 Claude Code 一样,先用正则剔除 <analysis> 块,然后更新边界索引,并插入系统提示:
fn apply_compaction(&mut self, raw_response: String) {
// 1. 剔除 <analysis> 块,只保留 <summary>
let summary = extract_summary(&raw_response);
// 2. 记录当前位置为新的边界
self.compact_boundary_index = Some(self.messages.len());
// 3. 插入摘要消息
let content = format!(
"This conversation continues from a previous conversation.\n\
Here is a summary of the conversation so far:\n\n{}",
summary
);
self.messages.push(ChatMessage::user(&content));
// 4. 重置状态
self.token_tracker.reset();
self.compact_failures = 0;
}
📝 总结:L1 → L3 → L5 三级压缩闭环
至此,我们完整解析了 AI Agent 上下文压缩的三级管线:
| 层级 | 机制 | 触发时机 | 成本 | 作用 |
|---|---|---|---|---|
| L1 | Tool Result Budget | 工具执行完毕时 | 极低(仅写磁盘) | 防止单次超大输出(如 bash、grep)撑爆上下文。 |
| L3 | Microcompact | 消息数量/时间超过阈值 | 极低(仅替换文本) | 淡化早期已失去价值的工具结果,释放空间。 |
| L5 | Auto Compact | Token 逼近上下文窗口上限 | 高(需调一次 API) | 终极兜底,将冗长历史浓缩为结构化摘要,彻底重置上下文。 |
核心哲学:便宜的先做,贵的兜底,信息永远可恢复。
通过这三级闭环,Agent 可以在有限的上下文窗口内,进行无限轮次的对话,始终保持对当前任务的专注,同时不丢失关键的历史记忆。这就是让 AI 真正具备"长期工作能力"的基石。