Documentation Index
Fetch the complete documentation index at: https://docs.apiyi.com/llms.txt
Use this file to discover all available pages before exploring further.
Claude Prompt Caching 入门:从零到能用
面向第一次接触 Anthropic Messages API 缓存功能的同学。读完你能做到:写出会缓存的请求、看懂账单、判断为什么没命中。
一、一句话理解
把一段反复使用的长 prompt(系统说明 / 长文档 / few-shot 示例)标记一下,服务器就会把它存起来。下次相同前缀的请求来,服务器跳过重复处理,便宜约 10 倍、也更快。5 分钟内没人再用,就过期。
二、为什么要用 —— 看账单
以输入 token 价格为 1× 计:
| 类型 | 价格 | 说明 |
|---|
| 普通输入 | 1× | 不缓存的部分,该多少钱多少钱 |
| 缓存写入(5 分钟 TTL) | 1.25× | 第一次写入贵一点点 |
| 缓存写入(1 小时 TTL) | 2× | 想存更久就要付更多 |
| 缓存读取(命中) | 0.1× | 重头戏。后续每次便宜 90% |
回本点:5 分钟 TTL 只需要 2 次请求复用同一前缀就回本(1.25 + 0.1 = 1.35,比 2 次都不缓存的 2.0 便宜);1 小时 TTL 需要 3 次才回本(2 + 0.2 = 2.2,比 3.0 便宜)。
什么场景适合:同一份长系统提示词被多次调用、多轮对话、批量处理同一份文档、RAG 把检索到的文档作为前缀。
什么场景不适合:每次 prompt 从第一个字开始都不一样;或者整体很短,根本到不了最低门槛(见下)。
三、触发缓存的三个硬条件
缺一不可。
1. 必须显式打标记 cache_control
content 不能是纯字符串,必须是 content block 数组,在要缓存的那一块上加 cache_control: {"type": "ephemeral"}:
# 错: 纯字符串永不缓存
"content": "一大段长文..."
# 对: content block + 标记
"content": [
{
"type": "text",
"text": "一大段长文...",
"cache_control": {"type": "ephemeral"},
},
{"type": "text", "text": "问题"},
]
2. 长度必须达到最小阈值
短于阈值的内容,就算打了标记也不会缓存(也不报错,静默忽略)。按模型不同:
| 模型 | 最小 tokens |
|---|
| Sonnet 4.5 / 4 / 3.7 | 1024 |
| Sonnet 4.6 / Haiku 4.5 之前的 Haiku | 2048 |
| Opus 4.5 / 4.6 / 4.7、Haiku 4.5 | 4096 |
中文 1 个字大约 0.5–1 token,所以 Sonnet 4.6 至少要 2000 字以上的稳定内容才有意义。
3. 前缀必须逐字节相同
缓存按前缀匹配:从请求开头一直到 cache_control 标记位置,这段字节流必须和上一次完全一样。改任何一个字符——哪怕是空格、JSON 字段顺序、时间戳——都算”新前缀”,会重新写入而不是命中。
实践含义:稳定的东西放前面,易变的东西放后面。
# 错的顺序: 每换一个问题,前缀就变了,永远命中不了
content = [
{"type": "text", "text": "请回答下面问题: " + 问题}, # 这块在变
{"type": "text", "text": 长文, "cache_control": {...}},
]
# 对的顺序: 长文在前打标记,问题在后不打标记
content = [
{"type": "text", "text": 长文, "cache_control": {...}}, # 这块稳定
{"type": "text", "text": 问题}, # 这块随便变
]
四、最小可运行示例
跑两次同一个长文 + 不同问题。第一次写入,第二次命中。
import json, os, requests
URL = "https://api.anthropic.com/v1/messages" # 用 apiyi 中转改成 https://api.apiyi.com/v1/messages
KEY = os.environ["ANTHROPIC_API_KEY"]
HEADERS = {
"content-type": "application/json",
"x-api-key": KEY,
"anthropic-version": "2023-06-01",
}
# 必须足够长。Sonnet 4.6 至少 2048 tokens,大约 2000+ 中文字。
LONG_TEXT = open("apiyi_messages_long_10k_zh.txt").read()
def ask(question: str, label: str):
payload = {
"model": "claude-sonnet-4-6",
"max_tokens": 256,
"messages": [{
"role": "user",
"content": [
{"type": "text", "text": LONG_TEXT, "cache_control": {"type": "ephemeral"}},
{"type": "text", "text": question},
],
}],
}
r = requests.post(URL, headers=HEADERS, data=json.dumps(payload), timeout=120)
u = r.json().get("usage", {})
print(f"[{label}] input={u.get('input_tokens')} "
f"write={u.get('cache_creation_input_tokens')} "
f"read={u.get('cache_read_input_tokens')}")
ask("请概括主旨", "第1次") # 期望 write>0, read=0
ask("请给3个关键词", "第2次") # 期望 write=0, read>0
期望看到:
[第1次] input=35 write=6512 read=0
[第2次] input=22 write=0 read=6512
第 2 次的 read ≈ 第 1 次的 write,说明同一段前缀被命中复用了。
五、怎么判断命中没命中 —— 看三个字段
每次响应的 usage 里:
| 字段 | 含义 | 计费倍率 |
|---|
input_tokens | 没被缓存的剩余输入 token | 1× |
cache_creation_input_tokens | 这次写入缓存的 token 数 | 1.25× 或 2× |
cache_read_input_tokens | 这次从缓存读取的 token 数 | 0.1× |
输入总量 = 三者之和。如果 read > 0,你就在省钱。
六、几个最常见的踩坑
| 现象 | 原因 |
|---|
write 永远是 0 或字段不存在 | 没打 cache_control 标记 / 长度没到阈值 / 中转站没透传 |
第 2 次 write 又 > 0,read 还是 0 | 前缀变了。常见:prompt 里有 datetime.now()、UUID、变化的用户 ID;或 JSON 序列化顺序不稳定;或加了带时间戳的 system 提示 |
| 隔了一会儿再调,又变成写入 | 5 分钟没人用就过期。需要常驻可以加 {"type": "ephemeral", "ttl": "1h"} |
| 同一份 prompt 切换模型后没命中 | 缓存按模型隔离,换模型相当于换 key |
| 多轮对话第一轮命中、第三轮又不命中 | 单次请求最多 4 个 cache_control 断点;且每个断点只往前找 20 个 content block,长对话要定期往新消息上补打断点 |
七、进阶:多轮对话怎么打
把 cache_control 打在最近一条 user 消息的最后一个 content block 上。每加一轮就把上一次的标记保留、再在新的最末块上加一个。这样每一轮的缓存读取范围都会自动延伸到上一轮结束的位置。
# 第 N 轮请求构造时
messages[-1]["content"][-1]["cache_control"] = {"type": "ephemeral"}
注意单次请求最多 4 个断点,所以长对话里要么周期性清理旧标记,要么靠最新断点自动覆盖前面所有内容(前缀匹配会一路向前找)。
八、用中转站(比如 apiyi)的额外提示
中转站是否真的支持缓存,取决于它两件事都做对:
- 把请求里的
cache_control 字段原样转发给 Anthropic 上游;
- 把上游响应里的
cache_creation_input_tokens / cache_read_input_tokens 原样回吐给你。
如果你的代码本身没问题(用直连 Anthropic 的 key 跑能看到 write > 0 和 read > 0),换到中转站 usage 字段就丢了或恒为 0,那就是中转的问题。先用直连做对照实验,再上中转。
九、要点回顾
- 标记:
cache_control: {"type": "ephemeral"},加在 content block 上。
- 长度:够长(Sonnet 4.6 ≥ 2048 tokens)才会真的缓存。
- 顺序:稳定内容在前,易变内容在后。
- 验证:看
usage.cache_read_input_tokens > 0。
- 省钱:命中只要 0.1×,但写入要 1.25×,2 次以上才回本。