Claude prompt caching 怎么验证命中?机制、脚本与静默 miss 排查(2026)
Prompt caching 缓存的是有序前缀,不是给每个请求自动降价的开关。这篇说明缓存断点放在哪里、怎样用首次创建与后续读取证明命中,以及看起来相同的请求为什么仍可能 miss。
1. 正确心智模型:缓存有序前缀,不是一袋文本
Anthropic 按固定顺序处理提示词:tools、system、messages。一个缓存断点代表从开头到该内容块的完整前缀;下一次请求只有在相关前序内容仍匹配时才能读取它。相同段落出现在另一个位置并不等价。若断点前的工具定义、系统指令、历史消息、模型选择或相关 thinking 设置发生变化,后续前缀可能需要重新创建缓存。因此,内容组织比缓存标记数量更重要:稳定的规则、工具 schema、示例或被反复查询的文档放在前面;时间戳、请求 ID、当前问题和经常变化的检索结果放在最后一个稳定断点之后。
2. 自动缓存与显式断点
Anthropic 当前支持两种启用方式。自动缓存使用顶层 cache_control,会随对话增长移动有效断点,适合普通多轮对话。显式缓存把 cache_control 放在具体内容块上,适合明确知道哪些材料长期稳定的批处理、RAG 和 Agent。不同部分更新频率不同时可以设置多个显式断点,但增加标记不能挽救不稳定的前缀。最容易诊断的起点是:在一段足够长且稳定的 system 内容最后放一个断点,把当前问题放在它之后。是否值得使用以及怎样计价,要结合所选模型和供应商的当前文档判断,不能套用固定倍率。
3. 正确读取三个输入字段
响应中的 usage 才是验证依据。新条目创建时,cache_creation_input_tokens 表示在断点写入的 token;命中时,cache_read_input_tokens 表示从已有条目读取的 token。缓存开启后,input_tokens 不是整个提示词,而是最终断点之后未缓存的输入。总输入应按 input_tokens + cache_creation_input_tokens + cache_read_input_tokens 计算。对一个新的合格前缀,首次响应通常应看到创建大于零、读取为零;TTL 内后续顺序请求应把可复用前缀计入读取字段。即使响应成功,两个缓存字段仍可能都是零,例如该前缀低于所选模型的最低可缓存长度。不要只凭延迟推断命中,也不要只用 input_tokens 推断总输入。
4. 把显式断点放在稳定 system 前缀末尾
下面的 Anthropic Messages 示例只设置一个显式断点。长而稳定的指令或文档放在 system 内容块中,cache_control 放在该稳定块末尾;每次变化的问题放进后续 user 消息。先用一个断点把顺序验证跑通,再根据真实的更新频率决定是否增加边界。本文的 usage 公式只适用于 Anthropic 的缓存字段;其他协议可能使用不同字段与包含关系,必须读对应官方文档。
{
"model": "你的模型名",
"max_tokens": 256,
"system": [
{
"type": "text",
"text": "这里是很长的系统提示词……(要超过最小长度门槛)",
"cache_control": { "type": "ephemeral" }
}
],
"messages": [{ "role": "user", "content": "这里是每次都不同的短问题" }]
}5. 顺序运行验证脚本,保留可审计证据
下面脚本把同一份足够长的稳定前缀顺序发送三次,并打印三个输入字段。必须等前一笔响应返回后再发下一笔,因为新条目要到第一笔响应开始后才可被读取。新前缀首次通常应出现 cache_creation_input_tokens,后续请求应出现 cache_read_input_tokens;若条目已存在,首次也可能直接读取。若缓存字段一直为零,先检查最低长度、断点位置、模型与路由支持、TTL 和模型可见前缀是否变化。脚本只帮助验证 Anthropic Messages 的实际效果,不能据此推断其他协议字段或最终账单价格。
import json, time, urllib.request
BASE = "https://cocodot.co/api/ai" # Anthropic 原生,不带 /v1
KEY = "你的-key"
MODEL = "你的模型名"
# 本次运行只生成一次标记,两笔之间保持完全相同
RUN_TAG = str(time.time_ns())
PREFIX = RUN_TAG + "\n" + ("你是一个严谨的代码审查助手。以下是团队的编码规范,请严格遵守。\n"
"规范条目:每个函数必须有类型注解;禁止裸 except;日志必须结构化。\n") * 220
def call(question):
body = {
"model": MODEL,
"max_tokens": 64,
"system": [{"type": "text", "text": PREFIX,
"cache_control": {"type": "ephemeral"}}],
"messages": [{"role": "user", "content": question}],
}
req = urllib.request.Request(
BASE + "/v1/messages",
data=json.dumps(body).encode(),
headers={"x-api-key": KEY,
"anthropic-version": "2023-06-01",
"content-type": "application/json"},
)
with urllib.request.urlopen(req, timeout=120) as r:
return json.load(r)["usage"]
for i in range(3):
u = call(f"第 {i+1} 次:用一句话回答 1+1 等于几")
read = u.get("cache_read_input_tokens", 0)
write = u.get("cache_creation_input_tokens", 0)
plain = u.get("input_tokens", 0)
total = read + write + plain
rate = read / total * 100 if total else 0
print(f"第{i+1}次 未缓存={plain:6} 写入={write:6} 命中={read:6} 命中率={rate:5.1f}%")6. 六类常见静默 miss 原因
第一,每次调用都在断点前加入时间戳、nonce、会话 ID 或请求 ID。第二,工具定义的顺序变化,或其 schema 被改动。第三,动态 RAG 结果放进 system 前缀,但它会随查询变化。第四,较早的对话消息被编辑、删除、总结或重排。第五,标记的前缀短于所选模型的最低可缓存长度。第六,下一次调用到来时条目已经过期。模型或相关 thinking 设置变化也可能使有效提示词不同。修复方向与原因一一对应:把易变元数据移到断点后,让工具顺序稳定,把稳定规则放在动态检索之前,追加新对话而不是改写历史,用明确足够长的前缀测试,并记录请求间隔。HTTP JSON 的无关空格或键排序不是这里要判断的核心;应比较模型实际看到的内容与顺序。
7. TTL、最低长度与并发会制造很多假警报
Anthropic 默认 ephemeral 缓存寿命为五分钟,从写入或读取请求开始计时,命中会刷新寿命。API 也定义了一小时 TTL,是否采用应查看供应商和模型的当前文档。最低可缓存长度随模型变化,所以小测试即使语法正确也可能让两个缓存字段都为零。并发是另一个陷阱:新条目要等首次响应开始后才可用,同时启动的相同请求未必能读取正在创建的条目。验证时应等待第一笔完成再发第二笔;生产并发可以先预热稳定前缀或控制首批请求。计价同样应以当前供应商文档和实际 usage 为准,不要把某一家倍率硬编码成通用结论。
8. 供应商这一层会不会把缓存弄丢
上游发生 Anthropic prompt caching 的必要条件,是链路保留 cache_control 与稳定的模型可见前缀。客户端若要验证命中并正确核算,响应还必须保留缓存 usage 字段。协议转换层可能忽略字段而仍返回 200;但缓存字段为零也可能来自前缀太短、模型或路由不支持、TTL 已过、工具顺序变化或并发时机。cocodot 的 Anthropic 格式端点会在上游响应包含相应字段时返回 cache_creation_input_tokens 与 cache_read_input_tokens;这是传输能力,不承诺每个前缀都会命中。以顺序测试验证实际效果,并用供应商当前计价文档解释账单。需要辅助时,probe.cocodot.co 可帮助组织可复现检查;对任何第三方诊断工具都使用临时、低额度 key,测完删除。
哪些工作负载适合缓存,以及什么必须保持稳定
| 工作负载 | 适合缓存的前缀 | 变化内容 | 主要风险 |
|---|---|---|---|
| 编程 Agent | 工具定义与系统规则 | 用户任务与工具结果 | 工具顺序或 schema 变化 |
| 文档问答 | 被反复查询的同一文档 | 每次问题 | 每次调用都替换文档 |
| 长对话 | 较早的对话轮次 | 最新一轮 | 改写或压缩历史消息 |
| RAG 应用 | 稳定指令与共享语料 | 每次检索结果 | 把动态检索放到断点前 |
| 批处理 | 共享规则与示例 | 每条记录 | 前缀低于模型最低长度 |
| 低频任务 | 任何可复用前缀 | 定时输入 | 下一次请求晚于 TTL |