||

Claude 缓存命中率掉到 0?3 步用 diagnostics 查出是哪一段变了

Claude 缓存命中率掉到 0?3 步用 diagnostics 查出是哪一段变了

作者注:Claude 提示词缓存命中率突然掉到 0,或者账单一直降不下来,可以用 diagnostics 三步找到前缀分叉的位置,再结合 usage 判断缓存是否真正命中。本文也会说明走 API易 中转时怎么自查。(如果你有这个问题,可以在看完这篇文章后把这篇文章发给你的 Agent,让他去排查)

缓存没命中不是玄学,API 会告诉你前缀从哪一段开始分叉。你只需要把上一轮响应的 id 传回去,就能看到第一个不一致的地方落在 model、system、tools 还是 messages。

这不是一篇功能发布解读。本文写给已经在用 Claude 提示词缓存、但命中率突然归零或账单居高不下的开发者,所有排查步骤都会写清楚每一步会看到什么。

核心价值:读完本文,你能在三轮请求之内定位缓存失效的原因。你也能分清 diagnostics 和 usage 各自说明什么,并知道走中转时用哪两个字段互相比对。

claude-prompt-cache-diagnostics-hit-rate-troubleshooting-image-0


Claude 缓存诊断 diagnostics 核心要点

先用一句话说清它是什么:把上一轮响应的 id 传进来,API 会对比两次请求,指出第一个分叉点落在 model、system、tools 还是 messages。以前缓存悄悄失效时,你只能看到 usage.cache_read_input_tokens 变成 0,却不知道是哪一段变了。

要点 说明 对你的价值
开关 请求体顶层带 diagnostics 对象才会存指纹 不用改业务逻辑,加一个字段即可
对比方式 previous_message_id 传上一轮响应 id 直接定位第一个分叉段
结论字段 cache_miss_reason 给出六种类型之一 每种类型都有明确的修复位置
配合指标 需要和 usage.cache_read_input_tokens 一起看 避免把「没分歧」误判成「没生效」
可用范围 只有官方 Claude API 提供 走其他通道需要换用 usage 自检

Claude 缓存诊断的时间线

这个功能在 2026 年 5 月以 beta 形式上线,当时需要带 anthropic-beta: cache-diagnosis-2026-04-07 这个 header。9 月期间规则收紧过一次:只有显式带了 diagnostics 对象的请求才会存指纹,只带 header 的请求不会被记录。

9 月 23 日它转为正式版,不再需要 cache-diagnosis-2026-04-07,带着旧 header 的请求也照常能用。现在 POST /v1/messages 的响应里总会出现 diagnostics 字段,没有开启时值为 null。所以看到这个字段是 null,并不代表接口出了问题。

指纹里只保存哈希和 token 数估计,不保存原始提示词内容。它按 response id 索引,作用域限定在本组织和本 workspace,保留时间较短。


3 步用 diagnostics 排查 Claude 缓存命中率

下面三步按官方字段说明来写。如果你通过 API易 调用,Messages 原生接口的 base_url 填 https://api.apiyi.com/v1,请求路径是 /v1/messages。不过要注意,后文实测部分会说明中转场景下 diagnostics 的实际回传情况。

claude-prompt-cache-diagnostics-hit-rate-troubleshooting-image-1

第 1 步:在请求顶层加 diagnostics 对象

diagnostics 是唯一的开关,没带它的请求不会存指纹,后面也就无从比较。第一轮没有可以对比的对象,所以 previous_message_id 传 null,意思是「从这一轮开始记录」。

  • 请求头保留常规版本号:anthropic-version: 2023-06-01
  • 正式版之后不需要再带:anthropic-beta: cache-diagnosis-2026-04-07
  • 在请求体顶层加上:"diagnostics": {"previous_message_id": null}
  • 需要缓存的 system 块照常打断点:"cache_control": {"type": "ephemeral"}
  • 会看到什么:响应里 diagnostics 为 null,cache_read_input_tokens 通常为 0,cache_creation_input_tokens 大于 0

第一轮看到 null 不代表失败。这一轮是在写缓存,本来就没有可比的上一轮,不需要排错。

第 2 步:之后每一轮都传上一轮响应的 id

