Skip to content

Latest commit

 

History

History
253 lines (138 loc) · 16.5 KB

File metadata and controls

253 lines (138 loc) · 16.5 KB

第 3 章:省钱指南——理解 Token 和 Cache

读完这章你能得到什么: 搞懂 Claude Code 的三级缓存为什么能帮你省 90% 的 token 费用,学会 fork 和 spawn 的成本差距(选错方式浪费大量 token),掌握模型选择的时机判断,以及理解五层压缩机制来避免不必要的 API 开销。

最后给你一份可执行的省钱 checklist。


3.1 三级 Prompt Cache 原理

Claude Code 内置了三级缓存系统。缓存命中的 token 价格是全价的 1/10——这不是比喻,是字面意思。你发给 API 的 10 万 token 里如果 9 万命中缓存,你只为 9 万 × 10% + 1 万 × 100% = 1.9 万等效 token 付钱。省了 81%。

Global Cache(全球共享)

Claude Code 的 system prompt 有 7 段静态内容,对所有用户都一样。这 7 段内容组成了 API 请求的前缀,全球所有用户共享同一份缓存。

你完全不用做任何事情就能享受这层缓存。它自动生效。

唯一需要注意的是:不要试图把你的私人配置(比如 CLAUDE.md 的内容)塞进 system prompt 里——这会破坏前缀匹配,导致你的请求无法命中全局缓存。这就是为什么 CLAUDE.md 的内容是作为第一条 user message 注入的,不是放在 system prompt 里。

还有一个内部细节:工具的 schema 定义用了一个叫 toolSchemaCache 的机制来防止缓存失效。Claude Code 的功能开关(feature flag)会动态决定启用哪些工具。如果每次工具列表变化都导致 schema 部分不同,缓存就全部作废了。

toolSchemaCache 把 schema 固定下来,不管功能开关怎么翻转,发给 API 的工具定义部分保持字节相同。

另外,Beta header 用了一种叫 sticky-on latch 的模式。一旦某个 beta feature 被开启过,后续请求都会带上同样的 header,不会因为某些条件变化而去掉。

这也是为了保持请求前缀一致,最大化缓存命中。

Org Cache(组织内共享)

如果你的团队用的是 Team 或 Enterprise 订阅,组织级别的配置(比如组织统一的 system prompt 附加内容)会在组织内所有成员之间共享缓存。

同一个组织里的人越多,这层缓存的价值越大。10 个人的团队,第一个人的请求"预热"了缓存,后面 9 个人都能命中。

Ephemeral Cache(对话级)

这一层直接影响你的日常使用体验。Ephemeral cache 的断点标记放在最后一条消息上,作用是保护前面所有对话历史的缓存。

具体来说:你和 Claude 聊了 20 轮,累积了 5 万 token 的对话历史。你发第 21 条消息时,前面 5 万 token 的部分作为缓存前缀被保护住了。

API 只需要为你的新消息(可能就几百 token)付全价。

但这里有个关键点:你的对话必须保持连贯。如果你在第 21 条消息里突然跳到一个完全不相关的话题,虽然前缀还是一样的(对话历史没变),缓存还是能命中。

真正破坏缓存的是:你开了一个新会话、你手动编辑了之前的消息、或者触发了自动压缩(下文会讲)。

踩坑经历: 以前我习惯用一个长会话做所有事情——写代码、查 bug、问架构问题。后来看了 token 账单才发现,每次话题跳跃虽然不直接破坏缓存前缀,但它让对话越来越长,最终触发自动压缩。

压缩之后对话历史被摘要替换了,前缀变了,缓存全部失效。现在我的做法是:一个主题一个会话,做完就开新的。


3.2 fork vs spawn 的成本差距

这是整章最值钱的知识点。搞懂这个能帮你显著降低 token 费用。

spawn:从零开始

spawn 创建一个全新的子 Agent。它从零开始构建上下文:重新组装 system prompt、重新注入工具定义、没有任何父进程的对话历史。

spawn 重建的是 system prompt + 工具定义(约 1-2 万 token),不会重发父进程的 10 万 token 对话历史。但核心问题是:spawn 的请求前缀和父进程不同,无法复用父进程已经积累的 prompt cache。这些 1-2 万 token 的 system prompt 和工具定义要重新计算一次。

