Files

180 lines
7.4 KiB
Markdown
Raw Permalink Normal View History

2026-06-08 23:08:57 +08:00
---
2026-06-09 23:15:17 +08:00
tags: [token-budget, 上下文窗口, 自动压缩, 缓存优化, 输出优化]
create time: 2026-06-09 22:15
2026-06-08 23:08:57 +08:00
---
2026-06-09 23:15:17 +08:00
# Token 预算管理 - 上下文窗口动态计算
2026-06-08 23:08:57 +08:00
2026-06-09 23:15:17 +08:00
## 概述
2026-06-08 23:08:57 +08:00
2026-06-09 23:15:17 +08:00
Claude Code 的 200K 上下文窗口并非全部可用于对话——系统提示词、工具定义、输出预留等占据大量空间。本文解析 token 预算的动态计算、近似与精确两级计数策略、自动压缩触发阈值和输出 token 的 slot 优化。
2026-06-08 23:08:57 +08:00
2026-06-09 23:15:17 +08:00
## 正文
### 上下文窗口: 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重试"
剩余
"对话历史空间, 随对话增长"
2026-06-08 23:08:57 +08:00
```
2026-06-09 23:15:17 +08:00
`getContextWindowForModel()`(`src/utils/context.ts:51`)按 5 级优先级解析窗口大小:
2026-06-08 23:08:57 +08:00
1. `CLAUDE_CODE_MAX_CONTEXT_TOKENS` 环境变量覆盖
2026-06-09 23:15:17 +08:00
2. 模型名含 `[1m]` 后缀 -> 1M tokens
2026-06-08 23:08:57 +08:00
3. `getModelCapability(model).max_input_tokens`
4. 1M beta header + 支持的模型(claude-sonnet-4, opus-4-6)
2026-06-09 23:15:17 +08:00
5. 兜底: 200K
2026-06-08 23:08:57 +08:00
**有效上下文** = 窗口大小 - min(maxOutputTokens, 20K),因为压缩摘要需要预留输出空间。
2026-06-09 23:15:17 +08:00
### Token 计数: 近似 vs 精确
2026-06-08 23:08:57 +08:00
2026-06-09 23:15:17 +08:00
系统使用两级 token 计数策略:
2026-06-08 23:08:57 +08:00
2026-06-09 23:15:17 +08:00
#### 近似估算(毫秒级)
2026-06-08 23:08:57 +08:00
```typescript
// src/services/tokenEstimation.ts
function roughTokenCountEstimation(content: string, bytesPerToken = 4): number {
return Math.round(content.length / bytesPerToken)
}
```
2026-06-09 23:15:17 +08:00
对不同内容类型有特殊处理:
- **JSON/JSONL**: `bytesPerToken = 2`(密集的符号,每个仅 1-2 token)
- **图片/文档**: 固定 2000 tokens(基于 2000x2000px 上限的保守估计)
- **thinking block**: 按实际文本长度 / 4
- **tool_use**: 序列化 `name + JSON.stringify(input)` 后 / 4
2026-06-08 23:08:57 +08:00
2026-06-09 23:15:17 +08:00
#### 精确计数(API 调用)
2026-06-08 23:08:57 +08:00
2026-06-09 23:15:17 +08:00
使用 Anthropic 的 `beta.messages.countTokens` 端点:
2026-06-08 23:08:57 +08:00
2026-06-09 23:15:17 +08:00
| Provider | 方法 | 注意事项 |
|----------|------|---------|
2026-06-08 23:08:57 +08:00
| **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` |
2026-06-09 23:15:17 +08:00
> [!warning] 3P Provider 的计数差异
> OpenAI 和 Gemini 兼容层**不支持精确 token 计数**,系统会退回到近似估算。这会影响自动压缩触发时机、压缩前后 token 对比和 Warning/Error 阈值判断。
2026-06-08 23:08:57 +08:00
2026-06-09 23:15:17 +08:00
精确计数在关键决策点使用(压缩前后对比、warning 判断),近似估算在热路径使用(每轮循环的 shouldAutoCompact 检查)。
2026-06-08 23:08:57 +08:00
2026-06-09 23:15:17 +08:00
### 自动压缩的触发阈值
2026-06-08 23:08:57 +08:00
| 常量 | 值 | 含义 |
|------|----|------|
| `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 次后停止尝试 |
2026-06-09 23:15:17 +08:00
以 200K 窗口为例:
- **~167K**: warning 闪烁,用户看到建议压缩的提示
- **~180K**: 自动压缩触发(200K - 20K 输出预留 = 180K 有效,再 - 13K buffer)
- **~197K**: 达到 blocking limit,新消息被阻止
2026-06-08 23:08:57 +08:00
2026-06-09 23:15:17 +08:00
`shouldAutoCompact()` 有多个逃逸条件:
2026-06-08 23:08:57 +08:00
- `compact` / `session_memory` 来源的查询永不触发(防递归死锁)
- `DISABLE_COMPACT` / `DISABLE_AUTO_COMPACT` 环境变量
- 用户配置 `autoCompactEnabled = false`
- Context Collapse 模式激活时抑制(collapse 自己管理上下文)
- Reactive Compact 实验模式下抑制主动压缩
- 超过连续失败上限(circuit breaker)
2026-06-09 23:15:17 +08:00
### Micro-Compact: 工具结果的渐进式压缩
2026-06-08 23:08:57 +08:00
2026-06-09 23:15:17 +08:00
在触发全量压缩之前,系统先尝试 **micro-compact**——只压缩旧的工具调用结果:
2026-06-08 23:08:57 +08:00
```
2026-06-09 23:15:17 +08:00
可压缩工具列表(COMPACTABLE_TOOLS):
2026-06-08 23:08:57 +08:00
FileRead, Bash, Grep, Glob, WebSearch, WebFetch, FileEdit, FileWrite
```
2026-06-09 23:15:17 +08:00
策略基于时间:
2026-06-08 23:08:57 +08:00
- 超过一定时间(由 `timeBasedMCConfig` 控制)的工具结果被替换为简短占位符
- 图片/文档结果替换为 `[image]` / `[document]` 文本
- 每次替换释放 tokens,可能推迟全量压缩
工具本身也有 `maxResultSizeChars`(通常 100K)硬限制,超长结果在写入消息前就被截断。
2026-06-09 23:15:17 +08:00
### 全量压缩的完整流程
```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["更新缓存基线"]
2026-06-08 23:08:57 +08:00
```
2026-06-09 23:15:17 +08:00
**Prompt Cache Sharing**: 压缩 API 调用通过 `runForkedAgent` 复用主线程的缓存前缀,将缓存命中率从 2% 提升到接近 100%,单独节省了舰队级约 0.76% 的 `cache_creation` tokens。
2026-06-08 23:08:57 +08:00
2026-06-09 23:15:17 +08:00
### 输出 Token 的 Slot 优化
2026-06-08 23:08:57 +08:00
2026-06-09 23:15:17 +08:00
一个经常被忽视的优化: **maxOutputTokens 的动态调整**。
2026-06-08 23:08:57 +08:00
```typescript
2026-06-09 23:15:17 +08:00
// src/services/api/claude.ts -- getMaxOutputTokensForModel()
2026-06-08 23:08:57 +08:00
const defaultTokens = isMaxTokensCapEnabled()
? Math.min(maxOutputTokens.default, 8_000) // 默认降到 8K
: maxOutputTokens.default // 原始默认 32K/64K
```
2026-06-09 23:15:17 +08:00
> [!info] 为什么降到 8K?
> 因为 API 的 slot 机制按 `max_tokens` 预留推理容量。BQ p99 输出仅 4,911 tokens,32K 默认值浪费了 8-16 倍的 slot 容量。降到 8K 后,不到 1% 的请求被截断——这些请求会自动获得一次 64K 的 clean retry。
2026-06-08 23:08:57 +08:00
2026-06-09 23:15:17 +08:00
这个优化对 token 预算的影响是间接的: 更多的 slot 容量意味着更少的排队延迟,间接减少了超时和重试。
2026-06-08 23:08:57 +08:00
2026-06-09 23:15:17 +08:00
### Partial Compact: 选择性地压缩
2026-06-08 23:08:57 +08:00
2026-06-09 23:15:17 +08:00
除了全量压缩,用户还可以在消息历史中选择某个位置,只压缩该位置之前或之后的内容:
2026-06-08 23:08:57 +08:00
2026-06-09 23:15:17 +08:00
- **`up_to` 方向**: 压缩选中消息之前的内容,保留最近的对话
- **`from` 方向**: 压缩选中消息之后的内容,保留早期的对话
2026-06-08 23:08:57 +08:00
`from` 方向保留 prompt cache(前缀不变),`up_to` 方向则破坏 cache(摘要插在保留内容之前)。
2026-06-09 23:15:17 +08:00
两种方向的 PTL 重试策略相同: 从最老的 API 轮次开始删除,确保至少保留一组消息供摘要。
## 关联笔记
- [[compaction]] - 上下文压缩三层策略详解
- [[system-prompt]] - System Prompt 缓存分块
- [[project-memory]] - 记忆系统与 Session Memory
- [[../conversation/the-loop]] - Agentic Loop 中的 Token Budget