从第二轮开始,把上一轮响应的 id 填进 previous_message_id。最常见的错误是只在第一轮带 diagnostics,之后漏掉。下一轮指向的那次请求没有被记录,结果会返回 previous_message_not_found。

  • 第二轮请求体:"diagnostics": {"previous_message_id": "<第一轮响应的 id>"}
  • 第三轮请求体:"diagnostics": {"previous_message_id": "<第二轮响应的 id>"}
  • 多轮循环的规律:第一轮传 null,第二轮传 r1.id,第三轮传 r2.id,以此类推
  • 流式调用时,结果出现在 message_start 事件里,id 同样一轮轮往后传
  • 会看到什么:相邻两轮间隔够短、且在同一 workspace 内时,响应里开始出现比较结果

第 3 步:读 cache_miss_reason 并对号修复

diagnostics 有三种取值,读之前先分清楚:

  • null:请求没带 diagnostics;或者 previous_message_id 为 null;或者比较已经完成且没有发现分歧
  • {"cache_miss_reason": null}:比较还在进行中,因为响应开始得太快,这一轮先不下结论,看下一轮
  • cache_miss_reason 是一个对象:已经有结论,里面带着分叉类型和细节
  • 会看到什么:拿到类型后按下一节的对照表修改,先修最早的那个分叉,因为它只报第一个分歧点,后面的问题可能被它挡住了

🚀 快速开始:如果想先确认自己的缓存前缀能不能写入和命中,可以在 API易 apiyi.com 用同一个 Key 切换 Claude 各型号做两轮对照,重点看 usage 里缓存写入和读取两个数字是否对得上。


cache_miss_reason 六种类型与修复位置

拿到 cache_miss_reason 之后,按下表对应到要改的地方:

类型 含义 改哪里
model_changed 模型和上一轮不同,比如路由、A/B 测试或 fallback 换了模型。缓存按模型隔离 同一段缓存对话里固定模型
system_changed system 变了,最常见的是插入了时间戳、请求 ID 等每次都不同的值 system 改成逐字节稳定的常量,动态数据挪到断点之后的第一条 user 消息
tools_changed 工具被增删、调换顺序,或 input_schema 序列化结果不稳定 每轮发送同一份工具列表,顺序固定,序列化按键排序
messages_changed 更早的消息被改动、重排或删除,而不是在末尾追加 历史只追加,assistant 内容和工具结果原样回传
previous_message_not_found 找不到对应指纹,不等于请求变了 每轮都带 diagnostics,不跨 workspace,间隔不要太长
unavailable 这一轮拿不到诊断结果,比如 tool_choice、thinking、context_management、output_config、output_format 或 beta header 集合不一致,也可能是对话太长 一条缓存对话的整个生命周期里,这些参数保持不变

四个 *_changed 类型还会附带 cache_missed_input_tokens,表示分叉点之后大约丢掉了多少可缓存的输入 token。这个数是按字节长度换算的,只能用来判断数量级,不是账单数字,偶尔还会比 usage.input_tokens 还大。


单看 diagnostics 会误判:结合 usage 判断 Claude 缓存命中

diagnostics 回答的是「我的请求变了吗」,usage.cache_read_input_tokens 回答的是「缓存命中了吗」。两个问题不一样,只看其中一个就容易得出错误结论。

diagnostics 结果 缓存读 token 结论 怎么做
null 高 前缀稳定,缓存命中 不用处理
null 低或为 0 请求没变,但缓存已经过期 缩短请求间隔,或改用 1 小时 TTL
*_changed 低 请求变了,导致没有命中 按上一节的对照表修复
*_changed 高 变化发生在靠后的位置,前面的断点仍然命中 值得修,但影响较小

需要再强调一次:null 不等于没生效,它可能正是最好的结果。还有几种情况不适用这张表:第一轮,cache_miss_reason 为 null(比较还在进行),以及 previous_message_not_found 和 unavailable。这几种情况下根本没有产生有效比较。

claude-prompt-cache-diagnostics-hit-rate-troubleshooting-image-2

如果你还没确定哪一轮属于哪个格子,可以在 API易 apiyi.com 上把同一份请求连发两轮,记录每轮的缓存读写数字,再对照上面的矩阵逐项排除。


Claude 缓存命中率低的常见原因:三个硬条件

很多时候命中率低,并不是请求变了,而是缓存从一开始就没写进去。下面三个条件缺一个,缓存就不会生效:

  1. 必须显式打 cache_control:可以放在具体内容块上做显式断点,也可以在请求顶层放一个 cache_control,由系统自动把断点打在最后一个可缓存块上。如果 content 是纯字符串,没有可以挂标记的内容块,就永远不会缓存。
  2. 长度达到最小可缓存长度:在 Claude API 上,Sonnet 5.5 和 Opus 5.5 都是 512 tokens。前缀不够长时,缓存会被静默忽略,也不会报错。
  3. 前缀逐字节相同:历史只追加,system 和 tools 保持稳定。空格、JSON 字段顺序、时间戳任何一处变了,都会被当成新的前缀。

