为什么需要上下文压缩?Token 窗口的物理限制
在开始深入 Claude Code 的压缩引擎源码之前,我们需要先理解一个根本性的物理约束:token 窗口限制。这不是工程师偷懒留下的技术债,而是当前大语言模型架构的基础局限。
Token 是什么,窗口有多大?
大语言模型(LLM)不直接处理文字,而是处理被切分后的"token"。一个英文单词通常是 1-2 个 token,一个中文字通常是 1-2 个 token,代码片段中的特殊符号也会各自占用 token。
Claude 3.5/3.7 系列支持最大 200,000 tokens 的上下文窗口。这听起来很大,但换算成实际内容后:
| 内容类型 | 大致可容纳量 |
|---|---|
| 普通英文文章 | 约 150,000 个英文词 |
| 中文文本 | 约 100,000 个汉字 |
| Python 代码 | 约 500,000 字节(~500KB) |
| 对话轮次 | 约 200-300 轮(视每轮长度而定) |
对于日常的简短任务,200K token 绰绰有余。但 Claude Code 面对的是长时间的开发任务:反复读取大型代码文件、执行 bash 命令并获取长篇输出、多轮工具调用的来回、用户反复修正需求……这些都会快速消耗 token 配额。
Token 消耗的增长曲线
一次典型的 Claude Code 会话中,token 消耗呈现出指数加速的特征,而非线性增长。
Token 使用量
200K │ ████ ← 触顶(被迫截断)
│ █████
│ ██████
150K │ █████
│ ██████ ← 自动压缩触发点(~180K)
│ ██████
100K │ ██████
│ ██████
50K │████
│
0 └─────────────────────────────────────→ 对话轮次
0 20 40 60 80 100 120
为什么是指数增长而非线性?
关键在于 API 的工作方式。每次向 Claude 发送请求时,整个对话历史都必须作为上下文传入。这意味着:
- 第 1 轮:发送 100 tokens
- 第 2 轮:发送 100 + 200 = 300 tokens(包含第1轮的问答)
- 第 10 轮:发送前 9 轮的所有内容 + 本轮问题
第 N 轮的 token 消耗 ≈ 所有历史轮次的 token 总和。这使得消耗曲线在中后期急剧上扬。
加上 Claude Code 特有的工具调用模式,问题更为严重:
[用户] 帮我重构 utils.py
[Claude] 好的,我先读取文件
[工具] FileRead: utils.py → 返回 2000 行代码(≈ 3000 tokens)
[Claude] 分析后,我需要检查所有调用方
[工具] Grep: 搜索所有 import utils → 返回 50 个文件列表(≈ 500 tokens)
[工具] FileRead: 读取每个调用方... × 10 次(≈ 15000 tokens)
[Claude] 开始逐一修改...
[工具] FileEdit × 20 次(每次输入输出各占 token)
一次完整的重构任务,仅工具调用的 tool_result 部分就可能消耗 50,000+ tokens。
不压缩会发生什么?
当 token 使用量接近或超过窗口上限时,有三种糟糕的结果:
1. 静默截断(最危险)
最早期的 LLM 实现会直接截断最旧的对话历史。从 token 计量角度看,这"解决"了问题,但模型丢失了关键上下文:
- 忘记了用户最开始的需求是什么
- 忘记了之前已经修改过哪些文件
- 忘记了用户明确说"不要用这个方案"的偏好
这会导致模型开始重复已经做过的工作,甚至撤销自己之前的修改。
2. API 报错(prompt_too_long)
Anthropic API 有严格的 token 上限检查。超出后会返回:
API Error: prompt_too_long - Your prompt exceeds the maximum token limit
Claude Code 的源码中专门处理这类错误:
// source/src/services/api/errors.ts
export const PROMPT_TOO_LONG_ERROR_MESSAGE = 'prompt_too_long'
export function getPromptTooLongTokenGap(
response: AssistantMessage,
): number | undefined {
// 解析 API 返回的 token 超出量
}
用户看到的现象是会话突然中断,且无法继续对话。
3. 性能严重下降
即使还没到上限,当 token 数量达到 150K+ 时,模型响应时间会显著延长(因为 attention 机制的计算复杂度是 O(n²)),每次 API 调用的费用也会急剧增加。
压缩的核心问题:什么值得保留?
上下文压缩本质上是一个信息筛选问题:在有限的空间里,保留最有价值的信息。
哪些信息是高价值的?
必须保留:
- 用户的原始需求和最新指令
- 已经完成的代码修改(文件名、关键片段)
- 遭遇的错误及解决方案
- 用户明确表达的偏好("不要用 async/await")
- 当前进行中的任务状态
可以摘要化:
- 大量工具调用的中间过程
- 探索性的文件读取结果
- 已经被推翻的方案讨论
可以丢弃:
- 重复的内容
- 无关的闲聊
- 工具调用的原始输出(保留摘要即可)
Claude Code 的压缩策略概览
Claude Code 实现了多层次的压缩策略,而不是简单粗暴地截断:
┌─────────────────────────────────────────────┐
│ 上下文管理层次结构 │
├─────────────────────────────────────────────┤
│ 1. microCompact 轻量级,仅压缩工具结果 │
│ 触发时机:每轮对话,渐进式清理 │
├─────────────────────────────────────────────┤
│ 2. autoCompact 自动触发全量压缩 │
│ 触发时机:token 接近阈值(~180K) │
├─────────────────────────────────────────────┤
│ 3. /compact 命令 用户手动触发 │
│ 触发时机:用户主动执行 │
├─────────────────────────────────────────────┤
│ 4. contextCollapse 折叠(不同于压缩) │
│ 触发时机:特定类型内容的可逆折叠 │
└─────────────────────────────────────────────┘
最核心的设计理念是:用 Claude 自身来压缩 Claude 的上下文(meta-compression)。将整个对话历史发给一个专用的 Claude 实例,让它生成高质量的结构化摘要,然后用这个摘要替换原始的长对话。
这比简单截断要智能得多——模型本身最懂得哪些信息对"继续完成任务"是关键的。
为什么这是一个工程难题?
上下文压缩看似简单,实现起来却充满挑战:
- 压缩时机:太早压缩浪费计算,太晚压缩则来不及(API 已经报错)
- 压缩质量:摘要质量直接影响后续任务的准确性
- 压缩过程中的中断:压缩本身也需要 API 调用,如果用户中断怎么办?
- 工具调用的配对性:压缩不能破坏
tool_use和tool_result的配对关系 - 成本:每次压缩都要额外消耗 API token
接下来的几篇文章将逐层深入 Claude Code 的压缩引擎,看它如何一一应对这些挑战。