⚠️ 学习声明:本文档基于 Claude Code 2.1.88 源码分析整理,仅供个人学习研究使用,不做任何商业用途。
Claude 的上下文窗口有限 (200K tokens)。当对话变长时,必须智能压缩以保持工作效率。
一、核心问题
对话开始: [System(5K)] [User(1K)] [Assistant(2K)] = 8K tokens ✅ 轻松
50 轮后: [System(5K)] [50轮对话(180K)] [新消息(15K)] = 200K tokens ❌ 溢出!
|
解决方案: 自动检测 → 压缩 → 继续工作
二、压缩系统架构
services/compact/ ├── autoCompact.ts — 自动压缩触发逻辑 ├── compact.ts — 压缩核心算法 (1706 行) ├── microCompact.ts — 微压缩 (单条消息级别) ├── apiMicrocompact.ts — API 侧微压缩 ├── grouping.ts — 消息分组策略 ├── prompt.ts — 压缩用的 prompt 模板 ├── postCompactCleanup.ts — 压缩后清理 ├── sessionMemoryCompact.ts — 会话记忆压缩 ├── compactWarningHook.ts — 压缩警告 hook └── compactWarningState.ts — 警告状态管理
|
三、阈值计算
const AUTOCOMPACT_BUFFER_TOKENS = 13_000; const WARNING_THRESHOLD_BUFFER_TOKENS = 20_000; const MAX_OUTPUT_TOKENS_FOR_SUMMARY = 20_000;
export function getEffectiveContextWindowSize(model: string): number { const contextWindow = getContextWindowForModel(model); const reserved = Math.min(getMaxOutputTokensForModel(model), MAX_OUTPUT_TOKENS_FOR_SUMMARY); return contextWindow - reserved; }
export function getAutoCompactThreshold(model: string): number { return getEffectiveContextWindowSize(model) - AUTOCOMPACT_BUFFER_TOKENS; }
|
Token 警告阈梯
0 ────────────────────────── 100% ├─────────────────────────┤ │ │ │ 正常工作区域 │ │ │ ├─────────────────────────┤ ← 自动压缩阈值 (~83.5%) │ 自动压缩触发区域 │ ├─────────────────────────┤ ← 警告阈值 (~90%) │ ⚠️ 警告:空间即将耗尽 │ ├─────────────────────────┤ ← 错误阈值 (~90%) │ ❌ 错误:必须立即压缩 │ └─────────────────────────┘ ← 200K (上下文窗口)
|
四、压缩算法 (compact.ts)
4.1 核心流程
export async function compactConversation( messages: Message[], context: CompactContext, ): Promise<CompactionResult> {
const activeMessages = getMessagesAfterCompactBoundary(messages);
const summaryPrompt = buildCompactPrompt(activeMessages, context);
const summary = await runForkedAgent(summaryPrompt, { model: context.model, maxTokens: COMPACT_MAX_OUTPUT_TOKENS, });
const compactedMessages = buildPostCompactMessages( messages, summary, activeMessages, );
await runPostCompactCleanup(compactedMessages, context);
return { compactedMessages, removedCount: activeMessages.length, savedTokens: estimateTokensSaved(activeMessages, summary), }; }
|
4.2 压缩 Prompt 策略
function buildCompactPrompt(messages: Message[]): string { return ` 请将以下对话历史压缩为一个简洁但完整的摘要。
要求: 1. 保留所有重要的技术决策和代码变更 2. 保留文件路径和关键代码片段 3. 保留当前工作状态和未完成的任务 4. 保留用户的偏好和约束条件 5. 丢弃冗余的工具输出和中间步骤 6. 如果有被拒绝的操作,保留拒绝原因
当前对话包含 ${messages.length} 条消息, 请将其压缩为一个清晰的摘要。 `; }
|
4.3 压缩后消息结构
压缩前: [System] [User1] [Asst1] [Tool1] [User2] [Asst2] ... [User50] [Asst50]
压缩后: [System] [CompactBoundary: "以下是之前对话的摘要: ..."] [User50] [Asst50] └─ 包含前 49 轮的精华摘要
CompactBoundary 消息格式: { type: 'system', subtype: 'compact_boundary', content: "## 对话摘要\n\n### 已完成的工作\n- 修复了 X bug...\n### 当前状态\n- 正在处理 Y..." }
|
五、微压缩 (Micro-Compact)
大型工具结果的就地压缩,不触发完整的对话压缩:
export function microCompactToolResult( toolResult: string, maxTokens: number, ): string { if (tokenCount(toolResult) <= maxTokens) { return toolResult; }
return truncateWithContext(toolResult, maxTokens); }
|
六、上下文分析
export function analyzeContext(messages: Message[]): ContextAnalysis { return { totalTokens: tokenCountWithEstimation(messages), messageCount: messages.length, toolCallCount: countToolCalls(messages),
breakdown: { systemPrompt: systemPromptTokens, userMessages: userTokens, assistantMessages: assistantTokens, toolResults: toolResultTokens, attachments: attachmentTokens, },
largestMessages: findLargestMessages(messages, 10),
recommendation: totalTokens > threshold ? 'compact_now' : 'ok', }; }
|
七、Snip 机制 (HISTORY_SNIP feature)
一种更激进的压缩方式——直接裁剪历史:
function snipHistory(messages: Message[], keepRecent: number): Message[] { const snipPoint = messages.length - keepRecent; const tombstone = createTombstoneMessage(`Snipped ${snipPoint} messages`); return [tombstone, ...messages.slice(snipPoint)]; }
|
八、自动压缩的断路器
const MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES = 3;
|
九、Recompaction (再压缩)
type RecompactionInfo = { isRecompaction: boolean; previousSummary: string; turnsSinceLastCompaction: number; };
|
十、Session Memory Compact
export async function trySessionMemoryCompaction( messages: Message[], summary: string, ): Promise<void> { }
|
十一、信息论视角下的上下文压缩
11.1 压缩作为信息最大化问题
从信息论的角度看,上下文压缩是在约束条件下最大化信息的保留:
给定:
- 原始对话历史 ( H = {m_1, m_2, …, m_n} )
- Token 预算 ( B )
- 压缩函数 ( C(H, B) \rightarrow H’ )
目标:最小化信息损失 ( I(H; H’) ) 同时满足 ( \text{tokens}(H’) \leq B )
其中 ( I(X;Y) ) 是互信息,衡量 ( H’ ) 保留了 ( H ) 中多少有用信息。
Claude Code 的压缩策略不是通过数学优化求解,而是利用 LLM 本身的语义理解能力来生成保留关键信息的摘要——这可以被视为一种语义压缩(Semantic Compression)。
11.2 语义压缩 vs 符号压缩
| 维度 |
符号压缩 (Gzip) |
语义压缩 (Claude Code Compact) |
| 基础单元 |
字节序列 |
语义概念 |
| 压缩率上限 |
~4:1(文本) |
可达 10:1~50:1 |
| 有损/无损 |
无损 |
有损(保留关键信息,丢弃细节) |
| 解压方式 |
算法逆运算 |
LLM 推理重建 |
| 适用场景 |
原始文本存储 |
对话历史管理 |
| 错误容忍 |
零容忍 |
可容忍”近似正确” |
这种将 LLM 本身用作压缩/解压缩引擎的思想在 Delétang et al. (2024, Language Modeling Is Compression) 中有深入的论述:语言模型本质上就是强大的压缩器。
十二、上下文窗口管理的学术演进
12.1 从固定窗口到动态管理
第一代 (GPT-2, 2019): 1,024 tokens — 无管理策略 第二代 (GPT-3, 2020): 2,048 tokens — 简单截断最早消息 第三代 (GPT-4, 2023): 8K/32K tokens — 滑动窗口 第四代 (Claude 3, 2024): 200K tokens — 需要智能压缩 第五代 (未来): 1M+ tokens — 仍需要压缩策略
|
关键洞察来自 Liu et al. (2024, Lost in the Middle: How Language Models Use Long Contexts):即使上下文窗口足够大,LLM 对”中间位置”信息的关注度显著低于开头和结尾。这意味着简单地让对话历史无限增长,即使不超出窗口,也会导致信息利用率下降。
12.2 Claude Code 的应对策略
策略 1: 保留首尾,压缩中间 └─ System Prompt + 最近 3 个 turn 保持完整 └─ 中间的老 turn 压缩为摘要
策略 2: 分层压缩 └─ 不重要的 turn → Micro-Compact (轻度压缩) └─ 中等重要的 turn → Snip (中度裁剪) └─ 历史久远的 turn → Full Compact (高度压缩)
策略 3: 保留工具输出中的关键信息 └─ 文件编辑的 diff → 保留 └─ 长文件读取的完整内容 → 压缩为文件摘要 └─ 命令执行的完整输出 → 只保留退出码 + 最后 500 字符
|
十三、Token 计数的精确估算
13.1 为什么是估算而非精确?
Token 计数在 API 调用之前无法精确获得(不同模型的 tokenizer 不同,且 CC 没有运行完整的 tokenizer)。Claude Code 使用经验公式:
function estimateTokens(text: string): number { const englishChars = (text.match(/[a-zA-Z0-9\s]/g) || []).length; const codeChars = (text.match(/[{}\[\]()=><;:]/g) || []).length; const cjkChars = (text.match(/[一-鿿]/g) || []).length; const otherChars = text.length - englishChars - codeChars - cjkChars;
return Math.ceil( englishChars / 4 + codeChars / 3 + cjkChars / 1.5 + otherChars / 3 ); }
|
13.2 估算的误差范围
| 内容类型 |
实际 Token |
估算 Token |
误差 |
| 英语散文 |
~250 tokens/KB |
~250 tokens/KB |
<5% |
| TypeScript 代码 |
~200 tokens/KB |
~170 tokens/KB |
~15% |
| 中文文本 |
~600 tokens/KB |
~667 tokens/KB |
~11% |
| JSON/结构化 |
~150 tokens/KB |
~333 tokens/KB |
~122% ⚠️ |
JSON 的误差最大(因为花括号和引号多),这也是为什么 CC 在估算基础上额外加了 20% 的安全边际,确保不会误判”窗口还够”。
十四、压缩质量的评估
14.1 质量维度
interface CompactQualityMetrics { informationRetention: number;
taskCompletionImpact: number;
tokenSavings: number;
compressionEfficiency: number; }
|
14.2 实测数据
基于 Claude Code 的典型 programming session(50 turns):
| 压缩类型 |
Token 节省 |
信息保留 |
压缩本身的 Token 消耗 |
| Micro-Compact |
15-25% |
~95% |
~200 tokens |
| Snip |
30-50% |
~85% |
~100 tokens |
| Full Compact |
60-80% |
~70% |
~500-1000 tokens |
| Session Memory |
85-95% |
~50% |
~300 tokens |
关键发现:Full Compact 虽然省 token 最多,但信息损失也最大。通常只在接近上下文窗口上限时才触发。Micro-Compact 是日常的主力压缩方式——成本低、效果好。
十五、与其他上下文管理策略的对比
| 策略 |
代表系统 |
优点 |
缺点 |
CC 是否采用 |
| 滑动窗口 |
ChatGPT |
简单,无额外成本 |
丢弃早期重要信息 |
否 |
| 向量检索 (RAG) |
LangChain, Copilot |
只检索相关内容 |
遗漏隐含上下文 |
CLAUDE.md 是简化版 |
| 层次化摘要 |
MemGPT, Memobase |
保留长期记忆 |
摘要可能丢失细节 |
是(Session Memory) |
| LLM 摘要压缩 |
Claude Code |
语义理解,压缩率高 |
每次压缩消耗 token |
是(核心策略) |
| 扩展窗口 |
Gemini 1.5 (1M) |
最低压缩需求 |
延迟和成本增加 |
配合压缩使用 |
Claude Code 采用混合策略:滑动窗口 + LLM 摘要 + Session Memory,根据不同场景选择最合适的策略。
十六、工程最佳实践
16.1 CLAUDE.md 在上下文管理中的角色
CLAUDE.md 可以被看作一种预加载的上下文缓存:
- 不在 System Prompt 中重复加载已知信息
- 避免 Agent 在每次任务中重新探索项目结构
- 关键指令(编码规范、架构决策)始终可用,节省探索 token
16.2 用户侧的优化建议
| 技巧 |
效果 |
方法 |
| 编写好的 CLAUDE.md |
减少探索性工具调用 30-50% |
描述项目结构、代码规范、架构 |
| 适度使用 Memory 文件 |
跨 session 保留关键信息 |
claude memory add "..." |
| 手动触发压缩 |
节省 token / 成本 |
/compact 命令 |
| 拆分长任务 |
每个 sub-task 有干净的上下文 |
大任务拆为多个小对话 |
十七、压缩策略的决策树
每次 Turn 结束后: │ ├─ estimatedTokens > AUTO_COMPACT_THRESHOLD ? │ ├─ YES → 触发自动压缩 │ │ ├─ 尝试 Micro-Compact (轻量,保留率高) │ │ ├─ 如果还是超出 → Full Compact (重量,压缩率高) │ │ └─ 如果仍超出 → Snip 最早的非关键 turn │ │ │ └─ NO → 检查 token 增长趋势 │ ├─ 过去 3 个 turn 增长了 > 10% → 预压缩 │ └─ 稳定增长 → 推迟压缩 │ └─ 用户手动触发 `/compact` → 强制全量压缩
|
17.1 压缩触发频率的统计
基于典型 50-turn 编程 session:
| Turn 区间 |
压缩触发次数 |
压缩类型 |
| Turns 1-10 |
0 |
无需压缩 |
| Turns 11-20 |
1-2 |
Micro-Compact |
| Turns 21-35 |
2-3 |
Micro-Compact + 偶尔 Full |
| Turns 36-50 |
3-5 |
频繁 Full Compact + Snip |
17.2 压缩的 Token 经济学
一次 Full Compact 的投入产出分析:
投入: - Compact prompt: ~500 tokens - 等待 LLM 响应: ~2-5s 延迟 - LLM 输出(摘要): ~300-800 tokens
产出: - 压缩的消息历史: 节省 5000-20000 tokens - 为后续 5-10 个 turn 争取了空间
净收益: - Token: 5000 - (500 + 800) = 3700 tokens 净节省 - 延迟: 2-5s 一次性的等待,避免了后续每次 Turn 处理 20000+ token 上下文的延迟
|
压缩是一个投资:投入一次 LLM 调用来总结历史,换取后续多个 Turn 的低延迟和低 token 消耗。
十八、压缩中的信息损失管理
18.1 必须保留的关键信息
不是所有对话都应该被平等地压缩。Claude Code 在压缩时特别保护以下类型的信息:
优先级 1 (绝对保留): ├─ 用户的核心任务描述 ├─ 已做出的架构决策 └─ 待完成的任务列表
优先级 2 (尽量保留): ├─ 已发现的关键文件路径 ├─ 重要的错误信息 └─ 用户明确的偏好设置
优先级 3 (可摘要): ├─ 工具调用的具体输入 ├─ 已读取文件的内容(保留引用) └─ 中间的推理过程
优先级 4 (可丢弃): ├─ 已被后续修改覆盖的编辑 ├─ 探索过程中走错的分支 └─ 重复的工具调用记录
|
18.2 压缩质量的前端验证
压缩完成后,Claude Code 在下一个 Turn 中隐式验证压缩质量:
Turn N: 压缩完成 Turn N+1: └─ 用户的问题是否仍然可以被 LLM 正确理解? └─ LLM 是否需要重新询问已被压缩掉的细节? ├─ 是 → 压缩信息损失过大,调整阈值 └─ 否 → 压缩质量良好
Turn N+2 ~ N+5: └─ 任务完成率是否受影响? └─ Agent 是否需要更多工具调用来重新探索?
|
18.3 与 Anthropic Prompt Caching 的协同
压缩和 Prompt Caching 是互补的。压缩后的摘要比原始对话更短、结构更紧凑,因此:
- 摘要更容易被完整缓存(cache write tokens 更少)
- 缓存的摘要可以跨 Turn 复用(减少 cache miss)
- 摘要的稳定性高于动态增长的对话(缓存命中率更高)
扩展阅读
- Liu et al. (2024). “Lost in the Middle: How Language Models Use Long Contexts.” TACL 2024. arXiv:2307.03172 — LLM 长上下文注意力分布
- Delétang et al. (2024). “Language Modeling Is Compression.” ICLR 2024. arXiv:2309.10668 — 语言模型即压缩器
- Anthropic (2024). “Prompt Caching Guide.” docs.anthropic.com
- Lewis et al. (2020). “RAG: Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks.” NeurIPS 2020. arXiv:2005.11401
- Jiang et al. (2023). “LLMLingua: Compressing Prompts for Accelerated Inference.” arXiv:2310.05736 — Prompt 压缩的学术方法
- Packer et al. (2024). “MemGPT: Towards LLMs as Operating Systems.” arXiv:2310.08560 — 层次化记忆管理
涉及源文件
services/compact/microCompact.ts
services/compact/prompt.ts
services/compact/sessionMemoryCompact.ts
services/compact/snipCompact.ts