API易 的缓存文档还写了两个硬限制:单次请求最多 4 个 cache_control 断点,每个断点只向前回溯最近 20 个 content block。长的多轮对话只在最后一条打标记,是覆盖不到全部历史的。

TTL 也经常被忽略。默认是 5 分钟,每次命中都会续期,但计时从写入或读取那次请求开始算,而不是从响应结束时算。如果一次流式响应输出了 4 分钟,下一次请求大约要在 1 分钟内发出。需要更长的窗口时,可以在 cache_control 里写 "ttl": "1h"。


Claude 缓存命中率掉到 0 的成本账

下面先列出价格,单位是美元/百万 tokens,数据时间为 2026-10-25,以控制台实时价格为准:

型号 输入 输出 缓存读 5 分钟写 1 小时写
Claude Sonnet 5.5 $2 $10 $0.10 $2.50 $4
Claude Opus 5.5 $4 $20 $0.20 $5 $8
Claude Haiku 5.5(10 万 tokens 以内) $0.1 $0.5 $0.01 $0.125 $0.20
Claude Haiku 5.5(超过 10 万 tokens) $0.50 $2.50 $0.05 $0.625 $1

倍率需要分开看。API易 缓存指南给的通用口径是:命中 0.1×,5 分钟写入 1.25×,1 小时写入 2×。Sonnet 5.5 在 10 月 7 日随官方调整,缓存读从 $0.20 降到 $0.10,变成基础输入价的 0.05×,写入价没有变。

这次调整不是全线降价,Sonnet 5 的缓存读仍然是 $0.20。所以不要拿 0.1× 去算 Sonnet 5.5,具体型号与价格以平台型号列表为准。

回本点按平台的通用 0.1× 口径计算:5 分钟 TTL 复用两次就能回本(1.25 + 0.1 = 1.35,低于不缓存的 2.0);1 小时 TTL 需要复用三次(2 + 0.2 = 2.2,低于 3.0)。

再看一个算例。Sonnet 5.5,固定前缀 3 万 tokens,一天发 200 次,全部在 5 分钟 TTL 内:

  • 全部命中:200 × 3 万 × $0.10/百万 = $0.60,加上一次写入 $0.075,合计约 $0.675/天
  • 全部不命中:200 × 3 万 × $2/百万 = $12/天

两者的整日账单差约 17.8 倍,这不是单价差。单价是 $2 对 $0.10,相差 20 倍。如果改用 1 小时 TTL,一次写入是 $0.12,命中价不变。请求间隔经常超过 5 分钟时,可以对比一下:多付的这部分写入费用,和反复重建前缀的费用相比哪个更低。

claude-prompt-cache-diagnostics-hit-rate-troubleshooting-image-3


走 API易 时如何排查 Claude 缓存命中率

先说实测结果。2026-10-26 我们在 API易 上实测,用 Claude Sonnet 5.5 跑了两轮:第一轮缓存写 2523、读 0;第二轮原样回传后,缓存读 2523、写 0,token 数完全对得上。Claude Opus 5.5 用长前缀同样能写入,也能命中。

但响应里没有 diagnostics 字段。非流式调用没有,流式调用的 message_start 里也没有。我们又带上 beta header 重发了一次,还故意修改 system 制造了一次真实的不命中(读 0、写 2526),响应里仍然没有这个字段。所有请求都返回 200,没有报错。中转层的上游通道不一定是官方那条 Claude API,因此在中转场景下,主要靠 usage 里的两个字段互相比对。

自检现象 说明
本轮 cache_read_input_tokens 精确等于上一轮 cache_creation_input_tokens 前缀完整命中
本轮读明显小于上一轮写 只命中了更靠前的一段,中间有内容变了
写入一直为 0 没打标记、长度不够,或者用了 OpenAI 兼容格式
第二轮又在写入,而读为 0 前缀变了,检查时间戳、UUID、序列化顺序

另外要特别注意:缓存只在原生 /v1/messages 下生效。用 OpenAI 兼容格式 /v1/chat/completions 调用 Claude,无论怎么传参都拿不到缓存计费。

💡 排查建议:API易 apiyi.com 会把 cache_control 原样转发给上游,响应里的两个缓存字段也原样返回,代码不需要为中转层额外适配。把「本轮读 = 上一轮写」做成日志告警,就能取代大部分人工排查。


Claude 缓存诊断 diagnostics 的限制

