更新于

Anthropic cache_control 五分钟入门到精通


Anthropic 的 prompt caching 省钱效果之前已经讲过(见prompt caching 在国内中转下省成本指南),这篇不谈”值不值”,只讲怎么写代码——cache_control 参数的具体用法、常见踩坑点,让你五分钟内能跑通第一个缓存请求。

一、最小可运行示例

cache_control 是加在消息内容块(content block)上的一个字段,标记”这一段之后要被缓存”:

from anthropic import Anthropic

client = Anthropic(api_key="your-api-key")

response = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=1024,
    system=[
        {
            "type": "text",
            "text": "你是一个资深代码审查助手。" + "(这里是很长的系统指令,比如几千字的代码规范)" * 50,
            "cache_control": {"type": "ephemeral"}
        }
    ],
    messages=[
        {"role": "user", "content": "帮我审查这段代码:def foo(): pass"}
    ]
)

print(response.content[0].text)
print(response.usage)  # 关注 cache_creation_input_tokens / cache_read_input_tokens

关键在 response.usage 里的两个字段:

  • cache_creation_input_tokens:本次请求新写入缓存的 token 数(第一次调用会有值)
  • cache_read_input_tokens:本次请求命中缓存的 token 数(第二次及之后调用才会有值)

第一次调用没有缓存可读,反而会比不开缓存略贵(写入缓存有小额溢价);从第二次调用开始,命中部分只收约 1/10 的费用。

二、断点(breakpoint)放在哪里

cache_control 标记的是”从内容开头到这个块为止”整体作为一个缓存单元,不是只缓存这一个块。这意味着断点的位置直接决定缓存粒度:

messages = [
    {
        "role": "user",
        "content": [
            {
                "type": "text",
                "text": "这是项目的完整代码库上下文(很长,10k+ tokens)...",
                "cache_control": {"type": "ephemeral"}  # 断点 1:缓存到这里为止
            },
            {
                "type": "text",
                "text": "这是本次用户的具体问题:如何优化这个函数?"
                # 不加 cache_control:这部分每次都变,不需要缓存
            }
        ]
    }
]

放置原则:把”几乎不变的内容”放在前面并标记断点,把”每次都变的内容”放在后面不标记。常见的四层结构:

[静态 system prompt] → 断点1
[项目文件/长文档上下文] → 断点2
[历史对话(增量累加)] → 断点3(可选)
[本次用户输入] → 不标记

一次请求最多可以设置 4 个缓存断点,超过会报错,多段场景要合理取舍,优先缓存体积最大、最不常变的部分。

三、TTL 怎么选:ephemeral 5 分钟 vs 1 小时

cache_control 目前支持两种生存时间:

# 默认:5 分钟 TTL,命中价格约为原价的 1/10
{"type": "ephemeral"}

# 显式指定 1 小时 TTL,命中价格约为原价的 1/5(写入成本也更高)
{"type": "ephemeral", "ttl": "1h"}
场景建议 TTL原因
Cursor/Claude Code 等交互式编码,用户操作间隔短5 分钟(默认)请求密集,5 分钟足够命中,成本最低
客服机器人,用户回复间隔可能超过 5 分钟1 小时避免缓存过期后又按全价重新写入
批量任务,同一 system prompt 跑上百次请求5 分钟,但控制并发让请求连续发出只要请求间隔小于 5 分钟,默认 TTL 更省钱

判断标准很简单:估算两次请求之间的典型间隔,如果经常超过 5 分钟,上 1 小时 TTL;如果请求密集(几秒到几十秒一次),默认 5 分钟性价比最高。

四、常见踩坑

踩坑 1:内容块太小,缓存不生效

Anthropic 对可缓存内容有最小 token 数要求(不同模型略有差异,通常是 1024 或 2048 tokens 起)。如果你标记 cache_control 的内容块本身很短(比如只有几十个 token 的系统提示),缓存不会生效,cache_creation_input_tokens 会一直是 0。

排查方法:检查 usagecache_creation_input_tokenscache_read_input_tokens 是否都是 0——如果是,说明内容块没达到最小缓存阈值。

踩坑 2:每次请求内容”看似相同”但实际有细微差异

缓存命中要求前缀逐字节完全一致。常见的隐藏差异:

  • 时间戳、请求 ID 混入了 system prompt
  • 文件内容读取顺序不稳定(比如用 os.listdir 遍历文件,顺序在不同系统上可能不同)
  • JSON 序列化时字段顺序不固定
# 错误示范:每次请求 system prompt 都不同,永远无法命中缓存
system_prompt = f"当前时间:{datetime.now()}\n你是代码审查助手..."

# 正确做法:把易变内容放到不参与缓存的部分
system_prompt = "你是代码审查助手..."  # 固定不变,可缓存
user_message = f"当前时间:{datetime.now()}\n{user_question}"  # 易变,不缓存

踩坑 3:多轮对话里忘记对历史消息也加断点

多轮对话场景中,如果只缓存了 system prompt,而不断增长的历史消息没有断点,随着对话轮次增加,未缓存部分会越来越大,缓存带来的省钱效果逐渐被摊薄。做法是每隔几轮对话,在最新一条历史消息上重新打一个断点:

def build_messages_with_cache(history, latest_user_input):
    msgs = history.copy()
    if msgs:
        # 在历史消息的最后一条上打断点,缓存到目前为止的全部历史
        msgs[-1] = {**msgs[-1], "cache_control": {"type": "ephemeral"}}
    msgs.append({"role": "user", "content": latest_user_input})
    return msgs

五、通过中转使用时的注意事项

通过 API 中转调用时,cache_control 参数本身完全透传,用法和直连 Anthropic API 一致:

client = Anthropic(
    api_key="your-relay-api-key",
    base_url="https://api.yotradeapi.com"
)
# messages.create 的调用方式和上面完全相同

需要确认的是中转商是否按 Anthropic 官方的缓存计费规则结算——即命中缓存部分是否也按折扣价计费,还是按全价计费后中转自己吃掉差价(不同中转商的计费口径可能不同,建议先用小额测试请求核对账单)。

六、常见问题

Q:缓存是跨账号共享的吗? 不是,缓存是隔离在单个 API Key(或组织)范围内的,不同用户/账号之间的相同内容不会互相命中。

Q:可以缓存 tool 定义(tools 参数)吗? 可以,tools 数组里的定义同样可以通过在最后一个 tool 上加 cache_control 来缓存,用法和 system/messages 一致。

Q:为什么第一次调用比不用缓存还贵一点? 因为写入缓存本身有约 25% 的溢价(相对该内容的原价)。这个成本会在后续命中时被摊销,只要同一内容会被复用 2 次以上,总体就是省钱的。

七、相关阅读

想用 Claude API 但担心国内网络不稳定影响缓存命中率?YoTradeApi 提供低延迟中转线路,按官方计费规则结算缓存折扣,支付宝充值即可接入。