第 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 个。一次请求的两条路径如下:
解读:命中的一路只付 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 批处理:把不着急的任务排队异步跑,输入输出一律半价。