diagnostics 不能解决所有问题,使用前先了解它的边界:

  • 只有官方 Claude API 提供,Amazon Bedrock 和 Google Cloud 托管的 Claude 都没有
  • 指纹只短期保留,需要连续几轮、间隔较短地发送请求
  • 两次请求必须属于同一组织和同一 workspace,可以用响应头 anthropic-workspace-id 核对
  • 长对话里,如果变化藏在消息列表很深的位置,可能只会返回 unavailable
  • 它是 best-effort 机制,拿不到结果也不会阻塞请求,也不会让请求失败

常见问题 FAQ

Q1:为什么 diagnostics 一直是 null?

分三种情况:没带 `diagnostics` 对象;当前是第一轮,`previous_message_id` 为 `null`;比较完成且没有分歧。要结合缓存读数来判断:读数高就说明一切正常,读数为 0 多半是缓存过期了。如果是走中转,字段可能根本不会回传,这时改用 usage 自检。

Q2:previous_message_not_found 是不是说明请求变了?

不是。它只表示找不到对应的指纹。常见原因有三个:上一轮没带 `diagnostics`,上一轮来自别的 workspace,或者间隔太久。每轮都带上 `diagnostics` 并缩短间隔即可。

Q3:诊断会不会额外收费?

官方文档没有把「诊断费用」单独列为一项,它随普通 Messages 请求一起计费。文档也没有明确写不加价,所以计费请以账单和控制台为准。

Q4:走中转还能用这套诊断吗?

我们在 API易 的实测中没有拿到 `diagnostics` 字段,但缓存的写入和命中都正常。可以用「本轮读是否精确等于上一轮写」来判断,接入方式见 API易 apiyi.com 的缓存计费文档。

Q5:写了 cache_control 为什么一次都没写进缓存?

先检查三点:content 是不是纯字符串;前缀是否达到 512 tokens(Sonnet 5.5 和 Opus 5.5);是否走的是 OpenAI 兼容格式。三者中有任何一个不满足,缓存都会被静默跳过。

Q6:同时出现多个问题,应该先修哪个?

先修 `cache_miss_reason` 报出来的那一个。它只报最早的分叉点,修好之后再跑一轮,后面被挡住的问题才会显现出来。在 API易 apiyi.com 上用同一份请求反复回放,可以比较快地逐个排除。


Claude 缓存诊断核心要点 Key Takeaways

  • diagnostics 是什么:传入上一轮响应 id,定位前缀第一个分叉段
  • 怎么开启:顶层带 diagnostics,第一轮传 null,之后每轮都要传
  • null 的含义:第一轮或没有分歧时也是 null,不能单独当作故障信号
  • 怎么判断:diagnostics 配合 cache_read_input_tokens 一起看才能下结论
  • 硬条件:显式打标记、达到 512 tokens、前缀逐字节相同
  • 成本差距:3 万前缀每天 200 次,整日账单相差约 17.8 倍
  • 中转自检:在 API易 apiyi.com 上看「本轮读是否等于上一轮写」

总结

Claude 缓存命中率排查可以归纳为三件事:

  1. 定位分叉:每轮带上 diagnostics,读 cache_miss_reason,先修最早报出的那一处
  2. 交叉判断:diagnostics 和 usage 两边一起看,null 不代表没生效
  3. 守住硬条件:打好标记,长度达标,历史只追加,system 和 tools 保持稳定

如果你的调用链路拿不到 diagnostics,就用 usage 里的两个字段建立自检。推荐先在 API易 apiyi.com 用两轮请求验证缓存读写数字,再上线到生产环境。


📚 参考资料

  1. Claude 官方文档:Cache diagnostics:诊断字段、六种类型与判断矩阵
  2. 链接: platform.claude.com
  3. 说明: 本文 diagnostics 的字段、时间线和限制均以该文档为准

  4. Claude 官方文档:Prompt caching:缓存规则、TTL、最小长度与价格

  5. 链接: platform.claude.com
  6. 说明: 最小可缓存长度和官方价格表的来源

  7. API易 型号列表:Claude 各型号的输入、输出与缓存读价格

  8. 链接: docs.apiyi.com/models
  9. 说明: 价格数据更新于 2026-10-25

  10. API易 Claude Prompt Caching 缓存计费指南:倍率、回本点与自检判据

  11. 链接: docs.apiyi.com/api-capabilities/claude-prompt-caching
  12. 说明: 中转场景下的排查依据

作者:APIYI 技术团队
技术交流:欢迎在 API易 网站首页联系我们,更多资料可访问 docs.apiyi.com 文档中心

延伸阅读

类似文章