token-budget.mdx 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. ---
  2. title: "Token 预算管理"
  3. description: "上下文窗口是 AI 的内存条——满了就得压缩"
  4. ---
  5. {/* 本章目标:从源码角度揭示 token 预算的动态计算、截断机制、缓存优化和自动压缩的完整链路 */}
  6. ## 上下文窗口:200K 不是全部
  7. Claude Code 的默认上下文窗口为 200K tokens(`MODEL_CONTEXT_WINDOW_DEFAULT = 200_000`),但实际可用于对话的空间远小于此:
  8. ```
  9. 上下文窗口(200K)
  10. ├── 系统提示词(~15-25K,缓存后成本低)
  11. ├── 工具定义(~10-20K,含 MCP 工具)
  12. ├── 用户上下文(CLAUDE.md、git status 等)
  13. ├── 输出预留(maxOutputTokens)
  14. │ ├── 默认上限:64K
  15. │ ├── 实际默认:8K(slot-reservation 优化)
  16. │ └── 触顶自动升级:一次 64K 重试
  17. └── 剩余:对话历史空间(随对话增长)
  18. ```
  19. `getContextWindowForModel()`(`src/utils/context.ts:51`)按 5 级优先级解析窗口大小:
  20. 1. `CLAUDE_CODE_MAX_CONTEXT_TOKENS` 环境变量覆盖
  21. 2. 模型名含 `[1m]` 后缀 → 1M tokens
  22. 3. `getModelCapability(model).max_input_tokens`
  23. 4. 1M beta header + 支持的模型(claude-sonnet-4, opus-4-6)
  24. 5. 兜底:200K
  25. **有效上下文** = 窗口大小 - min(maxOutputTokens, 20K),因为压缩摘要需要预留输出空间。
  26. ## Token 计数:近似 vs 精确
  27. 系统使用两级 token 计数策略:
  28. ### 近似估算(毫秒级)
  29. ```typescript
  30. // src/services/tokenEstimation.ts
  31. function roughTokenCountEstimation(content: string, bytesPerToken = 4): number {
  32. return Math.round(content.length / bytesPerToken)
  33. }
  34. ```
  35. 对不同内容类型有特殊处理:
  36. - **JSON/JSONL**:`bytesPerToken = 2`(密集的 `{`, `:`, `,` 符号,每个仅 1-2 token)
  37. - **图片/文档**:固定 2000 tokens(基于 2000×2000px 上限的保守估计)
  38. - **thinking block**:按实际文本长度 / 4
  39. - **tool_use**:序列化 `name + JSON.stringify(input)` 后 / 4
  40. ### 精确计数(API 调用)
  41. 使用 Anthropic 的 `beta.messages.countTokens` 端点。在不同 provider 上有不同路径:
  42. | Provider | 方法 |
  43. |----------|------|
  44. | Anthropic 直连 | `anthropic.beta.messages.countTokens()` |
  45. | AWS Bedrock | `@aws-sdk/client-bedrock-runtime` 的 `CountTokensCommand` |
  46. | Google Vertex | Anthropic SDK + beta 过滤 |
  47. | 兜底(Bedrock 不支持) | 用 Haiku 发送 `max_tokens=1` 的请求,读取 `usage.input_tokens` |
  48. 精确计数在关键决策点使用(压缩前后对比、warning 判断),近似估算在热路径使用(每轮循环的 shouldAutoCompact 检查)。
  49. ## 自动压缩的触发阈值
  50. ```
  51. src/services/compact/autoCompact.ts — 核心阈值
  52. ```
  53. | 常量 | 值 | 含义 |
  54. |------|----|------|
  55. | `AUTOCOMPACT_BUFFER_TOKENS` | 13,000 | 窗口减去此值 = 自动压缩触发点 |
  56. | `WARNING_THRESHOLD_BUFFER_TOKENS` | 20,000 | 在触发点 + 20K 处显示警告 |
  57. | `ERROR_THRESHOLD_BUFFER_TOKENS` | 20,000 | 在触发点 + 20K 处显示错误 |
  58. | `MANUAL_COMPACT_BUFFER_TOKENS` | 3,000 | 手动 /compact 的阻塞上限 |
  59. | `MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES` | 3 | 连续失败 3 次后停止尝试 |
  60. 以 200K 窗口为例:
  61. - **~167K**:warning 闪烁,用户看到建议压缩的提示
  62. - **~180K**:自动压缩触发(200K - 20K 输出预留 = 180K 有效,再 - 13K buffer)
  63. - **~197K**:达到 blocking limit,新消息被阻止
  64. `shouldAutoCompact()` 有多个逃逸条件:
  65. - `compact` / `session_memory` 来源的查询永不触发(防递归死锁)
  66. - `DISABLE_COMPACT` / `DISABLE_AUTO_COMPACT` 环境变量
  67. - 用户配置 `autoCompactEnabled = false`
  68. - Context Collapse 模式激活时抑制(collapse 自己管理上下文)
  69. - Reactive Compact 实验模式下抑制主动压缩
  70. - 超过连续失败上限(circuit breaker)
  71. ## Micro-Compact:工具结果的渐进式压缩
  72. 在触发全量压缩之前,系统先尝试 **micro-compact**——只压缩旧的工具调用结果:
  73. ```
  74. 可压缩工具列表(COMPACTABLE_TOOLS):
  75. FileRead, Bash, Grep, Glob, WebSearch, WebFetch, FileEdit, FileWrite
  76. ```
  77. 策略基于时间:
  78. - 超过一定时间(由 `timeBasedMCConfig` 控制)的工具结果被替换为简短占位符
  79. - 图片/文档结果替换为 `[image]` / `[document]` 文本
  80. - 每次替换释放 tokens,可能推迟全量压缩
  81. 工具本身也有 `maxResultSizeChars`(通常 100K)硬限制,超长结果在写入消息前就被截断。
  82. ## 全量压缩的完整流程
  83. ```
  84. autoCompactIfNeeded() / compactConversation()
  85. 1. 执行 PreCompact hooks(外部可注入自定义指令)
  86. 2. 尝试 Session Memory 压缩(更轻量,优先尝试)
  87. 3. Session Memory 失败 → 全量压缩
  88. a. 图片/文档从消息中剥离(替换为 [image]/[document])
  89. b. skill_discovery/skill_listing 附件剥离(压缩后会重新注入)
  90. c. 通过 forked agent 发送摘要请求(复用主线程的 prompt cache)
  91. d. 如果摘要请求本身触发 prompt-too-long → truncateHeadForPTLRetry()
  92. 从最老的 API 轮次开始删除,重试最多 3 次
  93. 4. 压缩成功后重建上下文:
  94. - compactBoundaryMarker(记录压缩类型、前 token 数等)
  95. - 摘要消息(不可见的 user 消息)
  96. - 最近 5 个文件的重新读取(POST_COMPACT_TOKEN_BUDGET = 50K)
  97. - plan 文件附件(如果有)
  98. - plan mode 指令(如果在计划模式中)
  99. - 已调用的 skill 内容(每 skill ≤5K,总计 ≤25K)
  100. - deferred tools / agent listing / MCP 指令的增量重新注入
  101. - SessionStart hooks 重新执行
  102. - PostCompact hooks 执行
  103. 5. 更新缓存基线,防止被误判为 cache break
  104. ```
  105. ### Prompt Cache Sharing
  106. 压缩 API 调用是整个会话中最昂贵的操作之一。系统通过 `runForkedAgent` 复用主线程的缓存前缀(system prompt + tools + context messages),将缓存命中率从 2% 提升到接近 100%。这个优化单独节省了舰队级约 0.76% 的 `cache_creation` tokens。
  107. ## 输出 Token 的 Slot 优化
  108. 一个经常被忽视的优化:**maxOutputTokens 的动态调整**。
  109. ```typescript
  110. // src/services/api/claude.ts — getMaxOutputTokensForModel()
  111. const defaultTokens = isMaxTokensCapEnabled()
  112. ? Math.min(maxOutputTokens.default, 8_000) // 默认降到 8K
  113. : maxOutputTokens.default // 原始默认 32K/64K
  114. ```
  115. 为什么?因为 API 的 slot 机制按 `max_tokens` 预留推理容量。BQ p99 输出仅 4,911 tokens,32K 默认值浪费了 8-16 倍的 slot 容量。降到 8K 后,不到 1% 的请求被截断——这些请求会自动获得一次 64K 的 clean retry。
  116. 这个优化对 token 预算的影响是间接的:更多的 slot 容量意味着更少的排队延迟,间接减少了超时和重试。
  117. ## Partial Compact:选择性地压缩
  118. 除了全量压缩,用户还可以在消息历史中选择某个位置,只压缩该位置之前或之后的内容:
  119. - **`up_to` 方向**:压缩选中消息之前的内容,保留最近的对话
  120. - **`from` 方向**:压缩选中消息之后的内容,保留早期的对话
  121. `from` 方向保留 prompt cache(前缀不变),`up_to` 方向则破坏 cache(摘要插在保留内容之前)。
  122. 两种方向的 PTL(prompt-too-long)重试策略相同:从最老的 API 轮次开始删除,确保至少保留一组消息供摘要。