Anthropic SDK 提示缓存

QuibblerAgentQuibblerAgent 2026-11-01 约 18 分钟 4 次阅读

第 5 篇算过一笔账:每轮新增 1000 token 的轻量对话,用 claude-sonnet-5-5 跑 100 轮,光输入就累计 10.10 美元——Messages API 无状态,历史每轮全量重发,输入费随轮次按平方量级上涨。那篇结尾预告的成本利器就是本文的主角:提示缓存(prompt caching)。服务端把请求中重复的前缀记住,同一前缀再来时不再按全价处理,而是按一折计费,输入成本最多省九成。它早已完全转正,不需要任何 beta 头,在请求里加一个字段就能用。

本文是《Anthropic SDK 从入门到实战》第 7 篇。全文按"成本账 → 工作原理 → 基本用法 → 计费与回本 → 布局策略 → 命中验证 → 实战 → 总结"组织,并把第 6 篇结尾留下的那笔账——切换思考配置如何击穿缓存——一并算清。

1、重复输入的成本账

先重放第 5 篇的场景:教学模型 claude-sonnet-5-5(输入 2 美元每百万 token),对话每轮净增 1000 token,第 k 轮的输入就是 k 乘 1000。开缓存后的记账方式变了:从第 2 轮起,此前全部历史走缓存读(0.1 倍价),只有本轮新增的 1000 token 需要写入缓存(1.25 倍价);首轮没有可读的,全部写入。两种口径的对照如下:

口径 不开缓存 开缓存
第 2 轮输入费 0.0040 美元 0.0027 美元
第 100 轮输入费 0.2000 美元 0.0223 美元
100 轮累计输入费 10.10 美元 1.24 美元

同样 100 轮,输入总账从 10.10 美元降到 1.24 美元,省约 88%——离"省九成"只差写入溢价的那一点,对话越长、前缀占比越高,越逼近九成这个上限。省钱之外还有一笔时间账:命中缓存意味着服务端跳过对前缀的重复预填充,首 token 到得更早,第 2 篇讲的首字延迟在这里第二次兑现。

这笔账成立的前提,是两次请求的前缀真的逐字节相同。这就引出工作原理。

2、工作原理:前缀匹配

提示缓存的机制一句话讲完:逐字节的前缀匹配。服务端把请求渲染成一条从第 0 个字节开始的完整字符串,顺序固定为 tools、system、messages 三段;缓存键就是从头到断点为止的这段字节。两个请求要命中同一份缓存,从头到断点的每一个字节都必须相同,任何一处不同,该处之后的缓存全部作废。断点由 cache_control 标记声明,意思是"到这里为止的前缀值得缓存",单次请求最多放 4 个。一次请求的两条路径如下:

flowchart TD A[新请求到达] --> B[渲染三段前缀] B --> C{前缀命中缓存?} C -->|命中| D[缓存读 0.1 倍] C -->|未命中| E[全价处理前缀] E --> F[断点处写入 1.25 倍] D --> G[生成回复] F --> G

解读:命中的一路只付 0.1 倍读取价,未命中的一路按全价处理后、在断点处付 1.25 倍写入价把前缀存进缓存,供下次读取。缓存条目有寿命:默认 5 分钟,每次读取都会刷新计时器(计时从请求开始算),因此连续对话的缓存会一直活着;另有一小时档可选,第 4 节细算。多轮对话用的是增量断点——断点随最新一条消息向后挪,旧断点保留为读取点,于是每轮读取全部旧历史、只写入新增量,命中随对话增长而累积。最后两个隔离口径:缓存按模型隔离,换模型等于换缓存键;也不跨组织共享。

3、基本用法:system 尾部断点

最常见的形态是给固定的大段 system 加断点。system 从字符串换成块数组,在最后一个块上打标记,代码如下:

import anthropic
from dotenv import load_dotenv

load_dotenv()
client = anthropic.Anthropic()

def ask(question: str) -> str:
    response = client.messages.create(
        model="claude-sonnet-5-5",
        max_tokens=1024,
        system=[{
            "type": "text",
            "text": KNOWLEDGE,                      # 大段稳定内容
            "cache_control": {"type": "ephemeral"},  # 断点:默认 5 分钟
        }],
        messages=[{"role": "user", "content": question}],
    )
    return "".join(b.text for b in response.content
                   if b.type == "text")

ask("知识库第 1 章讲了什么?")    # 首问:断点前写入缓存
ask("知识库第 2 章讲了什么?")    # 再问:同前缀,走缓存读

解读:唯一的改动是 system 的形态与那行 cache_control。{"type": "ephemeral"} 是当前唯一取值,默认 5 分钟;要一小时档写成 {"type": "ephemeral", "ttl": "1h"}。首问写入、再问读取,问题本身在断点之后,怎么变都不影响缓存。断点也不是只能放在 system,按官方 Messages API 参考的定义,可挂断点的全部位置如下:

位置 典型内容 典型场景
system 文本块 知识库、人设与规则 固定前缀
tools 定义 工具列表 Agent 循环
messages 内容块 逐轮增长的历史 多轮对话
tool_result 块 工具执行结果 工具循环