fork:继承一切

fork 是完全不同的机制。它继承父进程的完整上下文——系统提示、工具定义、对话历史,一字不差地复制过来。

关键在于 fork 内部的 buildForkedMessages() 函数的设计。它构建 API 请求时,刻意让前缀部分和父进程的请求在字节级别完全相同。怎么做到的?两个技巧:

第一,所有历史消息中的 tool_use 调用,它们的 tool_result 都被替换成同一个统一的占位符。不管原来的工具返回了什么,fork 后都变成同一段文字。

这保证了中间内容不会因为工具结果的差异而破坏前缀匹配。

第二,只有最后追加的指令部分是不同的——就是你让这个 fork 子 Agent 去做的具体任务。

结果就是:fork 出来的 API 请求,前 N 个 token 和父进程完全一样,prompt cache 命中率极高。你只为最后那几百个 token 的新指令付全价,前面的全部按缓存价算。

算一笔账

假设你的对话已经积累了 10 万 token 的上下文。现在你要执行 3 个独立的子任务,每个任务的指令是 500 token。

用 spawn 的成本:

  • 每个子任务:约 1.5 万 token 全价(system prompt + 工具定义,无法复用父进程缓存)+ 500 token 全价(指令)
  • 总计:3 x 15,500 = 46,500 token 全价
  • 而且子 Agent 拿不到父对话的上下文,可能需要额外的搜索和读取操作来获取信息,进一步增加开销

用 fork 的成本:

  • 每个子任务:10 万 token 缓存价(命中父进程缓存)+ 500 token 全价(新指令)
  • 总计:10 万 x 3 x 0.1 + 500 x 3 = 31,500 等效 token
  • 子 Agent 继承了完整上下文,不需要额外搜索

差距: fork 的等效 token 成本更低,而且省去了子 Agent 重新获取上下文的额外轮次。对话上下文越长,fork 的缓存优势越明显。

什么时候用 fork

  • 简单的独立子任务:查个文件、搜一段代码、改一个配置
  • 并行搜索:同时往 3 个方向搜索,结果汇总回来
  • 子任务需要父对话的上下文:比如"根据我们刚才讨论的架构方案,去检查 X 目录的代码是否符合"

什么时候用 spawn

  • 子任务需要完全干净的上下文:处理一个毫不相关的项目,父对话的上下文反而是噪音
  • 需要特定 Agent 类型:Explore Agent 自动用 Haiku 模型,比 Opus 便宜得多。这种情况下 spawn 虽然没有缓存复用,但模型本身便宜
  • 父对话上下文太长会干扰判断:偶尔会碰到上下文信息太多反而误导子 Agent 的情况

怎么触发

在 Agent 工具调用中,省略 subagent_type 参数 → 触发隐式 fork。 指定 subagent_type → 触发 spawn。

所以默认行为就是 fork,这是省钱的。你只有在明确需要 spawn 的场景时才去指定 subagent_type。

踩坑经历: 我有一次让 Claude 并行检查 5 个模块的代码质量。第一次用的 spawn——写了 subagent_type: "general-purpose"。看了账单,5 个子任务消耗的 token 远超预期,因为每个 spawn 都要重建上下文、而且子 Agent 拿不到父对话里已有的分析结论,又重复搜索了一遍。

后来改成省略 subagent_type(隐式 fork),同样的任务 token 消耗锐减。fork 命中了父进程的缓存,子 Agent 直接继承了上下文,省去了重复搜索。


3.3 模型选择策略

默认模型取决于你的订阅级别:

订阅 默认模型 上下文窗口
Max / Team Premium claude-opus-4-6 (1M) 1,000,000 tokens
Pro / Enterprise / PAYG claude-sonnet-4-6 200,000 tokens

Haiku 在后台自动用于辅助任务:token 计数估算、Explore Agent 的代码搜索、一些内部的轻量判断。你不需要手动选 Haiku,Claude Code 会在合适的时候自己用。

Fast mode 的真相

/fast 命令切换。很多人以为 Fast mode 是切换到了更弱的模型(比如从 Opus 降到 Sonnet)来换速度。不是。

