Files
cs-note/claude-code-best/docs/context/token-budget.md
T
2026-06-08 23:26:18 +08:00

196 lines
8.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: "Token 预算管理 - 上下文窗口动态计算"
description: "从源码角度揭示 Claude Code token 预算管理:200K 上下文窗口的动态计算、截断机制、缓存优化和自动压缩的完整链路。"
keywords: ["Token 预算", "上下文窗口", "token 计算", "截断机制", "缓存优化"]
---
{/* 本章目标:从源码角度揭示 token 预算的动态计算、截断机制、缓存优化和自动压缩的完整链路 */}
## 上下文窗口:200K 不是全部
Claude Code 的默认上下文窗口为 200K tokens(`MODEL_CONTEXT_WINDOW_DEFAULT = 200_000`),但实际可用于对话的空间远小于此:
```
上下文窗口(200K)
├── 系统提示词(~15-25K,缓存后成本低)
├── 工具定义(~10-20K,含 MCP 工具)
├── 用户上下文(CLAUDE.md、git status 等)
├── 输出预留(maxOutputTokens)
│ ├── 默认上限:64K
│ ├── 实际默认:8K(slot-reservation 优化)
│ └── 触顶自动升级:一次 64K 重试
└── 剩余:对话历史空间(随对话增长)
```
`getContextWindowForModel()`(`src/utils/context.ts:51`)按 5 级优先级解析窗口大小:
1. `CLAUDE_CODE_MAX_CONTEXT_TOKENS` 环境变量覆盖
2. 模型名含 `[1m]` 后缀 → 1M tokens
3. `getModelCapability(model).max_input_tokens`
4. 1M beta header + 支持的模型(claude-sonnet-4, opus-4-6)
5. 兜底:200K
**有效上下文** = 窗口大小 - min(maxOutputTokens, 20K),因为压缩摘要需要预留输出空间。
## Token 计数:近似 vs 精确
系统使用两级 token 计数策略:
### 近似估算(毫秒级)
```typescript
// src/services/tokenEstimation.ts
function roughTokenCountEstimation(content: string, bytesPerToken = 4): number {
return Math.round(content.length / bytesPerToken)
}
```
对不同内容类型有特殊处理:
- **JSON/JSONL**:`bytesPerToken = 2`(密集的 `{`, `:`, `,` 符号,每个仅 1-2 token)
- **图片/文档**:固定 2000 tokens(基于 2000×2000px 上限的保守估计)
- **thinking block**:按实际文本长度 / 4
- **tool_use**:序列化 `name + JSON.stringify(input)` 后 / 4
### 精确计数(API 调用)
使用 Anthropic 的 `beta.messages.countTokens` 端点。在不同 provider 上有不同路径:
| Provider | 方法 |
|----------|------|
| Anthropic 直连 | `anthropic.beta.messages.countTokens()` |
| AWS Bedrock | `@aws-sdk/client-bedrock-runtime` 的 `CountTokensCommand` |
| Google Vertex | Anthropic SDK + beta 过滤 |
| 兜底(Bedrock 不支持) | 用 Haiku 发送 `max_tokens=1` 的请求,读取 `usage.input_tokens` |
精确计数在关键决策点使用(压缩前后对比、warning 判断),近似估算在热路径使用(每轮循环的 shouldAutoCompact 检查)。
### 3P Provider 的 Token 计数差异
不同 Provider 的精确 token 计数实现方式不同,部分 provider 甚至不支持精确计数:
| Provider | 计数方式 | 注意事项 |
|----------|---------|---------|
| **Anthropic 直连** | `anthropic.beta.messages.countTokens()` | 标准 API,最准确 |
| **AWS Bedrock** | `CountTokensCommand` | 需要动态加载 279KB AWS SDK |
| **Google Vertex** | Anthropic SDK + beta 过滤 | 需要特定 beta headers |
| **OpenAI 兼容层** | 无精确计数 | **退回到近似估算** |
| **Gemini 兼容层** | 无精确计数 | **退回到近似估算** |
| **Bedrock 不支持时** | 用 Haiku 发送 `max_tokens=1` 请求 | 读取 `usage.input_tokens` |
OpenAI 和 Gemini 兼容层**不支持精确 token 计数**,系统会退回到近似估算。这会影响:
- **自动压缩触发时机**:可能略有偏差
- **压缩前后 token 对比**:仅为估算值,非精确
- **Warning/Error 阈值判断**:基于估算而非精确计数
```typescript
// src/services/tokenEstimation.ts - 近似估算函数
function roughTokenCountEstimation(content: string, bytesPerToken = 4): number {
return Math.round(content.length / bytesPerToken)
}
```
源码路径:`src/services/tokenEstimation.ts`
## 自动压缩的触发阈值
```
src/services/compact/autoCompact.ts — 核心阈值
```
| 常量 | 值 | 含义 |
|------|----|------|
| `AUTOCOMPACT_BUFFER_TOKENS` | 13,000 | 窗口减去此值 = 自动压缩触发点 |
| `WARNING_THRESHOLD_BUFFER_TOKENS` | 20,000 | 在触发点 + 20K 处显示警告 |
| `ERROR_THRESHOLD_BUFFER_TOKENS` | 20,000 | 在触发点 + 20K 处显示错误 |
| `MANUAL_COMPACT_BUFFER_TOKENS` | 3,000 | 手动 /compact 的阻塞上限 |
| `MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES` | 3 | 连续失败 3 次后停止尝试 |
以 200K 窗口为例:
- **~167K**:warning 闪烁,用户看到建议压缩的提示
- **~180K**:自动压缩触发(200K - 20K 输出预留 = 180K 有效,再 - 13K buffer)
- **~197K**:达到 blocking limit,新消息被阻止
`shouldAutoCompact()` 有多个逃逸条件:
- `compact` / `session_memory` 来源的查询永不触发(防递归死锁)
- `DISABLE_COMPACT` / `DISABLE_AUTO_COMPACT` 环境变量
- 用户配置 `autoCompactEnabled = false`
- Context Collapse 模式激活时抑制(collapse 自己管理上下文)
- Reactive Compact 实验模式下抑制主动压缩
- 超过连续失败上限(circuit breaker)
## Micro-Compact:工具结果的渐进式压缩
在触发全量压缩之前,系统先尝试 **micro-compact**——只压缩旧的工具调用结果:
```
可压缩工具列表(COMPACTABLE_TOOLS):
FileRead, Bash, Grep, Glob, WebSearch, WebFetch, FileEdit, FileWrite
```
策略基于时间:
- 超过一定时间(由 `timeBasedMCConfig` 控制)的工具结果被替换为简短占位符
- 图片/文档结果替换为 `[image]` / `[document]` 文本
- 每次替换释放 tokens,可能推迟全量压缩
工具本身也有 `maxResultSizeChars`(通常 100K)硬限制,超长结果在写入消息前就被截断。
## 全量压缩的完整流程
```
autoCompactIfNeeded() / compactConversation()
↓
1. 执行 PreCompact hooks(外部可注入自定义指令)
↓
2. 尝试 Session Memory 压缩(更轻量,优先尝试)
↓
3. Session Memory 失败 → 全量压缩
a. 图片/文档从消息中剥离(替换为 [image]/[document])
b. skill_discovery/skill_listing 附件剥离(压缩后会重新注入)
c. 通过 forked agent 发送摘要请求(复用主线程的 prompt cache)
d. 如果摘要请求本身触发 prompt-too-long → truncateHeadForPTLRetry()
从最老的 API 轮次开始删除,重试最多 3 次
↓
4. 压缩成功后重建上下文:
- compactBoundaryMarker(记录压缩类型、前 token 数等)
- 摘要消息(不可见的 user 消息)
- 最近 5 个文件的重新读取(POST_COMPACT_TOKEN_BUDGET = 50K)
- plan 文件附件(如果有)
- plan mode 指令(如果在计划模式中)
- 已调用的 skill 内容(每 skill ≤5K,总计 ≤25K)
- deferred tools / agent listing / MCP 指令的增量重新注入
- SessionStart hooks 重新执行
- PostCompact hooks 执行
↓
5. 更新缓存基线,防止被误判为 cache break
```
### Prompt Cache Sharing
压缩 API 调用是整个会话中最昂贵的操作之一。系统通过 `runForkedAgent` 复用主线程的缓存前缀(system prompt + tools + context messages),将缓存命中率从 2% 提升到接近 100%。这个优化单独节省了舰队级约 0.76% 的 `cache_creation` tokens。
## 输出 Token 的 Slot 优化
一个经常被忽视的优化:**maxOutputTokens 的动态调整**。
```typescript
// src/services/api/claude.ts — getMaxOutputTokensForModel()
const defaultTokens = isMaxTokensCapEnabled()
? Math.min(maxOutputTokens.default, 8_000) // 默认降到 8K
: maxOutputTokens.default // 原始默认 32K/64K
```
为什么?因为 API 的 slot 机制按 `max_tokens` 预留推理容量。BQ p99 输出仅 4,911 tokens,32K 默认值浪费了 8-16 倍的 slot 容量。降到 8K 后,不到 1% 的请求被截断——这些请求会自动获得一次 64K 的 clean retry。
这个优化对 token 预算的影响是间接的:更多的 slot 容量意味着更少的排队延迟,间接减少了超时和重试。
## Partial Compact:选择性地压缩
除了全量压缩,用户还可以在消息历史中选择某个位置,只压缩该位置之前或之后的内容:
- **`up_to` 方向**:压缩选中消息之前的内容,保留最近的对话
- **`from` 方向**:压缩选中消息之后的内容,保留早期的对话
`from` 方向保留 prompt cache(前缀不变),`up_to` 方向则破坏 cache(摘要插在保留内容之前)。
两种方向的 PTL(prompt-too-long)重试策略相同:从最老的 API 轮次开始删除,确保至少保留一组消息供摘要。