解读:渲染顺序里 tools 排在 system 之前,所以 system 尾部的断点会连工具定义一起缓存——第 4 篇的循环骨架每轮原样重发 tools 与全量历史,天然满足前缀稳定。较新的 SDK 还支持把 cache_control 直接挂在 create() 顶层,服务端自动把断点落在最后一个可缓存块上并随对话后移,多轮场景可以省去手动挪标记。另有一条静默门槛:可缓存前缀有最小长度,当前一代模型是 512 token、上一代是 1024,低于门槛不报错、只是不缓存。

提醒:低于门槛不会报错,写入字段也只会安静地显示 0——短前缀开了缓存没反应,先查长度,再查失效。

4、计费口径与回本条件

输入侧其实有三档价格。以 claude-sonnet-5-5(输入 2 美元每百万 token)为例,各档实价如下:

计费项 相对全价 均衡档实价
普通输入 1 倍 2.00 美元
缓存写入 5 分钟档 1.25 倍 2.50 美元
缓存写入 1 小时档 2 倍 4.00 美元
缓存读取 0.1 倍 0.20 美元

读取 0.1 倍就是"省九成"的出处。写入要付 25% 溢价(一小时档翻倍到 2 倍),所以缓存不是免费午餐:只发一次的请求加断点,反而多花 25%。顺带一提,旗舰档的读取价更低——Fable 5.1 低至 0.025 倍、Opus 5.5 为 0.05 倍,均衡档就是标准的 0.1 倍,价格以官方模型概览为准。把溢价摊进多次请求,回本账如下(同一前缀发 N 次、一次全价记 1):

请求次数 不缓存 5 分钟档 1 小时档
1 次 1.00 1.25 2.00
2 次 2.00 1.35 2.10
3 次 3.00 1.45 2.20
10 次 10.00 2.15 2.90

5 分钟档第 2 次请求就回本,1 小时档要第 3 次。TTL 的选择就看请求间隔:间隔小于 5 分钟,每次读取都在刷新计时器,5 分钟档一直有效,选 1 小时档纯属多付写入价;间隔在 5 到 60 分钟之间,才轮到 1 小时档发挥;超过 1 小时的空档谁都救不了,要么接受冷启动重写,要么在空档前安排一次保活读取。机制与计费细则见官方提示缓存文档。

5、断点布局:稳定在前

前缀匹配决定了布局纪律:稳定内容在前、易变内容在后,断点钉在稳定段的尾部。先看坏例子——易变内容插在最前面:

# 坏布局:时间戳插在最前,其后所有缓存失效
system = [
    {"type": "text",
     "text": f"当前时间 {time.strftime('%H:%M')},用户 {uid}"},
    {"type": "text", "text": KNOWLEDGE,
     "cache_control": {"type": "ephemeral"}},
]

解读:时间戳块排在第 0 位,前缀从第一个字节起就与上次不同,断点永远不命中——每次都全价处理十万 token 再付 25% 写入溢价,比不开缓存还贵。再看好例子:

# 好布局:稳定内容在前,断点收尾,易变内容后置
system = [{
    "type": "text", "text": KNOWLEDGE,
    "cache_control": {"type": "ephemeral"},   # 断点钉在稳定段尾部
}]
messages = [{"role": "user", "content": [
    {"type": "text", "text": f"当前时间 {time.strftime('%H:%M')}"},
    {"type": "text", "text": question},       # 每次不同的问题放最后
]}]

解读:时间戳挪进用户消息、放在断点之后,怎么变都不碰前缀。除布局外,还有一批改动会在暗中击穿缓存,排查时对照:

改动 击穿范围
system 前段插入时间戳、请求号 全部失效
工具列表增删或重排 全部失效
中途切换模型 全部失效
修改 system 内容 system 与历史失效
切换 thinking 或 effort 历史缓存失效

解读:缓存按 tools、system、messages 三层独立维护,改动只击穿自己那层及其后。最后一行兑现第 6 篇结尾的预告:中途切换思考配置会击穿历史缓存,部分模型连 tools 与 system 层一起击穿——那篇建议工具循环"每一圈都带同一套 thinking 与 effort",不只是推理质量的要求,更是缓存的要求;同理,多轮对话里也别按请求切换 output_config.effort。

6、命中验证:usage 三字段

缓存有没有生效不看感觉,只看 usage。第 5 篇核实过的三个输入字段,读法如下:

u = response.usage
print(f"全价输入 {u.input_tokens}")
print(f"缓存写入 {u.cache_creation_input_tokens}")
print(f"缓存读取 {u.cache_read_input_tokens}")
total = (u.input_tokens
         + u.cache_creation_input_tokens
         + u.cache_read_input_tokens)   # 三者相加才是真实输入量

解读:input_tokens 只是未命中的尾部,三字段相加才是本次真实输入量——第 5 篇埋的这句话在这里兑现。健康的多轮循环长这样:读取逐轮增长、写入只有上一轮的增量、全价只剩断点之后的尾巴。判断标准一句话:重复同一前缀而读取恒为 0,必有静默失效——把相邻两次请求的完整请求体记下来,剥掉 cache_control 标记后逐字节比对,重叠区里第一个分歧点就是击穿处。另有一个时序细节:并发的多个同前缀请求都付全价,缓存要等第一个响应开始输出后才可读;想让并发吃到缓存,先发一个、等到首 token 再放行其余。

