缓存的不是提示词原文,是渲染后的前缀
服务端缓存的单位是“渲染后的前缀”:instructions、developer messages、工具定义、历史消息、图片和文档,按请求里的实际顺序拼起来的那一段。命中要求从第一个 token 开始逐字相同,一直到断点位置为止。
- 前缀里任何一处改动,都会让断点之后的所有内容失效。
- 顺序也算内容:工具定义换了个位置,前缀就不一样了。
- 把稳定内容放前面、变化内容放后面,是唯一有效的结构。
- 缓存是按组织隔离的,别人的请求不会让您的缓存命中。
| 请求结构 | 能不能命中 | 说明 |
|---|---|---|
| 固定的 system/developer + 固定的工具定义 + 变化的问题 | 能 | 理想结构 |
| 每次请求都拼当前时间戳在最前面 | 不能 | 第一个 token 就不同 |
| 工具定义每次从 map 遍历生成,顺序不稳定 | 不稳定 | 顺序变了前缀就变了 |
| 历史消息每次重新摘要压缩 | 不能 | 压缩改变了前缀内容 |
| 多轮对话只追加新消息 | 能 | 经典可命中场景 |
省钱的两个硬条件:够长、够复用
第一是长度门槛。从 GPT-5.6 及之后的模型开始,可缓存前缀的最小长度是 1,024 个可见 token;短于这个长度的前缀不会被缓存,你也不会因此多付钱,只是没有折扣。第二是复用次数:写缓存要加价,读缓存才打折,只调用一次的话缓存一定不划算。
| 项目 | 计费方式(相对未缓存输入) | 说明 |
|---|---|---|
| 缓存命中读取 | 0.1×(GPT-6.1 Sol 为 0.05×) | 只有命中的那部分前缀享受折扣 |
| 缓存写入,TTL 30m | 1.25× | 写入按更高倍率计费 |
| 缓存写入,TTL 12h | 2.5× | 保留更久,写入更贵 |
| 未命中部分 | 1× | 正常价格,没有惩罚 |
| 最小可缓存前缀 | 1,024 可见 token | GPT-5.6 及之后;更早的模型规则不同 |
命中读取便宜,但命中的 token 仍然计入速率限制(TPM)。也就是说缓存省钱,不省配额:高并发场景下,限速不会因为命中而放宽。
显式断点与隐式断点
缓存默认开启。隐式模式下,断点落在“最新一条合格消息的末尾”:用户消息、最后一条工具结果、或者最初的 developer 块。你的多轮对话天然就在这些位置增长,所以隐式模式对聊天类应用基本够用。
显式模式让你自己指定断点位置,适合“一长段固定前缀 + 一段变化内容”的结构,比如固定的规则和工具定义很长、每次只有任务描述不同。写法是在 prompt_cache_options 里声明模式和 TTL,再在需要的位置给 input_text 块加断点标记。
const response = await client.responses.create({
model: "gpt-5.6", // 模型名与可用档位以官方文档为准
prompt_cache_options: {
mode: "explicit",
ttl: "30m",
prewarm: true,
},
input: [
{
role: "developer",
content: [
{
type: "input_text",
text: longRulesAndToolDocs, // 越长越稳定,越值得缓存
prompt_cache_breakpoint: { mode: "explicit" },
},
],
},
{
role: "user",
content: [{ type: "input_text", text: questionForThisCall }],
},
],
});- prewarm 可以在没有真实请求时先把前缀写进缓存,首个请求就不用等写入。
- 每个请求最多 4 次缓存写入,断点不是越多越好。
- 断点之后的内容仍按原价计算,所以断点要放在“最后一次变化”之前。
- 从隐式切到显式时,之前的隐式写入不会自动变成显式断点,会有一段空窗期。
prompt_cache_key:路由与账目归集
老一代模型的配置项不一样,用的是 prompt_cache_retention(可选 in_memory 或 24h)来控制保留策略。而 prompt_cache_key 在新旧两套里都有用:它决定请求被路由到哪台机器,也决定账单上这部分缓存算在谁头上。
如果同一个前缀的请求被随机分散到不同机器上,每台机器都要重新写一次缓存,命中率自然上不去。给同一类请求用同一个 key,命中率会明显稳定下来。官方建议每个 key 大约 15 请求/分钟的节奏,别把所有流量都塞进一个 key。
function cacheKeyFor(tenant: string, workflow: string) {
// 前缀完全相同的请求才共用 key
return tenant + ":" + workflow;
}
const response = await client.responses.create({
model: "gpt-5.6",
prompt_cache_key: cacheKeyFor("acme", "invoice-extract"),
input: [
{ role: "developer", content: [{ type: "input_text", text: sharedPrefix }] },
{ role: "user", content: [{ type: "input_text", text: document }] },
],
});常见的失效原因
命中率上不去,几乎都能归到同一类原因:你以为没变的前缀,其实变了;或者你变了断点之前的东西。下面这些是最常遇到的几种,按现象倒查比逐条排查快。
| 现象 | 原因 | 处理 |
|---|---|---|
| 前缀没变,但命中率是 0 | 动态内容(时间、随机 ID、用户昵称)出现在前缀里 | 把这些内容挪到断点之后 |
| 改了早先的一条消息后命中率骤降 | 那条消息的断点位置移动了,后面的前缀全部重算 | 把易变消息放到最后,或改完让它自然重新写入 |
| 从隐式切显式后一段时间没命中 | 之前的写入是隐式的,不匹配显式断点 | 接受这段过渡期,或先用 prewarm 预热 |
| 多轮对话越长命中率越低 | 上下文被摘要或裁剪,前缀内容被改写 | 保留原文,只追加;压缩放到另一条链路 |
| 同一份提示词两套服务命中率不同 | 缓存按组织隔离,且路由可能落在不同机器 | 统一 key,确认走的是同一个入口 |
| 以为命中了就不用管限速 | 命中 token 仍计入 TPM | 按总 token 规划配额,而不是按账单金额 |
成本账怎么算
只看“命中率”会算错。缓存的价值只发生在前缀那一部分,而且第一次要付写入溢价。把账拆成三段:前缀写入、前缀命中读取、其余未缓存部分。
# 设前缀部分单次原价为 1,复用 n 次
# 不用缓存:n
# 用缓存:写入倍率 + (n - 1) * 命中倍率
# 30m TTL:1.25 + 0.1 * (n - 1) -> n >= 2 时开始便宜
# 12h TTL:2.5 + 0.1 * (n - 1) -> n >= 3 时开始便宜
# 结论:复用两三次以上,前缀越长越值得缓存- 先算清前缀有多大:instructions + 工具定义 + 固定示例,通常比你以为的大。
- 再估复用频次:同一前缀一天被调多少次,决定选 30m 还是 12h。
- 写入溢价只发生在前缀变化后的第一次,不要把 1.25× 乘到所有请求上。
- 输出 token 不打折,缓存只影响输入侧。
- 真实单价请以官方页面的当前价格为准,本文只用倍率说明结构。
上线前的清单
- 01
固定前缀顺序
instructions、工具定义、固定示例按固定顺序拼接,不要在中间插入动态内容。
- 02
检查长度门槛
前缀是否达到当前模型的最小可缓存长度;没达到就先优化结构,别硬凑。
- 03
选模式与 TTL
聊天类多轮用隐式即可;固定长前缀 + 变化任务用显式,按复用间隔选 30m 或 12h。
- 04
分配 cache key
按“前缀相同”的维度分组,避免按请求生成 key。
- 05
打点并复核
记录命中/未命中 token、每次请求的写入次数,一周后按真实数据回头看成本。
- 命中率不是目标,单位任务成本才是。
- 前缀改动要当成一次发布:改完缓存必然重建,别在高峰期改。
- 限速按总 token 规划,命中不会让 TPM 变宽。
- 多环境(开发/预发/生产)不要共用一个 key,会互相干扰账目。
常见问题
Prompt 缓存需要手动开启吗?
默认开启,命中与否取决于前缀是否逐字相同。要自己控制断点位置和保留时长时,才需要显式声明模式和 TTL。
为什么我的提示词没变,还是没有命中?
最常见的原因是前缀里混进了动态内容——时间戳、请求 ID、用户名——或者工具定义的拼接顺序不稳定。逐字相同才算命中,顺序变了也不行。
缓存命中后,速率限制会放宽吗?
不会。命中的 token 仍然计入 TPM 等限速指标,缓存省的是钱,不是配额。高并发场景要按总 token 规划容量。
前缀很短,还有必要优化缓存吗?
不值得。低于当前模型的最小可缓存长度时不会产生缓存效果;为了够长度而补充无意义内容,通常比不缓存更贵。
官方文档与参考
本文依据上述官方文档整理;价格、额度与功能会随官方更新变化,第三方中转站的行为以其自身说明为准。