如果你做过一个真正面向用户的 AI 应用,大概率见过这样的路线图。
第一周接入模型,Demo 很惊艳。第二周接知识库,开始做 RAG。第三周加重排、加摘要、加多轮记忆、加工具调用。一个月后,系统看起来很完整,用户却开始抱怨三件事。
响应慢。
有时候回答像失忆。
账单涨得比 DAU 还快。
这时候团队通常会继续补 RAG。检索不准,那就换 embedding。召回不够,那就加多路召回。上下文塞不下,那就做更复杂的分块。最后工程复杂度上去了,但增长指标未必改善。
我的判断更直接一点:很多 AI 应用早期不该先把 RAG 做重,而应该先把上下文缓存做起来。
这里的上下文缓存不是简单的 HTTP cache,也不是把模型回答存在 Redis 里糊弄用户。它是一层专门服务 AI 应用的工程机制,目标是把重复的背景、稳定的业务规则、已验证的会话状态、常用检索结果沉淀下来,让每次模型调用少做重复劳动。
增长视角下,它解决的不是一个后端优化问题,而是 AI 产品的第一性问题:用户能不能更快、更稳定、更便宜地得到一个可信答案。
一、为什么很多团队一上来就把 AI 应用做重了
RAG 是一个很容易让人上瘾的词。
它听起来像完整方案。知识进向量库,问题进检索器,结果进 prompt,模型再生成答案。架构图好看,模块也清楚。对老板汇报时,它比"我们先做缓存"更像一个正经 AI 项目。
但真实业务里,用户请求并不都是"需要检索新知识"的问题。
以一个客服 Copilot 为例,用户每天会问这些东西。
- 你们的退款政策是什么
- 这个订单为什么不能退
- 帮我写一段安抚客户的话
- 按我们的品牌语气重新说一遍
- 这个客户是 VIP,应该怎么处理
- 上次我已经说明过背景了,继续
这些请求里,只有一部分需要实时检索知识库。更多部分是在重复使用稳定上下文:产品政策、客服口径、品牌语气、工具说明、用户权限、历史摘要、工单状态。
如果每次都把这些内容重新拼进 prompt,再让模型重新读一遍,你实际上是在为同一份背景反复付费,也在让用户反复等待。
这就是 AI 应用早期最容易被忽视的浪费:不是模型太贵,而是你把同一个上下文当成一次性耗材。
传统 Web 应用里,我们不会每次请求都重新计算配置、重新加载用户画像、重新查一遍不变的字典表。可到了大模型应用,很多团队又把这些基本功忘了。
RAG 负责"找新东西"。上下文缓存负责"不要重复解释旧东西"。这两个问题不是一回事。
二、增长瓶颈往往不是 RAG 缺席,而是上下文重复劳动
一个 AI 应用增长不起来,通常不会只因为"知识库没接好"。更常见的瓶颈有四类。
第一,首 token 慢。用户点击发送后,界面停在那里几秒没有反馈。即使最终答案不错,体验也已经变差。
第二,结果不稳定。同样的问题,今天回答 A,明天回答 B。不是因为业务变了,而是 prompt 组装、历史摘要、检索结果每次都有细微漂移。
第三,成本不可预测。某些大客户、长会话或复杂页面会突然把上下文拉爆,单次调用成本失控。
第四,调试困难。线上出了问题,团队不知道是检索错、prompt 错、历史压缩错,还是模型本身发散。
上下文缓存能直接打到这四个点。
它让稳定前缀不必重复处理,让常见问题不必每次完整生成,让会话状态可追踪,让检索结果可以复用并被审计。更重要的是,它把 AI 应用从"每次临场发挥"推向"有可复用的工作台"。
增长指标里,这会表现为更低的等待时间、更高的完成率、更少的人工兜底、更稳的毛利率。
不要小看这些指标。对很多 AI 产品来说,用户不是因为你没有 Agent 框架而流失,而是因为第一次打开就慢、第二次回答变了、第三次额度用完。
三、四类上下文缓存,分别解决不同问题
我建议把 AI 应用的缓存拆成四层,而不是混在一个 Redis key 里。
1. Prefix Cache:缓存稳定前缀
稳定前缀包括系统提示词、角色设定、工具协议、输出格式、品牌语气、固定政策摘要、产品说明等。
这些内容的特点是改动不频繁,但几乎每次调用都要带上。它们非常适合做前缀缓存。
不少模型服务已经提供 prompt caching 或 context caching 相关能力。不同厂商的细节不一样,有的自动识别重复前缀,有的需要显式声明缓存边界,有的有 TTL。工程上不必绑定某一家实现,关键是设计 prompt 时要让稳定内容形成可复用的前缀。
一个常见错误是把动态内容插在系统提示词中间。
txt
你是客服助手。
当前用户是:{{user_name}}
你必须遵守以下退款政策:...
当前订单是:{{order_id}}
请按照以下品牌语气回答:...
这样稳定内容被动态字段切碎,缓存命中会变差。更好的结构是先放稳定前缀,再放动态上下文。
txt
[稳定前缀]
你是客服助手。
你必须遵守以下退款政策:...
请按照以下品牌语气回答:...
工具调用协议如下:...
[动态上下文]
当前用户:{{user_name}}
当前订单:{{order_id}}
本轮问题:{{question}}
这不是 prompt 洁癖,而是成本设计。
2. Semantic Response Cache:缓存常见意图的答案骨架
很多用户问题表面不同,意图相同。
"怎么退款""退货流程在哪""买错了能退吗"可能都落到同一个退款流程解释。完全复用最终答案有风险,但复用答案骨架很有价值。
语义响应缓存可以存三类东西。
- 标准答案模板
- 已验证的引用材料
- 生成时使用的约束和格式
命中后,模型不必从零开始生成,只需要结合当前用户状态做轻量改写。
这里要注意,不建议把所有自然语言回答都永久缓存。AI 应用的缓存如果不带版本、权限、来源和过期策略,很快会变成脏答案仓库。
3. Session Memory Cache:缓存会话状态
多轮对话最怕两件事。一件是把完整历史无限塞回 prompt,另一件是粗暴摘要后丢掉关键约束。
会话记忆缓存应该保存结构化状态,而不是只保存一段"对话摘要"。例如:
json
{
"user_goal": "为企业客户解释退款失败原因",
"constraints": ["语气安抚", "不要承诺人工无法确认的赔付"],
"known_facts": {
"order_status": "超过可退款期限",
"customer_tier": "VIP"
},
"open_questions": ["是否有特殊审批权限"],
"last_verified_at": "2026-10-14T08:03:00+08:00"
}
这类缓存让模型每轮都能拿到稳定状态,而不是在一长串聊天记录里猜重点。
4. Retrieval Result Cache:缓存检索结果
这层最容易被忽略。
很多团队会缓存 embedding 或最终回答,却不缓存检索结果。结果是同一个问题、同一个用户、同一个知识库版本,每次都重新召回、重排、截断。
检索结果缓存可以按以下维度建 key。
txt
tenant_id + permission_scope + normalized_query + knowledge_version + retriever_version
只要知识库版本和权限范围没有变化,常见问题的召回结果就可以短时间复用。这样不仅省延迟,也方便排查"为什么今天召回了这三段"。
四、什么时候用缓存,什么时候用 RAG
缓存不是 RAG 的替代品。更准确地说,缓存应该站在 RAG 前面。
我会用一个简单判断。
如果问题依赖稳定规则、固定口径、用户会话状态、常见意图,先考虑缓存。
如果问题依赖频繁更新的事实、长尾文档、权限隔离知识、需要引用来源,使用 RAG。
如果问题既有稳定规则又有实时事实,先缓存稳定部分,再对动态事实做检索。
用表格看更清楚。
| 场景 | 更适合缓存 | 更适合 RAG |
|---|---|---|
| 系统提示词、工具协议 | 是 | 否 |
| 品牌语气、输出格式 | 是 | 否 |
| 退款政策稳定摘要 | 是 | 视更新频率而定 |
| 每天变化的活动规则 | 短 TTL 缓存 | 是 |
| 多租户权限文档 | 只缓存权限内结果 | 是 |
| 用户会话目标和约束 | 是 | 否 |
| 长尾产品手册问答 | 可缓存高频召回 | 是 |
| 需要可追溯引用的事实 | 缓存检索结果 | 是 |
很多团队的问题是把所有东西都送进 RAG,最后 RAG 既要负责找资料,又要负责记状态,还要负责保持语气。这会让系统越来越重。
更干净的分工是:缓存负责复用确定性上下文,RAG 负责获取外部知识,模型负责推理和表达。
五、一个可运行的最小实现
下面给一个简化版 Node.js 示例。它不是生产级框架,但足够说明工程边界。
我们实现四件事。
- 稳定前缀按版本缓存
- 语义问题做规范化
- 检索结果按知识库版本缓存
- 会话状态结构化保存
js
import crypto from "node:crypto";
class MemoryCache {
constructor() {
this.store = new Map();
}
get(key) {
const item = this.store.get(key);
if (!item) return null;
if (item.expiresAt && item.expiresAt < Date.now()) {
this.store.delete(key);
return null;
}
return item.value;
}
set(key, value, ttlMs) {
this.store.set(key, {
value,
expiresAt: ttlMs ? Date.now() + ttlMs : null
});
}
}
const cache = new MemoryCache();
function sha(input) {
return crypto.createHash("sha256").update(input).digest("hex").slice(0, 16);
}
function normalizeQuestion(q) {
return q
.toLowerCase()
.replace(/订单号[::]?\s*\w+/g, "订单号")
.replace(/\s+/g, " ")
.trim();
}
function buildStablePrefix({ policyVersion, toneVersion }) {
const key = `prefix:${policyVersion}:${toneVersion}`;
const hit = cache.get(key);
if (hit) return { text: hit, hit: true };
const text = [
"你是一个客服 Copilot。",
"回答必须准确、克制、可执行。",
`退款政策版本:${policyVersion}`,
"超过退款期限时,不得承诺自动退款。",
`品牌语气版本:${toneVersion}`,
"语气要清楚、安抚、避免指责用户。",
"输出结构:先解释原因,再给下一步建议。"
].join("\n");
cache.set(key, text, 24 * 60 * 60 * 1000);
return { text, hit: false };
}
async function fakeRetrieve(query, { tenantId, permissionScope, knowledgeVersion }) {
const normalized = normalizeQuestion(query);
const key = `retrieval:${tenantId}:${permissionScope}:${knowledgeVersion}:${sha(normalized)}`;
const hit = cache.get(key);
if (hit) return { chunks: hit, hit: true };
const chunks = [
{
id: "refund-policy-7d",
text: "普通订单支持签收后 7 天内申请退款,超过期限需人工审批。"
},
{
id: "vip-escalation",
text: "VIP 客户可转交高级客服复核,但不得承诺必然退款。"
}
];
cache.set(key, chunks, 10 * 60 * 1000);
return { chunks, hit: false };
}
function getSessionState(sessionId) {
return cache.get(`session:${sessionId}`) || {
user_goal: null,
constraints: [],
known_facts: {},
open_questions: []
};
}
function saveSessionState(sessionId, state) {
cache.set(`session:${sessionId}`, {
...state,
last_verified_at: new Date().toISOString()
}, 2 * 60 * 60 * 1000);
}
async function buildPrompt(input) {
const prefix = buildStablePrefix({
policyVersion: input.policyVersion,
toneVersion: input.toneVersion
});
const session = getSessionState(input.sessionId);
const retrieval = await fakeRetrieve(input.question, {
tenantId: input.tenantId,
permissionScope: input.permissionScope,
knowledgeVersion: input.knowledgeVersion
});
const prompt = [
prefix.text,
"\n[会话状态]",
JSON.stringify(session, null, 2),
"\n[检索材料]",
retrieval.chunks.map(c => `- ${c.id}: ${c.text}`).join("\n"),
"\n[用户问题]",
input.question
].join("\n");
return {
prompt,
cacheMeta: {
prefixHit: prefix.hit,
retrievalHit: retrieval.hit
}
};
}
const input = {
tenantId: "shop-a",
permissionScope: "support:vip",
sessionId: "s-10086",
question: "这个 VIP 客户订单号 A123 超过 7 天了,还能退款吗?",
policyVersion: "refund-2026-10",
toneVersion: "brand-calm-v3",
knowledgeVersion: "kb-2026-10-14"
};
const first = await buildPrompt(input);
console.log("first call", first.cacheMeta);
saveSessionState(input.sessionId, {
user_goal: "解释 VIP 客户超过退款期限后的处理方式",
constraints: ["不得承诺必然退款", "给出升级复核路径"],
known_facts: { order_status: "超过 7 天退款期", customer_tier: "VIP" },
open_questions: ["是否存在质量问题凭证"]
});
const second = await buildPrompt(input);
console.log("second call", second.cacheMeta);
运行后,第二次调用会命中稳定前缀和检索结果。真实生产环境里,你可以把 MemoryCache 换成 Redis,把 fakeRetrieve 换成向量检索加重排,把缓存命中率和延迟写进观测系统。
这个例子真正想表达的是:缓存不是一个单点优化,而是一种上下文治理方式。
六、该看哪些指标
如果你要把上下文缓存作为增长飞轮,不要只看服务端 QPS。
我建议至少看七个指标。
第一,TTFT,也就是 first token 出来的时间。用户体感最明显。
第二,总响应时间。尤其是 P75 和 P95,不要只看平均值。
第三,输入 token 数。稳定前缀、会话历史、检索材料分别占多少,要拆开看。
第四,缓存命中率。prefix、semantic response、session memory、retrieval result 要分层统计。
第五,命中后的质量差异。缓存命中不能只带来便宜,还要保证答案不变差。
第六,人工兜底率。客服、销售、运营类 Copilot 最应该看这个指标。
第七,单位成功任务成本。不要只算单次调用成本,要算"完成一个用户目标"花了多少钱。
一个简单的埋点结构可以这样设计。
json
{
"request_id": "req_001",
"user_id": "u_123",
"task_type": "refund_support",
"latency_ms": 1840,
"ttft_ms": 420,
"input_tokens": 3200,
"output_tokens": 480,
"cache": {
"prefix_hit": true,
"semantic_hit": false,
"session_hit": true,
"retrieval_hit": true
},
"quality": {
"user_accepted": true,
"human_escalated": false
}
}
有了这些数据,你才能回答一个增长问题:缓存到底是在帮用户更快完成任务,还是只是让服务端看起来更省钱。
七、几个容易踩的坑
第一,把缓存当知识库。
缓存是复用机制,不是事实来源。事实来源仍然应该是数据库、文档系统、知识库或业务服务。缓存里的内容必须能追溯来源和版本。
第二,跨用户复用敏感上下文。
AI 应用里最危险的缓存 bug 不是缓存没命中,而是命中了不该命中的内容。key 里必须包含租户、权限、知识版本等边界。
第三,不做失效策略。
政策变了、活动结束了、权限改了,缓存必须能失效。否则你会得到一个非常自信但过期的 AI。
第四,只缓存最终回答。
最终回答最容易受用户语气、当前状态和权限影响。更稳的做法是缓存中间上下文:稳定前缀、召回结果、答案骨架、结构化会话状态。
第五,不记录缓存参与了哪次生成。
线上排查时,你需要知道某个回答命中了哪些缓存、缓存版本是什么、对应材料是什么。没有这些记录,缓存会变成黑盒。
八、缓存优先,不代表不要 RAG
我不是在唱衰 RAG。恰恰相反,好的 RAG 很重要。
但 RAG 应该处理它擅长的问题:从大量外部知识里找到与当前问题相关、可追溯、权限正确的材料。它不应该承担所有上下文管理工作。
如果你把角色设定、品牌语气、用户状态、常见问题、固定政策、历史摘要都塞给 RAG,系统会越来越慢,也越来越难调。
更好的路线是:先把稳定上下文缓存住,让每次调用变轻;再把真正需要检索的动态知识交给 RAG;最后用评估系统判断质量。
这条路线对增长更友好。因为它不会等到知识库完美才产生收益。只要你识别出高频稳定上下文,就可以立刻降低延迟和成本,提升一致性。
九、一个实践顺序
如果你的团队现在已经有一个 AI 应用,但增长数据一般,我建议按这个顺序改。
第一步,拆 prompt。把稳定前缀和动态上下文分开,避免动态字段打断可复用前缀。
第二步,记录 token 结构。每次调用里,系统提示词、历史、检索材料、用户输入分别占多少。
第三步,做 prefix cache。先缓存系统提示词、工具协议、输出格式、品牌语气。
第四步,做 retrieval result cache。对高频问题,在权限和知识库版本不变时复用召回结果。
第五步,做结构化 session memory。不要让模型每次从完整聊天记录里找重点。
第六步,再考虑语义响应缓存。只对高频、低风险、容易验证的意图做。
第七步,建立缓存评估。命中率、延迟、成本、质量、人工兜底率一起看。
这个顺序的好处是,每一步都能独立上线,风险也可控。
十、结尾:先把飞轮做轻,再把 RAG 做准
AI 应用工程有一个很朴素的规律:系统越早进入真实用户场景,越应该先优化稳定性和反馈速度,而不是先追求架构完整。
RAG 是重要能力,但它不是所有问题的起点。对很多产品来说,第一块增长飞轮应该是上下文缓存。
它让用户少等一点,让回答稳一点,让成本低一点,让工程团队更容易知道问题出在哪里。
等这层飞轮转起来,再去做更复杂的检索、重排、Agent 编排,才不会把一个本来可以轻量增长的产品,做成一个昂贵又迟钝的系统。
如果你正在做 AI 应用,不妨先问自己一个问题:
我们每次模型调用里,有多少内容其实昨天就已经解释过了?
答案通常会比想象中多。