Fast mode 用的还是同一个 Opus 4.6 模型,只是通过 API 的 beta header 启用了快速输出模式。质量不变,推理能力不变,但生成速度明显加快。

代价是可能在极端复杂的推理任务上表现略有不同,但日常开发中你感受不到差异。

/model 切换的时机

任务类型 选哪个模型 为什么
改变量名、格式化、简单修复 Sonnet 这类任务 Sonnet 和 Opus 效果一样,但 Sonnet 便宜 5-8 倍
架构设计、多文件重构、复杂 debug Opus 需要强推理和长上下文能力
代码搜索、文件探索 不用选 Explore Agent 自动走 Haiku

操作方法:输入 /model 回车选模型,或者 /model claude-sonnet-4-6 直接切。切换是即时的,不需要重启会话。

一个实际的省钱技巧: 我在做大型重构前,先用 Opus 做架构分析和方案设计(需要强推理),然后切到 Sonnet 做具体的文件修改(机械劳动不需要强推理)。

一个 2 小时的重构任务,Opus 用了 30 分钟出方案,Sonnet 用了 90 分钟做执行。如果全程用 Opus,成本大约高 3-4 倍。


3.4 五层压缩与成本

长对话会触发多层压缩机制。每一层的触发时机和成本不一样,理解它们能帮你在对的时机主动压缩,而不是被动等系统处理。

第 1 层:Tool Result Budget

每轮对话都会检查。如果某个工具返回的结果太长(比如你让 Claude 读了一个 5000 行的文件),系统会把结果截断到预算范围内。

成本:零。纯粹的文本截断,不消耗额外 API 调用。你唯一的损失是被截掉的信息 Claude 看不到了。

第 2 层:Snip Compact

每轮对话都会检查。把对话历史中超长的内容块裁剪掉。比如之前一轮对话里 Claude 输出了一大段代码,现在已经过了好几轮了,那段代码的详细内容会被裁成一个摘要标记。

成本:零。同样是截断操作,不额外调 API。

第 3 层:Microcompact

每轮对话都会检查。对大型工具结果做轻量压缩——不是简单截断,而是提取关键信息、去掉冗余内容。

成本:极低。处理逻辑很轻量,几乎不影响你的 token 账单。

第 4 层:Context Collapse

每轮对话都会检查。把已经处理完的旧对话段折叠起来。比如你在第 5 轮让 Claude 搜了 10 个文件找 bug,到了第 15 轮这个 bug 早就修完了,那第 5 轮的搜索详情会被折叠成一句话摘要。

成本:低。折叠逻辑本身消耗不大,但你会丢失被折叠部分的细节。如果后面又需要那些细节,Claude 可能需要重新搜索。

第 5 层:Auto Compact

这是唯一一层"贵"的压缩。触发条件是:当前对话的 token 数接近上下文窗口的上限——具体阈值是上下文窗口大小减去 33,000 tokens

Opus 的 1M 上下文窗口,阈值大约在 967,000 tokens。 Sonnet 的 200K 上下文窗口,阈值在 167,000 tokens。

触发时,系统会 fork 出一个子 Agent,让它把整个对话历史读一遍,生成一份结构化摘要,然后用这份摘要替换掉原始的对话历史。

为什么贵? 因为这本身就是一次完整的 API 调用。fork 出去的子 Agent 要读全部对话历史(input token),然后输出摘要(output token)。

如果你的对话历史有 90 万 token,光这一次压缩的 input 就是 90 万 token——虽然 fork 能命中缓存省一些,但摘要输出和处理开销加起来还是很可观。

更重要的是:压缩后你的对话历史变成了一份摘要,原始的缓存前缀被替换掉了。后续请求的前缀和压缩前不一样了,之前积累的 Ephemeral cache 全部失效。

你需要从这份新的摘要开始,重新积累缓存。

/compact 手动触发的时机

与其等 Auto Compact 被动触发,不如主动控制。手动 /compact 的最佳时机:

  • 完成一个阶段性任务后。 比如你花了 20 轮找到并修复了一个 bug,bug 已经修完了,那些搜索过程、排查细节就不需要了。/compact 一下,释放空间给下一个任务。

  • 感觉回复质量下降时。 上下文接近上限的一个明显信号是回复开始变得含糊、重复、或者忘记之前聊过的内容。这时候 /compact,压缩掉不再需要的历史,质量会明显恢复。

  • 在 Auto Compact 触发之前。 自动触发说明上下文已经非常紧了。在那之前主动压缩,你还有余裕选择哪些信息该保留;等到自动触发时,系统会按自己的判断来压缩,你没有控制权。

