AI 缓存不是答案缓存:从 Prompt Cache 到成本治理
最近聊 AI 调用成本时,经常会碰到一个词:缓存。
这个词听起来很熟。做传统后端的人,一听到缓存,脑子里可能马上想到 Redis、CDN、本地内存、接口结果复用。
但 AI 里的 Prompt Cache,和我们过去说的“接口缓存”不是一回事。
它不是把上一次回答存起来,下次遇到相似问题就直接返回。它真正复用的是模型处理输入时已经算过的中间状态。
这个区别很重要。
如果把 Prompt Cache 理解成“答案缓存”,后面很多判断都会偏掉:你可能会担心它返回旧答案,也可能会错误地用它保存会话状态。但实际上,它优化的是推理过程里的重复计算,而不是替你管理业务数据。
一次 AI 请求里到底有什么
一次稍微复杂一点的 AI 请求,通常不是只有用户问的一句话。
它可能长这样:
- System Prompt
- Developer Prompt
- 工具定义
- JSON Schema
- 历史对话
- RAG 检索资料
- 本轮用户问题
模型真正处理时,要先完成一遍 Prefill。
简单说,就是把输入 Token 经过 Transformer 各层计算,变成注意力里的 Key / Value 状态。后面生成答案时,模型会基于这些状态继续往下生成。
所以你可以把 KV Cache 粗略理解成:模型读完输入后留下的一份“中间笔记”。
第一次请求时,模型要完整读一遍:
第一次:
[固定前缀................][本轮问题]
固定前缀和本轮问题都要完整计算。
第二次,如果前面那一大段固定内容完全一样,只是用户问题变了,就有机会复用前面已经算好的 KV 状态:
第二次:
[固定前缀................][新的问题]
固定前缀复用缓存 KV,新的问题继续计算。
所以文章里经常看到的“缓存读取 Token”,准确说是:
输入上下文中复用了已有计算结果的 Token。
它不是“不再发送这些 Token”,也不是“直接返回上一次答案”。
OpenAI 官方文档也明确说明,Prompt Caching 不影响输出 token 的生成,也不会改变最终回答;无论是否命中缓存,答案仍然会基于本次请求重新计算生成。
缓存主要解决两个问题:钱和时间
Prompt Cache 最直接的作用,是降低输入成本。
很多长上下文请求里,真正变化的内容其实很少。比如一个 Agent 每轮都带着相同的系统规则、工具说明、安全策略和历史上下文,只是在末尾追加一个新问题。
如果这些稳定内容都从头算一遍,账单就会很难看。
命中缓存以后,缓存部分会按更低的输入单价计费。OpenAI 当前文档给出的说法是,在合适场景下,Prompt Caching 可以让输入 token 成本最多降低约 90%,延迟最多降低约 80%。但这里要注意,“最多”不是“必然”,也不是所有模型都按同一个折扣。
成本应该按公式看:
总成本 =
未缓存输入 Token × 普通输入单价
+ 缓存输入 Token × 缓存输入单价
+ 输出 Token × 输出单价
所以看 AI 成本,不能只看总 Token。
一个请求可能有十几万 Token,但其中大部分都命中了缓存,最终成本并不高。另一个请求看起来只有几万 Token,但完全没有缓存,反而更贵。
除了成本,Prompt Cache 还会影响首字延迟。
长上下文慢,很多时候不是模型不会回答,而是它要先把一大段输入读完。命中缓存以后,模型可以跳过大量重复的 Prefill 计算,首个 token 就能更快出来。
这对产品体验很关键。
用户感知里,最难忍的不一定是完整回答慢几秒,而是点了发送以后半天没有任何反应。
Agent 为什么特别需要缓存
Agent 场景里,缓存的价值会被放大。
因为 Agent 的请求经常会反复携带一堆稳定内容:
- 长 System Prompt
- 工具描述
- 工具参数 Schema
- 安全规则
- 历史对话
- 环境说明
- 当前任务背景
如果每一轮都从头计算,成本和延迟会随着上下文增长越来越明显。
举个很常见的例子。
一个代码 Agent 可能先读项目结构,再读几段代码,再调用工具,再根据结果继续判断。每一步看起来都是一次新的模型调用,但前面那套规则、工具定义和任务背景大概率是一样的。
如果这些稳定前缀能复用,系统就不用每轮都“重新认识自己”。
这也是为什么 Prompt Cache 对长对话、RAG、代码分析、合同审查、客服 Agent 这类场景特别重要。
它不是让模型变聪明,而是减少模型重复读同一批上下文的成本。
缓存不会让 Token 从请求里消失
这里有一个容易误解的点。
命中缓存,不代表这些 Token 就不用放进请求了。
从业务语义上看,你仍然需要把完整上下文发给模型。模型也仍然处在这个上下文窗口里。缓存只是让其中一部分输入前缀的计算可以复用。
所以缓存解决的是:
- 计算成本
- 输入计费
- 首字延迟
- GPU 重复计算
它不解决:
- 上下文窗口占用
- TPM 速率限制
- 业务状态存储
- 会话持久化
OpenAI 文档也说明,缓存不会影响 TPM rate limits。也就是说,cached tokens 仍然会计入速率限制。
这点在工程设计里很重要。
如果一个请求已经接近上下文窗口上限,不能因为“反正能缓存”就继续往里塞。缓存不会把上下文窗口变大。
开发时第一件事:把 Prompt 当成有版本的工程资产
很多团队做 AI 应用时,一开始会把 Prompt 写得比较随意。
这里拼一点,那里拼一点。今天加一句规则,明天改一下工具描述,后天又在某个分支里补一个 Schema。
这种做法对缓存非常不友好。
Prompt Cache 依赖相同前缀。前缀一变,缓存命中就会下降。
所以建议从一开始就把 Prompt 当成有版本的工程资产管理:
- agent_name
- prompt_version
- tools_version
- schema_version
- model
- cache_key
比如:
customer-support:v7:tools-v3:schema-v2
这样做有几个好处:
- 可以判断缓存下降是不是由发布导致
- 可以对比不同 Prompt 版本的命中率和成本
- 需要失效旧缓存时,可以通过新版本自然切换
- 回滚时知道恢复的是哪一套前缀
尤其是那些不能手动清理缓存的平台,更需要版本管理。OpenAI 的普通 Prompt Cache 由平台自动淘汰,并不提供手工清空能力。
稳定内容放前面,动态内容放后面
Prompt Cache 通常要求前缀精确匹配。
因此请求结构应该尽量这样组织:
- 固定的 System / Developer Prompt
- 固定示例
- 固定排序的工具定义
- 固定输出 Schema
- 历史对话
- RAG 检索结果
- 本轮用户输入
反过来,如果你把动态内容放在最前面,缓存基本就废了。
比如这样:
当前时间:2026-06-24 10:35:18
请求 ID:随机 UUID
用户问题:……
System Prompt:……
工具定义:……
时间、随机 ID 和用户数据放在最前面,会让每一次请求从开头就不同。后面即使有几万 Token 完全一样,也很难形成相同前缀。
这些追踪字段应该放进 API 的 metadata,不要塞进 Prompt 前部。
官方文档也建议,把固定指令、示例、工具和 Schema 放在开头,把用户相关的变化内容放到末尾。
人眼相同,不代表 Token 前缀相同
Prompt Cache 对“相同”的要求比人更严格。
下面这些变化,都可能破坏缓存:
- 工具顺序变化
- JSON 字段顺序变化
- 空格和换行变化
- Schema 描述变化
- Prompt 中加入当前时间
- 工具参数默认值动态变化
- 同一批 RAG 文档顺序随机变化
所以工程上要做几件事:
- 工具按固定规则排序
- Schema 使用确定性序列化
- Prompt 由统一模板生成
- RAG 结果按稳定规则排序
- 稳定前缀里禁止随机数和时间
尤其是 Agent,不要每轮根据权限动态拼出一套不同顺序的工具数组。
更稳妥的做法是,让完整工具定义保持稳定,再通过平台支持的工具选择参数限制本轮允许调用哪些工具。
多轮对话尽量追加,不要每轮改写前文
适合缓存的对话结构,是不断向尾部追加:
第一轮:
[系统提示][工具][用户1][助手1]
第二轮:
[系统提示][工具][用户1][助手1][用户2]
第三轮:
[系统提示][工具][用户1][助手1][用户2][助手2][用户3]
每轮只是向尾部追加内容,前缀自然可以复用。
但很多系统为了省 Token,会每轮都删一点历史,或者压缩一点历史。
这件事本身没错,压缩历史确实能减少 Token。但它和缓存之间有一个权衡:
如果每一轮都删除最早的一轮,整个历史前缀都会移动,Prompt Cache 反而容易失效。
更好的做法是设置明确的压缩检查点:
- 正常阶段持续追加消息
- 达到阈值后,一次性生成历史摘要
- 建立新的稳定前缀版本
- 继续在摘要后追加消息
不要每一轮都对历史做一点删除、改写或者重新排序。
RAG 里至少有两类缓存
RAG 场景里,“缓存”这个词尤其容易混。
至少要分成两类:
- 检索缓存:问题 -> 检索到的文档片段
- Prompt Cache:重复输入前缀 -> 已计算的 KV 状态
这两件事完全不同,指标也应该分开统计。
普通 RAG 请求可能是:
[稳定的系统规则]
[稳定的工具和输出格式]
[历史对话]
[本轮检索片段]
[用户问题]
由于每次检索结果可能不同,它通常只能命中前面的系统规则、工具和历史部分。
但如果是“针对同一份超长合同连续提问”,就很适合 Prompt Cache:
[固定规则]
[完整合同内容]
[问题 A]
下一轮:
[固定规则]
[完整合同内容]
[问题 B]
完整合同形成了一个很长、可反复命中的公共前缀。
这类场景里,缓存收益会非常明显。
prompt_cache_key 不是答案缓存的 key
一些平台支持类似 prompt_cache_key 的参数。
这个 key 的作用,不是让应用自己保存和读取回答,而是帮助平台把拥有相同前缀的请求路由到更可能命中缓存的位置。
所以 key 应该按稳定边界设计:
- support-agent:v7:tools-v3
- contract-review:v4:schema-v2
- coding-agent:v12
不要这样设计:
- request:{uuid}
- timestamp:{timestamp}
每个请求一个 key,相当于主动放弃复用。
但也不要把所有业务都塞进同一个 key。粒度太粗,会让不同前缀混在一起,缓存效果也不好。
比较合理的划分方式,是按 Agent、Prompt 版本、工具版本、Schema 版本这些稳定边界来设计。
必须记录缓存指标
缓存不能只靠感觉。
至少要记录这些字段:
- model
- prompt_version
- tools_version
- input_tokens
- cached_input_tokens
- uncached_input_tokens
- output_tokens
- cache_ratio
- time_to_first_token
- total_latency
- estimated_cost
- request_success
核心指标可以这样算:
Token 缓存率 =
cached_input_tokens / input_tokens
请求命中率 =
cached_input_tokens > 0 的请求数 / 总请求数
未缓存输入 =
input_tokens - cached_input_tokens
建议重点观察:
- 按 Prompt 版本的缓存率
- 按 Agent 的缓存率
- 发布前后的缓存率变化
- P50 / P95 首字延迟
- 每次成功任务的平均成本
- 输出 Token 占总成本的比例
只看总 Token 没什么意义。
一个请求总 Token 很大,但其中 90% 以上命中缓存,成本结构和完全未命中的请求完全不是一回事。
缓存不能参与业务正确性
Prompt Cache 应该满足一个基本原则:
- 命中:回答正常,只是更快、更便宜
- 未命中:回答仍然正常,只是更慢、更贵
- 缓存被淘汰:业务不出错
不要依靠它保存用户会话、业务状态或长期记忆。
它是推理优化,不是数据库,也不是持久化会话服务。
传统缓存经常讨论“数据过期导致脏读”。Prompt Cache 的主要风险不太一样,更多是:
- 前缀频繁变化导致命中率低
- 以为缓存了,实际没缓存
- 发布后成本突然上升
- 缓存有效期结束导致冷启动
- 敏感内容的保留策略不符合要求
所以它应该被当成性能和成本优化层,而不是业务状态层。
还要关注保留时间和数据政策
缓存不是永远存在的。
以 OpenAI 当前机制为例,普通内存缓存通常在空闲约 5 到 10 分钟后开始被淘汰,最长可能保留约一小时。支持扩展缓存的模型最长可保留 24 小时。
OpenAI 文档还提到,扩展缓存会在内存满时把 key/value tensors 暂存在 GPU 本地存储;原始 prompt 文本只保留在内存里,但 KV 张量本身仍然来自客户内容。
因此涉及敏感数据时,要结合组织的数据保留、地域和 Zero Data Retention 要求选择策略。
最重要的是:缓存保留时间不能当作业务 SLA。
你的程序不能假设“十分钟内一定命中”,也不能假设“上一轮缓存一定还在”。
一套比较实用的结构
如果做一个合同审核 Agent,可以这样拆:
cache_key:
contract-review:v5:tools-v2:schema-v3
稳定前缀:
- Developer Prompt v5
- 审核规则
- 示例
- Tools v2,固定排序
- JSON Schema v3
- 当前合同全文
增长部分:
- 已发生的对话,保持追加
动态后缀:
- 本轮补充资料
- 本轮用户问题
发布流程可以是:
- 修改 Prompt
- 生成新 prompt_version
- 灰度 5% 流量
- 观察缓存率、TTFT、成本和质量
- 逐渐扩大
- 旧版本等待平台自然淘汰
这样做的好处是,缓存、成本、质量和发布都能被放到同一套工程治理里,而不是靠猜。
最后,把两种缓存分清楚
讨论 AI 缓存时,最后一定要把两种东西分开:
Prompt / Context Cache:
- 复用输入前缀的计算结果
- 不直接复用旧答案
- 主要优化成本和延迟
- 本文讨论的是这个
Response / Semantic Cache:
- 直接复用以前生成的回答
- 需要处理时效、权限、知识版本和错误传播
- 属于另一套应用层机制
前者更像推理系统的计算优化,后者更像业务应用的结果复用。
两者都叫缓存,但风险和设计方式完全不同。
我现在越来越觉得,AI 应用进入生产环境后,真正重要的不只是“能不能调通模型”,而是能不能把上下文、缓存、版本、成本和延迟都当成工程对象来管理。
Prompt Cache 看起来是一个性能功能,但它背后其实是一个更大的变化:
AI 系统正在从一次次孤立调用,走向围绕上下文生命周期的工程化管理。
这件事做好了,应用会更快、更便宜,也更容易长期维护。