很多团队做 AI 应用时,第一版都会把 prompt 写成一个长字符串。系统角色、业务规则、工具说明、JSON Schema、few-shot 示例、用户输入,全都拼在一起,然后直接发给模型。这个做法在 Demo 阶段没问题,但一旦进入真实业务流量,问题会很快暴露出来。
最明显的是成本。每一次请求都重复发送一大段稳定上下文,哪怕用户只改了一句话,系统仍然要重新处理完整 prompt。其次是延迟。上下文越长,首 token 越慢,工具调用链越长,用户越容易觉得产品卡。更隐蔽的是治理问题。prompt 改了一行,到底影响了哪些请求?缓存是否还有效?某个租户的权限规则更新后,旧缓存会不会继续命中?这些问题如果没有工程化设计,最后都会变成线上事故。
这篇文章想讲一个很具体的实践:不要把 Prompt Cache 当成简单的"请求结果缓存",而要把它当成 AI 应用的一层基础设施。它缓存的不是模型最终答案,而是 prompt 中可复用、可版本化、可观测的稳定片段。
一、为什么"整段 prompt 做 hash"通常不够
最容易想到的方案是把完整 prompt 做一次 hash,然后用 Redis 存模型返回。这个方案在 FAQ、固定问答、离线摘要里有价值,但在交互式 AI 应用里很快失效。
原因很简单,真实请求里有大量小变化:
- 用户输入每次都不同
- 当前时间、地域、语言偏好会变化
- 检索结果会变化
- 工具状态会变化
- 租户权限和业务配置会变化
- 输出格式可能按场景调整
只要其中任何一项变化,整段 hash 就完全不同。命中率低,缓存基本等于摆设。更糟糕的是,团队为了提高命中率,可能会把不该忽略的字段从 hash 中拿掉,最后引入权限串扰或脏数据。
所以,Prompt Cache 的关键不是"缓存整段 prompt",而是识别 prompt 里的稳定层和变化层。
二、把 prompt 拆成三层
我更推荐把 AI 应用里的 prompt 分成三层。
第一层是基础策略层。它包括系统身份、回答边界、安全策略、输出语言、格式约束。这一层通常跟应用版本绑定,变化频率最低。
第二层是业务知识层。它包括产品说明、工具列表、字段 schema、few-shot 示例、常见错误处理方式。这一层会随着业务迭代变化,但不是每个请求都变。
第三层是请求上下文层。它包括用户当前输入、检索片段、会话摘要、临时变量、工具返回结果。这一层变化最快,通常不适合直接做长期缓存。
工程上的目标,是让第一层和第二层尽可能稳定、可命中;第三层只参与最终调用,不污染稳定缓存。
可以把它想象成前端构建里的 chunk。系统规则像 vendor chunk,业务 schema 像 feature chunk,用户输入像 runtime data。你不会因为用户点了一个按钮就重新打包整个前端项目,同样也不应该因为用户改了一句话就让模型重新处理所有稳定上下文。
三、缓存键不是字符串拼接,而是版本协议
Prompt Cache 最容易出问题的地方是缓存键。一个安全的 key 至少应该包含这些维度:
- appId:哪个应用
- tenantId:哪个租户
- promptVersion:prompt 模板版本
- policyVersion:权限和安全策略版本
- schemaVersion:输出结构版本
- locale:语言和地区
- modelFamily:模型族或能力档位
- segmentHash:稳定片段内容 hash
注意,这里没有把 userId 放进所有 key。是否需要 userId,取决于缓存片段是否包含用户私有信息。系统规则和通用 schema 不应该按用户拆,否则命中率会很差。包含用户权限、用户画像、私有文档摘要的片段必须按用户或权限边界隔离。
一个常见错误是只使用 prompt 文本 hash。它看起来很干净,但缺少版本语义。比如权限策略没写进 prompt 文本,而是在服务端工具层执行;当权限策略升级时,旧缓存仍然可能命中。这种 bug 很难排查,因为从 prompt 文本看一切都没变。
四、一个最小实现
下面是一个简化的 TypeScript 风格实现。它不是完整框架,但足够表达核心设计。
ts
import crypto from "node:crypto";
type Segment = {
name: string;
content: string;
scope: "global" | "tenant" | "user";
version: string;
};
type CacheContext = {
appId: string;
tenantId: string;
userId?: string;
modelFamily: string;
policyVersion: string;
locale: string;
};
function sha256(input: string) {
return crypto.createHash("sha256").update(input).digest("hex").slice(0, 16);
}
function segmentKey(ctx: CacheContext, seg: Segment) {
const parts = [
"prompt-segment",
ctx.appId,
ctx.modelFamily,
ctx.locale,
`policy:${ctx.policyVersion}`,
`segment:${seg.name}`,
`v:${seg.version}`,
`h:${sha256(seg.content)}`,
];
if (seg.scope === "tenant") parts.push(`tenant:${ctx.tenantId}`);
if (seg.scope === "user") parts.push(`tenant:${ctx.tenantId}`, `user:${ctx.userId ?? "anonymous"}`);
return parts.join(":");
}
这段代码有两个重点。第一,内容 hash 和显式版本同时存在。hash 保证内容变化会失效,版本保证语义变化可以主动失效。第二,scope 明确表达隔离边界,避免所有缓存都粗暴地按用户拆,也避免用户私有信息进入全局缓存。
真实系统里,缓存值可以是预处理后的 prompt 片段、token 统计、压缩摘要、工具 schema 编译结果,或者某些模型供应商支持的 prefix cache 引用。不同模型能力不同,但上层的分段、版本和观测思路是通用的。
五、不要只看命中率
很多团队上线缓存后只看 hit rate,这是不够的。Prompt Cache 至少要看五类指标。
第一是 segment hit rate。整段请求命中率低不代表缓存没价值,稳定片段命中率高才是关键。
第二是 saved tokens。缓存命中节省了多少输入 token,这比命中次数更接近成本收益。
第三是 first token latency。用户感受到的是首 token 时间,不是服务端函数执行时间。
第四是 stale prevention。因为版本、权限、schema 变化导致的主动失效次数,要能被记录。
第五是 fallback rate。缓存系统异常时有多少请求回退到无缓存路径。如果 fallback 过高,说明缓存层不稳定;如果没有 fallback,说明缓存层可能变成单点故障。
可以用下面的结构记录每次调用。
ts
type PromptCacheTrace = {
requestId: string;
segments: Array<{
name: string;
scope: string;
hit: boolean;
inputTokens: number;
savedTokens: number;
version: string;
}>;
latencyMs: number;
firstTokenMs?: number;
fallback: boolean;
invalidationReason?: string;
};
这些 trace 不需要永久保存全文。相反,生产环境应该避免把用户 prompt 明文写进日志。记录 segment 名称、版本、hash、token 数和命中状态通常就够了。
六、一个可复现实验
为了避免只讲概念,我写了一个小实验。它不调用真实模型,而是模拟三种策略的成本和延迟:
- no-cache:每次都处理完整 prompt
- full-hash-cache:完整 prompt 相同才命中
- segment-cache:稳定片段可单独命中
js
const requests = Array.from({ length: 1000 }, (_, i) => ({
tenant: `t${i % 10}`,
schemaVersion: i < 700 ? "v1" : "v2",
userInput: `question-${i}`,
retrievedDoc: `doc-${i % 80}`,
}));
const baseTokens = 1800;
const schemaTokens = 900;
const dynamicTokens = 500;
function simulate(strategy) {
const cache = new Set();
let costTokens = 0;
let hits = 0;
for (const r of requests) {
if (strategy === "no-cache") {
costTokens += baseTokens + schemaTokens + dynamicTokens;
continue;
}
if (strategy === "full-hash-cache") {
const key = `${r.tenant}:${r.schemaVersion}:${r.userInput}:${r.retrievedDoc}`;
if (cache.has(key)) hits++;
else {
cache.add(key);
costTokens += baseTokens + schemaTokens + dynamicTokens;
}
continue;
}
if (strategy === "segment-cache") {
const baseKey = `base:${r.tenant}`;
const schemaKey = `schema:${r.schemaVersion}`;
if (cache.has(baseKey)) hits++; else { cache.add(baseKey); costTokens += baseTokens; }
if (cache.has(schemaKey)) hits++; else { cache.add(schemaKey); costTokens += schemaTokens; }
costTokens += dynamicTokens;
}
}
return { strategy, hits, costTokens };
}
console.table([
simulate("no-cache"),
simulate("full-hash-cache"),
simulate("segment-cache"),
]);
在这个模拟里,full-hash-cache 几乎没有收益,因为用户输入每次都不同。segment-cache 的收益明显,因为租户基础规则和 schema 版本大量复用。真实线上结果会受模型、请求结构和业务分布影响,但方向通常一致:缓存稳定片段,比赌整段请求重复更可靠。
这里要强调一点:这不是厂商价格 benchmark,也不是模型性能评测。它只是帮助团队验证自己的 prompt 结构是否具备复用性。真正上线前,应该在影子流量里记录 token、延迟和失效原因,再决定哪些片段值得缓存。
七、缓存失效要可运营
Prompt Cache 不是写完 key 就结束。线上更重要的是失效策略。
推荐至少保留三种失效方式。
第一,版本失效。发布新 prompt、新 schema、新权限规则时,直接提升版本号。这是最可控的方式。
第二,范围失效。当某个租户配置变化时,只清理 tenant scope 的缓存,不影响全局缓存。
第三,紧急熔断。当发现缓存污染或输出异常时,可以按 app、tenant、segment、modelFamily 快速关闭缓存。
不要只依赖 TTL。TTL 可以兜底,但不能表达业务语义。权限变更要求立即生效,不能等缓存自然过期。schema 发布也不应该靠"等十分钟"。
一个比较实用的控制面可以长这样:
json
{
"appId": "support-agent",
"cacheEnabled": true,
"disabledSegments": ["user-private-profile"],
"minPromptVersion": "2026-10-12.3",
"tenantOverrides": {
"tenant-a": {
"cacheEnabled": false,
"reason": "permission policy migration"
}
}
}
这类配置最好由服务端读取,而不是写死在应用代码里。AI 应用变化快,如果每次关缓存都要重新发版,事故处理会很被动。
八、常见坑
第一个坑是把检索结果长期缓存。RAG 场景里,检索结果往往和索引版本、权限、时间有关。可以缓存检索 query 的规范化结果,也可以缓存公共知识片段,但不要忽略文档权限和索引版本。
第二个坑是把工具返回值混进稳定层。工具结果通常代表当前状态,例如订单、库存、审批流、账户余额。它们应该是请求上下文,而不是全局 prompt 资产。
第三个坑是缓存了错误格式。模型输出 JSON 失败后,如果把失败结果缓存起来,后续请求会持续踩坑。对最终答案做缓存时,必须只缓存通过 schema 校验的结果。
第四个坑是没有区分"节省输入 token"和"节省端到端时间"。如果瓶颈在外部工具调用或检索,Prompt Cache 对端到端延迟帮助有限,但仍可能降低成本。指标要分开看。
第五个坑是日志泄露。为了排查命中率,把 prompt 全文打到日志里,看起来方便,实际上很危险。更好的做法是记录 hash、版本、token 数和抽样后的脱敏片段。
九、一份落地清单
如果你准备在团队里做 Prompt Cache,可以按这个顺序推进。
- 先把 prompt 拆成 segment,不急着上缓存。
- 给每个 segment 标注 scope、version、owner。
- 建立 key 协议,把租户、权限、schema、模型族写进去。
- 在影子流量里记录 token、延迟、命中候选和失效原因。
- 只缓存命中潜力高、权限边界清晰的稳定片段。
- 加 fallback,缓存异常不能影响主链路。
- 加控制面,支持按 app、tenant、segment 熔断。
- 发布后看 saved tokens、first token、fallback rate,而不只看 hit rate。
这套工作听起来比"加个 Redis"麻烦,但它换来的是可解释、可回滚、可治理。AI 应用进入生产后,prompt 会越来越像代码和配置的混合体。既然代码需要版本、测试、灰度和回滚,prompt 也应该有同等级别的工程待遇。
结语
Prompt Cache 的本质不是省一点 token,而是承认 prompt 已经成为 AI 应用的核心资产。把它继续当字符串拼接,团队会在成本、延迟和治理上反复吃亏。把它拆成稳定片段,建立版本协议、隔离边界和观测指标,AI 应用才有机会从 Demo 走向可靠产品。
如果只能带走一句话,那就是:不要缓存"看起来一样的请求",而要缓存"语义上稳定、边界上安全、版本上可控的 prompt 资产"。