1 同样的任务,成本差好几十倍
两个人,同一家公司,都让 Claude Code 修登录模块——活差不多,模型一样,轮数也差不多。收工时各敲了一下 /cost:一个算下来十几块钱,另一个五十多了。
差在哪?同一个 token,落在哪个计价上。
token 不是一口价,是四个计价,最贵和最便宜差 50 倍。这一篇拆这本账:先看价目表,再看账怎么算清,最后看 Claude Code 怎么把尽量多的 token 送到便宜的计价上——省钱的活,全在模型外面那层代码里。
2 token 有四个计价
先看账单长什么样。每一轮发过去的,不只是你新打的那句话,是整个会话从头到尾的全部内容。服务器还会返回四个 token 用量数据:
usage: {
input_tokens: 35, // 没赶上缓存的一小截
cache_creation_input_tokens: 1977, // 这轮新增的:你的新消息、模型上轮回答、工具结果
cache_read_input_tokens: 41260, // 上一轮发过、这轮原样重发的大头
output_tokens: 804 // 模型这轮吐出来的字
}
这些用量不用自己计算,拿到就累加进这个会话的成本。
缓存:就是服务器把你发过的内容存一份,下一轮整个会话再发过去,旧的部分一字没动、新的放在后面,这样旧的那部分不用重算
四个 token 用量,对应四个计价。以默认的 Sonnet 看,每百万 token:
| 计价项 | 谁走这个价 | 单价 | 相对输入 |
|---|---|---|---|
| 输入 | 不走缓存的部分 | $3 | 1 倍 |
| 输出 | 模型吐出来的字 | $15 | 5 倍 |
| 缓存写入 | 第一次发、顺手存进缓存 | $3.75 | 1.25 倍 |
| 缓存读取 | 原样重发上一轮发过的 | $0.3 | 0.1 倍 |
价格表里最贵的是输出,最便宜的是缓存读取——5 除以 0.1,正好 50 倍。倍数这一列各档模型都基本一样:换模型换的是单价,不是倍数。
同样的内容,落在缓存读取还是落在重新写入,账单差多少?拿一轮典型对话算笔账:
| 这轮发的内容 | token 数 | 缓存都在 | 缓存过期 |
|---|---|---|---|
| 原样重发的部分 | 41 000 | 一折:$0.0123 | 重新写入,1.25 倍:$0.154 |
| 这轮新增 | 2 000 | 写入,1.25 倍:$0.0075 | 并入上面,一起重新写入 |
| 模型输出 | 800 | 五倍:$0.012 | 五倍:$0.012 |
| 合计 | 约 $0.032 | 约 $0.17 |
一模一样的活,差五倍多。原样重发的这部分,通常占一轮请求的 90% 以上,会话越长,这一行占比越大。
3 一折是怎么来的?
缓存读取价凭什么能打一折?
大模型处理输入不是扫一眼就完——每个 token 都要过一遍内部计算,得出中间结果,回答就从这些结果接着生成。这一步吃算力,是输入费的大头。
缓存省的正是这一步。上一轮发过去四万多 token,服务器算完,把中间结果留下。这一轮再发,旧的一段一字没动,新的接在后面。服务器从请求的第一个字逐字对过去,连着对到新内容开始的地方,对上的这一整段不用重算,只按一折收费,这就命中了缓存。
命中了为什么还收钱?
留下的中间结果是块不小的数据,得一路占着存储等下一轮;请求来了,再整块搬回计算芯片。存它、取它都要花钱,只是远比重算便宜——所以打一折,不是免费。写入价反倒贵一点(1.25 倍):算完还多一步,把结果存下来。
规则三条:
- 只认从头连着的一整段:中间隔着的不算。
- 一字不能变:对到某个位置对不上了,从那里往后全部重算。
- 有时限:存的东西会过期。
一轮请求的四笔钱怎么流,拼成一张图:
省钱的方向就一个:让每轮重发的内容,尽量都命中缓存。可命中缓存的这段经不起改动——会话里到处是会变的东西。接下来三节,讲 Claude Code 是怎么让每轮都命中缓存的。
4 怎么让每轮都命中缓存?
标记是什么?
缓存不是服务器白送的:每轮发过去的全部内容里,得明确带一句「请存」,服务器才会把中间结果留下;一句不带,算完就扔,下一轮全部重算。打标记就是带这句「请存」,在发过去的内容里挑一条,附上一行小字段(cache_control)。这行字段告诉服务器:从头到这条为止,中间结果存下来。标记打在哪条上,缓存就保到哪条。
标记不止一处。
系统提示词、工具清单、对话历史——各段各打各的标记。服务器不用区分谁是谁:标记长在哪条内容上,位置就是身份;见到一个,就存一条缓存。
标记在请求里的位置:
请求的第一个字
│ 系统提示词(不变)
├─ 标记1
│ 工具清单(不变)
├─ 标记2
│ 辅助工具说明(随开随关)
│ 对话历史
└─ 标记3
每条缓存的范围,起点是同一个字——请求的第一个字,量到各自的标记为止:
标记1:第一个字 ──────────── 到标记1
标记2:第一个字 ────────────────────── 到标记2
标记3:第一个字 ─────────────────────────── 到标记3(整包)
标记越靠后,缓存命中的越多,但是标记得打在会变的内容前面。
对话的标记:打在最末。
发过的对话不会再改,新的只会接在后面——整段没有会变的,标记一路推到最末,打在这一轮最后一条消息上,能保多长保多长。源码注释钉死了数量:
Exactly one message-level cache_control marker per request. (每个请求有且只有一个消息级缓存标记。)
「消息级」三个字,说的就是对话这一段:段内只此一个。
工具的标记:停在会变的前面。
工具清单里有随开随关的东西。比如一个辅助工具:源码注释专门交代了它的位置——说明排在带标记的工具清单后面。开关一变,变的只是标记后面那一小截,前面已经缓存的工具定义一点不碰。
分身的标记挪一位。
把主对话复制一份、派出去单独干活的分身,请求里最后那条消息是它自己领的任务,和主对话不共享。标记挪到倒数第二条——那是它和主对话还一字不差的最后一条。
5 缓存能放多久?
缓存分两档:
- 默认 5 分钟;
- 内部用户或者订阅用户且没超套餐量的用户,用 1 小时。
选哪档由 Claude Code 定:打标记时在标记里写上时长,不写就是 5 分钟;服务器不替你选,照着写好的时长存,到期就扔。
5 分钟的意思是:两轮请求之间断档超过 5 分钟,缓存才过期。接着聊,两轮间隔一般也就几十秒,一直续得上。真正的坑是断档:去开了个会回来,缓存早没了,下一轮这几万 token 全部重新写入。
有一处设计值得单独看:能不能用 1 小时,会话开始查一次,之后整个会话不再查。
为什么锁死?
能不能用 1 小时,每次请求都写在请求头里;可请求头变一个字,缓存就整个失效,而订阅用量一超,这项就变——所以会话开始查一次就锁死,之后哪怕变了,请求头也照旧写。
6 什么都没改,折扣怎么也丢了?
上一篇讲压缩时说过:改写已经发出去的历史,折扣当场失效。那是自己动手改的。可有时候你什么都没动,折扣照样丢。除了缓存过期,还有一种更隐蔽。
怎么知道「折扣丢了」?两道手段,一前一后:
- 事前记指纹:发请求前,把这次会影响命中的各段内容——系统提示词、每只工具的定义、之前的对话——各算一串指纹存下来,最多盯 10 个来源。哪段变了,指纹就变。
- 事后看账单:每轮盯着账单里的缓存读取数,比上一轮掉了 5% 以上、还实打实少了 2000 token,就报一次「缓存失效」,把存着的指纹翻出来,看是哪段变了。
跟踪下来发现:有一类失效,内容一个字没动,是服务器那边自己丢的——请求被路由到了另一台机器,或者缓存被挤掉了。「事前指纹 + 事后对账」正好派上用场:这类问题得先发现,改进才有方向。
7 输出为什么限在 8000 token?
另外一个要考虑的就是输出。每次调用大模型,都得告诉服务器这次最多允许它输出多少 token;服务器照这个数提前占地方——报 6.4 万就按 6.4 万占,模型实际只吐 800,多占的照样空着,服务器资源就被浪费了。
后台实测:99% 的请求吐不到 4911 token。所以 Claude Code 默认只报 8000;撞了上限的(不到 1%)再拿 6.4 万从头重跑一遍,平时少占,偶尔不够再加。真有任务需要长输出,环境变量 CLAUDE_CODE_MAX_OUTPUT_TOKENS 可以改。
8 具体用了多少,怎么看?
用量按会话记:每轮的四个数加在一起,就是整个会话用了多少;第二天 resume 回到同一个会话,接着上次的数往下加。
想看,敲 /cost:用 API 计费的,看到的是金额;订阅用户看到的不是金额,是用量,超了限流、不扣钱。
写到这里,一件自始至终没变的事:模型对这本账一无所知,不知道自己吐的字按五倍计,不知道改一个字丢多少折扣,更不知道自己一次最多只能吐 8000 token,这些全是外面那层代码在管。
智能在模型,干活在 harness——连省钱,也是 harness 干的活。
下一篇:它想删你一个文件、跑一条命令之前,谁点头、谁拦着?——权限。
小技巧:找一个聊了几十轮的长会话,紧接着问一句短的,
/cost涨得很慢,重发的都在按一折走;/compact压缩一次,再问同一句,涨一大截:缓存过期,那轮全部重新写入。