踩坑经历: 有一次做一个大型重构,对话从早上聊到下午,中间没 compact 过。下午 3 点左右 Claude 突然开始给出前后矛盾的方案——上午讨论决定用方案 A,下午它开始按方案 B 写代码。

我检查了一下 token 用量,发现已经自动压缩过一次了,上午的讨论细节被摘要吞掉了。从那以后我养成了习惯:每完成一个阶段就 /compact。


3.5 省钱 Checklist

按影响从大到小排列,每一条都是可以直接执行的操作:

  • fork 优先于 spawn — 子任务省略 subagent_type 参数,自动走 fork。fork 能复用父进程的 prompt cache 且继承上下文,省去重复搜索的开销。这是影响最大的一条。

  • 简单任务切 Sonnet — 改变量名、格式化、写注释这些机械操作,用 /model claude-sonnet-4-6 切到 Sonnet,省 5-8 倍。做完再切回 Opus。

  • 代码搜索用 Explore Agent — 它自动走 Haiku 模型。不要手动用 Opus 来搜文件和读代码,让 Explore Agent 干这个活儿。

  • 阶段性手动 /compact — 完成一个子任务就 /compact 一次。不要等 Auto Compact 自动触发——那时候已经晚了,压缩开销更大,信息损失更多。

  • CLAUDE.md 控制在 1000 字以内 — CLAUDE.md 的内容每一轮对话都要重新读一遍。1000 字大约是 1500 token,100 轮对话就是 15 万 token 的额外消耗。精简到只留必要的规则和偏好。

  • 分主题开新会话 — 一个 100 轮的长对话比 5 个 20 轮的短对话贵得多。长对话的后期,每一轮都要把前面所有历史发给 API。短对话每次从头开始,单轮成本低。完成一个主题就开新会话。

  • 保持对话连贯 — 在同一个会话内不要频繁跳话题。话题连贯时,Ephemeral cache 命中率高。突然跳到不相关的话题虽然不直接破坏缓存前缀,但会加速对话膨胀,更快触发 Auto Compact。

  • 用 /cost 定期检查 — 输入 /cost 看当前会话的 token 消耗明细。养成每做完一个任务就看一眼的习惯。如果某个任务消耗异常高,回忆一下是不是用了 spawn 而非 fork,或者是不是自动压缩触发了。

  • 关键信息放在最近的消息 — 如果有些信息后面还要用到,在最近的消息里重新提一下。压缩机制优先删除远的内容,最近的消息最后才被动。这样即使触发压缩,关键信息也能保住。

  • 用 TodoWrite 固定关键信息 — TodoWrite 工具写入的内容独立于对话历史存储,压缩时不会被摘要替换。如果有些决策、规范、约定需要全程保持,让 Claude 用 TodoWrite 记下来,比放在对话历史里更保险。

一个综合的省钱案例

场景:你要对一个项目做代码审查,涉及 5 个模块。

费钱的做法: 在一个 Opus 长会话里,逐个模块看代码、提问题、讨论修改方案。到第 3 个模块时对话已经很长了,Auto Compact 触发,前面两个模块的审查细节被压缩掉。

你问"第 1 个模块那个 race condition 怎么处理的来着",Claude 答不上来,你又得重新看一遍。

省钱的做法:

  1. 先用 Sonnet 跑一遍 Explore Agent,把 5 个模块的代码结构、关键函数、依赖关系摸清楚(Haiku 模型,成本极低)
  2. 切到 Opus,对每个模块开一个独立短会话做深度审查(5 个短会话,每个 20 轮以内,不会触发压缩)
  3. 最后开一个汇总会话,把 5 个模块的审查结论整合(新会话,上下文清爽)

总 token 消耗大约是"费钱做法"的 1/4 到 1/3。审查质量反而更高,因为每个会话的上下文都是聚焦的,没有无关信息干扰。