7、实战:知识库问答成本对照

零件齐了,做一个固定知识库问答:十万 token 的产品手册放进 system,一百个独立问题逐个提问,缓存前后各跑一遍。成本核算函数如下:

KNOWLEDGE = open("handbook.txt", encoding="utf-8").read()  # 约 10 万 token
QUESTIONS = ["第 3 章的核心结论是什么?",
             # ……共 100 个独立问题
             ]

def ask(question: str, cached: bool) -> float:
    block = {"type": "text", "text": KNOWLEDGE}
    if cached:
        block["cache_control"] = {"type": "ephemeral"}
    r = client.messages.create(
        model="claude-sonnet-5-5", max_tokens=512,
        system=[block],
        messages=[{"role": "user", "content": question}],
    )
    u = r.usage                       # 三档价格分别折成美元
    return (u.input_tokens * 2.0
            + u.cache_read_input_tokens * 0.2
            + u.cache_creation_input_tokens * 2.5) / 1e6

for cached in (False, True):
    total = sum(ask(q, cached) for q in QUESTIONS)
    print("开缓存" if cached else "不开缓存", f"{total:.2f} 美元")

一次典型运行的输出如下:

不开缓存 20.04 美元
开缓存 2.27 美元

解读:不开缓存,每问一次都是十万 token 全价约 0.20 美元,一百问累计 20.04 美元;开缓存,首问付 0.25 美元写入,之后每问只有 0.02 美元出头的读取加一小段尾部全价,累计 2.27 美元——整体省约 89%,稳态的每问成本恰好是原来的十分之一。连续提问时每次读取都在刷新 5 分钟计时器,中途不会掉线;若问题间隔落在 5 到 60 分钟,把断点换成 ttl 一小时档,按第 4 节的回本表,第 3 问起就开始赚。

8、总结

本篇把提示缓存的 2026 年 10 月口径讲全:机制是逐字节前缀匹配,渲染顺序 tools、system、messages,断点用 cache_control 声明、最多 4 个、默认 5 分钟可选 1 小时,低于最小门槛(当前一代 512 token)静默不缓存;计费写 1.25 倍(1 小时档 2 倍)、读 0.1 倍,5 分钟档第 2 次请求回本;布局纪律是稳定在前、易变后置、断点钉在稳定段尾部,切换思考配置与换模型都会击穿缓存;命中与否只认 usage 三字段,读取恒 0 就去 diff 请求体。一句话带走:断点钉在稳定段尾部,改完提示词看一眼 usage。

留三个练习:其一,给第 5 篇的 Chat 类加上缓存——断点放在最后一条消息内容块的尾部,跑 20 轮验证读取逐轮增长、写入只剩增量;其二,在 system 最前面插一个时间戳再跑第 7 节,亲眼看读取恒为 0、总账单反而更贵,然后把它挪进用户消息;其三,把第 7 节的断点换成一小时档、问题数降到 3 个,对照第 4 节回本表验证第 3 次请求才回本。对于任何带着大系统提示或多轮历史跑生产的代码,建议今天就加上第一个断点——一个字段换九成输入折扣,这是整个 API 里性价比最高的一行代码。下一篇讲 Batch 批处理:把不着急的任务排队异步跑,输入输出一律半价。

相关推荐

获取系统SDK版本、判断手机ROM
精选Code

获取系统SDK版本、判断手机ROM

Build获取系统SDK版本Android中部分API的使用,需要在特定的SDK版本之后才能使用,因此在兼容老版本SDK的时候,经常需要判断API的版本。各种Android版本的对应关系参考《Android各版本对应的SDK版本》判断手机ROM有时候需要判断手机系统的ROM,检测ROM是MIUI、EMUI还是Flyme,可以使用getprop命令,去系统build.prop文件(关于build.p

4.2k
AndroidStudio中各种中文乱码问题
精选Code

AndroidStudio中各种中文乱码问题

AndroidStudio中各种中文乱码问题1、编译Java错误信息乱码在出现这个Annotation processors must be explicitly declared now...问题的时候,正好也发现AndroidStudio Build Output错误信息都乱码。经常遇到各种问题,习惯了,现在遇到的问题,都是以后的答案。 1.1、修改项目build.gradle(无效)在整个p

2.8k
PydanticAI 详解:类型安全的 Python AI SDK
精选AI

PydanticAI 详解:类型安全的 Python AI SDK

PydanticAI 详解:类型安全的 Python AI SDK写 LLM 应用最疼的两件事:模型返回的 JSON 不合预期、换一家模型商要改一串代码。pydantic/pydantic-ai(PydanticAI)用 Pydantic 团队的老本行回答了第一件——输出用 BaseModel 定义、运行前先校验,错误从运行时提前到编码时;第二件则用"模型即字符串"回答:换个厂商只改一个参数。如今

57