Files

180 lines
7.4 KiB
Markdown
Raw Permalink 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.
---
tags: [token-budget, 上下文窗口, 自动压缩, 缓存优化, 输出优化]
create time: 2026-06-09 22:15
---
# Token 预算管理 - 上下文窗口动态计算
## 概述
Claude Code 的 200K 上下文窗口并非全部可用于对话——系统提示词、工具定义、输出预留等占据大量空间。本文解析 token 预算的动态计算、近似与精确两级计数策略、自动压缩触发阈值和输出 token 的 slot 优化。
## 正文
### 上下文窗口: 200K 不是全部
Claude Code 的默认上下文窗口为 200K tokens(`MODEL_CONTEXT_WINDOW_DEFAULT = 200_000`),但实际可用于对话的空间远小于此:
```mermaid
mindmap
root(("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(基于 2000x2000px 上限的保守估计)
- **thinking block**: 按实际文本长度 / 4
- **tool_use**: 序列化 `name + JSON.stringify(input)` 后 / 4
#### 精确计数(API 调用)
使用 Anthropic 的 `beta.messages.countTokens` 端点:
| 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` |
> [!warning] 3P Provider 的计数差异
> OpenAI 和 Gemini 兼容层**不支持精确 token 计数**,系统会退回到近似估算。这会影响自动压缩触发时机、压缩前后 token 对比和 Warning/Error 阈值判断。
精确计数在关键决策点使用(压缩前后对比、warning 判断),近似估算在热路径使用(每轮循环的 shouldAutoCompact 检查)。
### 自动压缩的触发阈值
| 常量 | 值 | 含义 |
|------|----|------|
| `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)硬限制,超长结果在写入消息前就被截断。
### 全量压缩的完整流程
```mermaid
flowchart TD
A["autoCompactIfNeeded / compactConversation"] --> B["执行PreCompact hooks"]
B --> C{"Session Memory可用?"}
C -- 是 --> D["SM压缩 不调用API"]
C -- 否 --> E["全量压缩"]
E --> F["剥离图片/文档和skill附件"]
F --> G["通过forked agent发送摘要请求"]
G --> H{"触发prompt-too-long?"}
H -- 是 --> I["truncateHeadForPTLRetry 最多重试3次"]
H -- 否 --> J["压缩成功"]
I --> J
D --> J
J --> K["重建上下文"]
K --> L["compactBoundaryMarker"]
L --> M["摘要消息"]
M --> N["最近5个文件重新读取 50K预算"]
N --> O["skill/MCP/plan重新注入"]
O --> P["SessionStart + PostCompact hooks"]
P --> Q["更新缓存基线"]
```
**Prompt Cache Sharing**: 压缩 API 调用通过 `runForkedAgent` 复用主线程的缓存前缀,将缓存命中率从 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
```
> [!info] 为什么降到 8K?
> 因为 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 重试策略相同: 从最老的 API 轮次开始删除,确保至少保留一组消息供摘要。
## 关联笔记
- [[compaction]] - 上下文压缩三层策略详解
- [[system-prompt]] - System Prompt 缓存分块
- [[project-memory]] - 记忆系统与 Session Memory
- [[../conversation/the-loop]] - Agentic Loop 中的 Token Budget