缓存命中率为什么重要
更新·markdown 原文
我梳理了一下 Anthropic 提示词缓存的计价和失效规则。跑 Claude Code 的时候,缓存读取占掉的 token 比其他三项加起来还多,所以命中率不是优化项,是账单的主项。数字截至 2026 年 7 月。
三个价钱
| 相对输入价 | |
|---|---|
| 缓存读取 | 0.1 倍 |
| 缓存写入(5 分钟档) | 1.25 倍 |
| 缓存写入(1 小时档) | 2 倍 |
命中一次,那段 token 只花十分之一的钱。代价是写进去的时候要先付一次溢价。
所以有回本点:
- 5 分钟档:写 1.25 + 读 0.1 = 1.35,而不缓存跑两次是 2.0。两次请求就回本。
- 1 小时档:写 2 + 读 0.2 = 2.2,不缓存三次是 3.0。要三次才回本。
1 小时档的用处是熬过流量空档——请求间隔超过五分钟时缓存不会凉。但写入贵一倍,得靠更多次读取摊平。
短了不缓存,而且不报错
最小可缓存前缀的长度按模型不同,短于这个数就静默地不缓存——没有报错,只有 cache_creation_input_tokens: 0:
| 模型 | 最小长度 |
|---|---|
| Opus 4.8 / 4.7 / 4.6 / 4.5、Haiku 4.5 | 4096 token |
| Fable 5、Sonnet 4.6 | 2048 token |
| Sonnet 4.5 及更早 | 1024 token |
同一段 3000 token 的前缀,在 Sonnet 4.5 上缓存得好好的,换到 Opus 4.8 就一次都不命中。换模型之后账单变了却查不出原因,先看这张表。
怎么看有没有命中
响应的 usage 里三个字段:
| 字段 | 含义 |
|---|---|
cache_creation_input_tokens | 这次写进缓存的(付了 1.25 或 2 倍) |
cache_read_input_tokens | 这次从缓存读的(付了 0.1 倍) |
input_tokens | 没命中的那部分,全价 |
`input_tokens` 是余数,不是总数。 提示词总长 = 三个字段相加。跑了一整晚的 agent 只显示 4K input_tokens,不是它没吃 token,是剩下的都从缓存来了。
反过来,如果重复请求之间 cache_read_input_tokens 一直是 0,说明有东西在悄悄让缓存失效。
静默失效:前缀里改一个字节,后面全废
缓存是前缀匹配。 渲染顺序固定:tools → system → messages。前缀里任何位置变了一个字节,那个位置之后的全部作废。
常见的六个凶手,都在前缀最前面:
| 写法 | 后果 |
|---|---|
system 里插 datetime.now() / Date.now() | 每次请求前缀都不一样,一次都不会命中 |
| 早期内容里放 UUID、请求 ID | 同上 |
json.dumps(d) 没加 sort_keys=True,或者遍历 set | 序列化顺序不稳,字节就不稳 |
| 把用户 ID、会话 ID 拼进 system | 每个用户一套前缀,跨用户完全不共享 |
if flag: system += ... 条件拼接 | 每种开关组合都是一份独立前缀 |
| 工具集按用户变 | 工具渲染在最前面,全员不共享 |
修法只有一个方向:把会变的东西挪到最后,挪到最后一个断点之后。
不是所有改动都全废
有个分层,改动只影响自己那层和下面:
| 改了什么 | 工具缓存 | system 缓存 | messages 缓存 |
|---|---|---|---|
| 工具定义(增删改顺序) | 废 | 废 | 废 |
| 换模型 | 废 | 废 | 废 |
| system 内容 | 保 | 废 | 废 |
tool_choice、开关 thinking | 保 | 保 | 废 |
换模型等于缓存清零——缓存是按模型存的。这条对换中转站的人尤其要紧:换一家站就是换一个端点,之前攒的缓存一次都用不上,头几个请求全是冷写入。
两个不那么显眼的坑
20 条回看窗口。 每个断点最多往回找 20 个 content block。agent 循环里一轮塞进去二三十个 tool_use / tool_result 是常事,超过之后下一次的断点就找不到上一次的缓存,静默 miss。长轮次里每 15 条左右补一个断点。
并发全付全价。 缓存要等第一个响应开始返回之后才可读。同时发 N 个前缀相同的请求,谁也读不到还在写的那份,N 份全价。正确顺序是先发一个,等它吐出第一个 token,再发剩下的。
这跟买中转站有什么关系
三件事:
一、逆向档大多没有缓存。 这是它比价时最容易被漏掉的一块。假设一次请求里 80% 的输入是可缓存的重复前缀(跑 Claude Code 时这个比例不算高):
- 有缓存:0.8 × 0.1 + 0.2 × 1 = 0.28 倍输入价
- 没缓存:1 倍输入价
差 3.5 倍。一个名义上便宜三倍的逆向档,实际跑起来可能比官转还贵。(上面 80% 是举例用的假设,不是实测值——你自己的比例看 usage 三个字段现算。)
二、比价要连缓存一起比。 「占官方价百分比」比的是单价。如果两边的缓存支持不一样,单价一样也不等于账单一样。
三、换站的成本不只是重新充值。 缓存按模型和端点存,换过去头几个请求都是冷写入,付的是 1.25 或 2 倍。频繁比价换站的人,这笔钱是隐性的。
附:术语速查
| 词 | 意思 |
|---|---|
| 前缀匹配 | 缓存按提示词开头的字节序列命中,中间变一个字节,后面全废 |
| 断点(breakpoint) | cache_control 标记的位置,每次请求最多 4 个 |
| 冷写入 | 第一次写进缓存,付 1.25 或 2 倍 |
| TTL | 缓存存活时间,5 分钟或 1 小时两档 |
| 命中率 | cache_read_input_tokens 占总输入的比例 |