跳转到主要内容

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× 计:
类型价格说明
普通输入不缓存的部分,该多少钱多少钱
缓存写入(5 分钟 TTL)1.25×第一次写入贵一点点
缓存写入(1 小时 TTL)想存更久就要付更多
缓存读取(命中)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.71024
Sonnet 4.6 / Haiku 4.5 之前的 Haiku2048
Opus 4.5 / 4.6 / 4.7、Haiku 4.54096
中文 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
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)的额外提示

中转站是否真的支持缓存,取决于它两件事都做对:
  1. 把请求里的 cache_control 字段原样转发给 Anthropic 上游;
  2. 把上游响应里的 cache_creation_input_tokens / cache_read_input_tokens 原样回吐给你。
如果你的代码本身没问题(用直连 Anthropic 的 key 跑能看到 write > 0read > 0),换到中转站 usage 字段就丢了或恒为 0,那就是中转的问题。先用直连做对照实验,再上中转。

九、要点回顾

  1. 标记:cache_control: {"type": "ephemeral"},加在 content block 上。
  2. 长度:够长(Sonnet 4.6 ≥ 2048 tokens)才会真的缓存。
  3. 顺序:稳定内容在前,易变内容在后。
  4. 验证:看 usage.cache_read_input_tokens > 0
  5. 省钱:命中只要 0.1×,但写入要 1.25×,2 次以上才回本。