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

  1. 先看懂缓存的是什么:KV Cache 与"前缀"
  2. OpenAI:自动前缀缓存,一个数组走天下
  3. Claude:cache_control 断点,缓存边界由你定
  4. 同一个对话数组,两家分别缓存了什么
  5. 计费与命中率:数字对数字
  6. 缓存失效:哪些改动会让前缀前功尽弃
  7. 工程建议:让缓存命中率高的写法

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"} 就是全部关键。缓存规则:

  • 断点之前全缓存:从 toolssystemmessages 的前缀顺序,一直到最后一个断点
  • 最少 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_choicedisable_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)。

参考


作者: itech001
来源: 公众号:AI人工智能时代(the-ai-era)
网站: https://www.theaiera.top/
关注每日最新AI新闻和技术博客,主页有更多的文章的AI 技术参考:https://www.theaiera.top

关注公众号,获取更多 AI 技术干货!

posted @ 2026-09-10 21:44  iTech  阅读(6)  评论(0)    收藏  举报