返回列表10 分钟
AI 工程2026-06-24·10 分钟

AI 缓存不是答案缓存:从 Prompt Cache 到成本治理

#AI 缓存#Prompt Cache#KV Cache#Agent#成本优化
ENTRY LOG

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 通常要求前缀精确匹配。

因此请求结构应该尽量这样组织:

  1. 固定的 System / Developer Prompt
  2. 固定示例
  3. 固定排序的工具定义
  4. 固定输出 Schema
  5. 历史对话
  6. RAG 检索结果
  7. 本轮用户输入

反过来,如果你把动态内容放在最前面,缓存基本就废了。

比如这样:

当前时间: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 反而容易失效。

更好的做法是设置明确的压缩检查点:

  1. 正常阶段持续追加消息
  2. 达到阈值后,一次性生成历史摘要
  3. 建立新的稳定前缀版本
  4. 继续在摘要后追加消息

不要每一轮都对历史做一点删除、改写或者重新排序。

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
  • 当前合同全文

增长部分:

  • 已发生的对话,保持追加

动态后缀:

  • 本轮补充资料
  • 本轮用户问题

发布流程可以是:

  1. 修改 Prompt
  2. 生成新 prompt_version
  3. 灰度 5% 流量
  4. 观察缓存率、TTFT、成本和质量
  5. 逐渐扩大
  6. 旧版本等待平台自然淘汰

这样做的好处是,缓存、成本、质量和发布都能被放到同一套工程治理里,而不是靠猜。

最后,把两种缓存分清楚

讨论 AI 缓存时,最后一定要把两种东西分开:

Prompt / Context Cache:

  • 复用输入前缀的计算结果
  • 不直接复用旧答案
  • 主要优化成本和延迟
  • 本文讨论的是这个

Response / Semantic Cache:

  • 直接复用以前生成的回答
  • 需要处理时效、权限、知识版本和错误传播
  • 属于另一套应用层机制

前者更像推理系统的计算优化,后者更像业务应用的结果复用。

两者都叫缓存,但风险和设计方式完全不同。

我现在越来越觉得,AI 应用进入生产环境后,真正重要的不只是“能不能调通模型”,而是能不能把上下文、缓存、版本、成本和延迟都当成工程对象来管理。

Prompt Cache 看起来是一个性能功能,但它背后其实是一个更大的变化:

AI 系统正在从一次次孤立调用,走向围绕上下文生命周期的工程化管理。

这件事做好了,应用会更快、更便宜,也更容易长期维护。