OpenAI 和 Claude 的 API 缓存原理:用 conversation history 数组拆给你看
做多轮对话 Agent 的人迟早会盯上账单。假设你的系统提示词加工具定义有 50k token,用户每说一句话,你都要把这 50k 原样发给 API。一次两次还好,一天几百万次调用,这个"每次都重复发送的部分"就是账单的大头。
OpenAI 和 Claude 都在 2024 年 8 月前后上线了 Prompt Caching(提示词缓存),专门治这个病。原理相同——复用 Transformer 前向计算里的 KV Cache(注意力键值对),让重复的输入不用重新算——但实现路径完全不同:OpenAI 全自动,Claude 手动打标。
这篇文章不抽象讲,直接拿一个多轮对话的 messages 数组(conversation history)逐格拆,看两家各自缓存了哪一段、为什么、怎么省钱。
本文 outline
- 先看懂缓存的是什么:KV Cache 与"前缀"
- OpenAI:自动前缀缓存,一个数组走天下
- Claude:cache_control 断点,缓存边界由你定
- 同一个对话数组,两家分别缓存了什么
- 计费与命中率:数字对数字
- 缓存失效:哪些改动会让前缀前功尽弃
- 工程建议:让缓存命中率高的写法
1. 先看懂缓存的是什么:KV Cache 与"前缀"
大模型生成回答时,不是把整段输入重新理解一遍就能直接蹦出答案。Transformer 解码是逐 token 的:每个新 token 要跟之前所有 token 算注意力,而注意力的结果依赖每个 token 的 Key 和 Value 向量。为了不重复计算,推理引擎会把已经算好的 K/V 向量存在内存里,叫 KV Cache。
关键推论:输入的前缀只要完全一样,前缀对应的 KV Cache 就可以直接复用,不需要重新前向传播。Prompt Caching 干的就是这件事——把计算好的前缀 KV Cache 存起来,下次请求前缀相同就直接加载,省掉这一段的 GPU 计算,也省掉这一段的钱。
"前缀"是个硬概念:必须从头开始、逐字节相同的连续段。中间不能有洞,后面不能断。这决定了后面所有的行为——为什么聊天数组的开头要稳定,为什么改一个字就全废。
多轮对话的 messages 数组天然长这个样:
messages = [
{"role": "system", "content": "你是客服助手,请严格按公司政策回答。"}, # 稳定前缀
{"role": "user", "content": "你们的退款政策是什么?"},
{"role": "assistant", "content": "我们的退款政策是 30 天内无理由退……"},
{"role": "user", "content": "那我需要上传什么凭证?"}, # ← 只有这行是"新"的
]
第一轮:system + 用户问题。第二轮:把第一轮的对话完整保留,再追加新问题。第三轮继续追加。数组越接越长,但前面 90% 的内容每一轮都是原封不动的——这正是缓存要吃掉的部分。
2. OpenAI:自动前缀缓存,一个数组走天下
OpenAI 的 Automated Prompt Caching 在 2024 年 8 月上线,最大的特点是:你什么都不用做。没有参数、没有标记、没有开关,符合条件就自动缓存。
它的规则非常朴素,全自动执行:
- 1024 token 起步:输入低于 1024 token 不参与缓存
- 精确前缀匹配:前 1024 个 token 必须逐字节一致,任何一处不同(哪怕一个字符)整个前缀 miss
- 128 token 粒度:命中部分按 128 token 的增量计量
- TTL 短:5-10 分钟不复用就过期,1 小时内基本清空,持续复用可以一直续命
- 命中计费打折:折扣幅度看模型,gpt-4o 是 5 折,gpt-4.1 是 7.5 折,gpt-5 直接 1 折
它的缓存判定就是拿你的 messages 数组当"输入文本"整体看:数组前面的稳定段(system + 历史对话)逐字节一致 → 命中;只有最后几行在变 → 只有最后几行的计算是"新"的。
对多轮对话,官方文档的建议就是一句大白话:把稳定的内容放前面,把动态内容放后面,历史只追加不修改。你照这个习惯写,命中率自然高。命中结果通过响应的 usage 字段看:
response.usage
# {
# "prompt_tokens": 52100,
# "completion_tokens": 180,
# "total_tokens": 52280,
# "prompt_tokens_details": {
# "cached_tokens": 51000 # ← 这 51000 个 token 是命中,只按折扣价收
# }
# }
你控制不了它,但你能监控它——cached_tokens 就是你的命中率仪表盘。
3. Claude:cache_control 断点,缓存边界由你定
Claude 这边完全不同的哲学:手动指定缓存边界。你在某个 content block 上放一个 cache_control 标记(Anthropic 叫它 breakpoint),缓存从输入开头一直划到这个断点为止,断点之前(含断点)全部进缓存。
以系统提示词为例:
from anthropic import Anthropic
client = Anthropic(api_key="sk-ant-...")
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=1024,
system=[
{
"type": "text",
"text": "你是客服助手,请严格按公司政策回答……", # 50k token 的系统提示词
"cache_control": {"type": "ephemeral"}, # ← 断点:到这里为止都缓存
}
],
messages=[
{"role": "user", "content": "你们的退款政策是什么?"},
],
)
这一行 "cache_control": {"type": "ephemeral"} 就是全部关键。缓存规则:
- 断点之前全缓存:从
tools→system→messages的前缀顺序,一直到最后一个断点 - 最少 1024 token(具体按模型,Sonnet 4.5 是 1024)
- 每次请求最多 4 个断点(自动缓存模式用 1 个)
- TTL 两档:5 分钟(默认)和 1 小时(要加
"ttl": "1h",写价翻倍) - 缓存写价是原价 1.25 倍,读价是原价 1 折
因为断点是你打的,Claude 的缓存边界比 OpenAI 更"懂你"——OpenAI 只能缓存从数组最开头开始的连续前缀,Claude 可以让你把某个断点放在 20k 或 80k token 的位置,然后它按需回溯查找。
Claude 的响应里也用 usage 字段报缓存:
response.usage
# {
# "input_tokens": 1300, # 最后一个断点之后的 token 数
# "cache_creation_input_tokens": 50000, # 这次"写"了 50000 token 进缓存
# "cache_read_input_tokens": 50000, # 这次命中了 50000 token
# }
2025 年底开始,Claude 也加了自动缓存模式——在请求体顶层放一个 cache_control,系统自动把断点放在最后一个可缓存块上,并随对话增长向前移动。但核心机制还是那个:断点前的部分进缓存。
4. 同一个对话数组,两家分别缓存了什么
这是全文最关键的对比。同一个多轮对话的 messages 数组,两家的缓存边界完全不同:
messages = [
system 50k token, # ← 稳定
user "退款政策?", # ← 第 1 轮
assistant "30 天无理由…", # ← 第 1 轮回答
user "要上传凭证吗?", # ← 第 2 轮新问题
...
]
OpenAI(自动):把整个数组从第 1 个字节到某个稳定点当缓存前缀。第 1 轮请求后,system + 第一轮对话被缓存;第 2 轮请求,前缀(system + 第 1 轮)命中,只有新追加的 user 问题需要完整计算。缓存边界是"数组里最后一个稳定的连续前缀",由系统自动判定,你猜不到精确位置但一定能吃到。
Claude(手动断点):你在 system 上打一个断点,缓存从数组开头划到 system 结尾。第 1 轮请求后 system 的 50k 进缓存;第 2 轮请求 system 命中,对话部分重新计算。缓存边界是"你打的最后一个断点",位置完全由你控制。
区别在哪?看两件事:
第一,对话历史本身能不能被缓存。 OpenAI 的自动缓存会一路吃满整个稳定前缀——包括之前几轮的用户/助手消息(只要它们字节不变)。Claude 手动模式如果你只在 system 打了断点,历史对话不在缓存里;你得在历史末尾再打一个断点,才能把"system + 到目前对话为止"整个缓存起来。所以 Claude 的多轮对话 Agent,常见做法是每次请求把断点打在历史对话的最后一个 assistant 消息上,让 system 和历史一起缓存。
第二,谁对"边界"负责。 OpenAI 是 best-effort:你只要保持前缀稳定,系统尽力帮你缓存,位置自动;缺点是无法精确控制,也没有手动开关。Claude 是显式声明:边界是你的契约,位置精确,代价是你要维护断点位置(自动缓存模式缓解了这个)。
5. 计费与命中率:数字对数字
两家缓存命中部分的计费逻辑不同:OpenAI 缓存命中按折扣价收(不另收写入费),Claude 缓存写入加价 25%、读取打 1 折。
OpenAI 缓存命中价(每百万 token):
| 模型 | 正常输入 | 缓存命中 | 折扣 |
|---|---|---|---|
| gpt-4o | $2.50 | $1.25 | 50% |
| gpt-4.1 | $2.00 | $0.50 | 75% |
| o1 | $15.00 | $7.50 | 50% |
| gpt-5 | $1.25 | $0.125 | 90% |
| gpt-5-mini | $0.25 | $0.025 | 90% |
Claude 缓存价格(每百万 token,Sonnet 4.5 档):
| 项目 | 价格 |
|---|---|
| 正常输入 | $3.00 |
| 缓存写入(5 分钟 TTL) | $3.75(1.25 倍) |
| 缓存写入(1 小时 TTL) | $6.00(2 倍) |
| 缓存读取 | $0.30(1 折) |
| 输出 | $15.00(不打折) |
算一笔账:假设你的输入 90% 可缓存(如 90k 的 system+历史,10k 的新问题)。
OpenAI + gpt-5:9 万 token 命中按 1 折算 = 0.9 × 0.1 = 0.09,加 1 万正常 = 0.01,总成本约为全价的 10%。
Claude + Sonnet 4.5:9 万 token 如果每轮都要"写入"就要 1.25 倍,但命中的读取只要 0.1 倍。长期运行下:每次请求 9 万命中读取 × 0.1 + 1 万新输入 × 1 = 0.9 万 + 1 万 = 1.9 万 token 等价成本,约为全价的 19%。如果一轮对话中 system 只写一次、之后全是读取,这个数字还会更接近 10%。
两边都能省 80-90%,只是省的路径不同:OpenAI 靠自动命中吃折扣,Claude 靠手动断点吃读取折扣。
6. 缓存失效:哪些改动会让前缀前功尽弃
这是最容易踩的坑。前缀必须逐字节一致,任何改动都会让缓存从改动点往后全部失效。
最典型的几个:
OpenAI 侧:
- 修改了 messages 数组里靠前的任何一条(哪怕只改一个空格、一个标点)
- 换了一个不同的 system prompt 模板
- 数组前 1024 个 token 内有任何动态内容(时间戳、随机 ID、session 变量)
- 工具定义(tools 参数)顺序变了
Claude 侧:
- 修改断点之前的任何内容
- 修改工具定义会令整个缓存失效
- 切换 web search / citations 会令 system + messages 失效
- 修改 tool_choice、disable_parallel_tool_use、图片、thinking 参数,messages 缓存失效
- TTL 到期(5 分钟没复用)
一个常见的反模式:在 system prompt 里拼时间戳。你以为"现在时间是 2026-09-09 10:30"很有用,但它每 30 秒变一次,直接让整个前缀每次都 miss。正确做法:时间戳放数组最末尾的 user 消息里,让前缀保持稳定。
另一个反模式:每轮都重建 messages 数组。有些框架会把历史按"最后 N 条"截断重新组装,导致前缀和上一轮不完全一致。多轮对话要缓存命中,就必须原样保留前缀,只追加新内容。
7. 工程建议:让缓存命中率高的写法
把两家规则收敛成几条可执行的建议:
1. 数组顺序 = 稳定性顺序。 最稳定的放最前(system、工具定义、固定示例),最动态的放最后(当前用户问题)。这是两家的共同要求,也是唯一不冲突的建议。
2. 历史只追加,不重写。 多轮对话就坚持"旧消息原样 + 新消息追加"。任何对旧消息的裁剪、改写、重排都会破坏前缀。要做上下文窗口管理,就做"从中间丢弃最旧轮次",而不是"改写已有轮次"。
3. Claude 用户:断点打在哪有讲究。 系统提示词独立一个断点;如果对话历史够长(超过 1024 token),在历史末尾再打一个断点让历史也进缓存。频繁变动的 tool_choice 等参数会令 messages 缓存失效,能不动就不动。
4. 监控命中率。 OpenAI 看 usage.prompt_tokens_details.cached_tokens,Claude 看 cache_read_input_tokens。命中率低于 70% 就排查:是不是前缀里有动态内容、TTL 是不是太短、数组是不是每轮在重写。缓存是 best-effort 的,不能假设它一定命中。
5. 预热。 长 system 第一次请求必然 miss(要"写入"),如果对首字延迟敏感,Claude 可以用 max_tokens: 0 的预写调用把缓存先热起来,OpenAI 侧则是持续的小流量自然保活。不要让关键请求吃冷启动。
6. 别忘输出不打折。 缓存只作用于输入。如果预算大头其实在输出 token 上,缓存帮不了你,那是另一套优化(结构化输出、降温度、控制 max_tokens)。
参考
- OpenAI Prompt Caching 文档 — 自动缓存规则、1024 token 阈值、TTL
- OpenAI 定价页 — 各模型缓存命中价
- Anthropic Prompt Caching 文档 — cache_control、断点规则、TTL、定价
- Anthropic Prompt Caching 公告 — KV Cache 原理与延迟降幅数据
- Anthropic Cookbook: Prompt Caching — 多轮对话与自动缓存示例
作者: itech001
来源: 公众号:AI人工智能时代(the-ai-era)
网站: https://www.theaiera.top/
关注每日最新AI新闻和技术博客,主页有更多的文章的AI 技术参考:https://www.theaiera.top
关注公众号,获取更多 AI 技术干货!

浙公网安备 33010602011771号