RAG 的单次问答成本不难算:嵌入一次查询(便宜)、检索一次(几乎免费)、rerank 一次(小贵)、生成一次(大头)。重复问题在真实流量里占比惊人——企业知识库的查询日志里,「报销怎么走」这类问题每天被问几十遍,措辞还各不相同(「报销流程」「费用怎么报」「差旅费如何报销」)。如果每次都跑完整条流水线,就是为同一个答案反复付钱、反复付延迟。语义缓存(semantic cache)在最前面加一道判断: incoming 查询与历史查询语义相似就直接返回缓存答案,命中一次,整条链路全免。这篇讲它的实现要点与那些让它从省钱利器变成事故源的坑。

缓存键的三层设计
语义缓存的难点全在「什么算同一个问题」。工程上分三层,从保守到激进。
第一层:精确缓存。 查询规范化(去空白、统一大小写、去标点)后做字符串键,命中即返回。零风险,但命中率天然有限——用户措辞千变万化,字符串相等的重复问法占比通常不到两成。它是所有语义缓存的必备地基。
第二层:语义缓存。 对查询做嵌入,与缓存索引里的历史查询向量算相似度,超过阈值即视为同一问题,返回其答案。缓存索引本身就是一个小的向量库(几千到几万条历史查询,暴力扫描足够快)。这一层贡献主要的命中率提升,也是全部风险所在——语义相似不等于答案等价,下一节细说。
第三层:答案级缓存(进阶)。 不缓存「查询-答案」对,缓存「检索结果」:语义相似的查询往往命中同一批文档块,缓存检索层结果、生成仍然现算。它把风险降了一档(生成总基于当下模型与当下 prompt 模板),但省的钱也少了(生成的大头没省)。适合模型或 prompt 频繁迭代的系统。
相似阈值的调法与边界 case
语义缓存的成败系于阈值一个参数,而且没有放之四海的默认值——不同嵌入模型的相似度分布不同,必须用真实查询校准。方法是把相似缓存明确拆成两个动作:先判断「等价」,再决定「缓存」。等价判断可以不只是相似度数字:阈值粗筛后,让一个小模型做一次等价性确认(「这两个问题会得到相同答案吗」),粗筛阈值可以放宽(宁多勿漏),确认层兜住误判。多一道确认多几十毫秒与一点成本,换来的是误命中率的数量级下降,多数生产系统值得。
哪些查询永远不该命中缓存,清单要写死在缓存层:时间敏感类(「今天的汇率」「现在几点」——答案随时间变)、带个性化参数的(「我的订单」「我们部门的政策」——答案因人而异,《权限感知检索》的权限维度必须进缓存键)、长尾复杂查询(复合问题相似度低但歧义高,缓存收益小风险大)。实现上就是缓存层的「禁缓存」前置规则,规则判不出来的再走语义判断。
失效:文档更新了,缓存里的答案就错了
缓存答案在「知识库更新」的那一刻开始变质。用户上周问「报销比例」缓存了答案,这周财务部更新了制度文档,缓存的旧答案继续被返回——缓存把 RAG 的实时性优势缓存没了。失效机制因此是语义缓存的生死线,设计要点:建立「缓存条目 → 依赖的 chunk id」的反向索引——生成答案时用了哪些块,都记录在案;文档更新触发块级联重建(《增量更新与模型迁移》)时,顺藤摸瓜把依赖这些块(或同源文档)的缓存条目全部失效。做不到 chunk 级追踪的退化方案是时间窗 TTL:缓存条目只活几小时到一天,用命中率换新鲜度——简单粗暴但在高频更新的语料上常常是唯一安全解。
def answer_with_cache(query, user_ctx, pipeline, cache):
if banned(query): # 时间敏感/个性化黑名单
return pipeline.run(query, user_ctx)
exact = cache.get_exact(normalize(query), scope=user_ctx.scope)
if exact:
return exact
hit = cache.search_semantic(embed(query), threshold=0.94,
scope=user_ctx.scope, verify=True)
if hit: # 粗筛+等价确认,且带权限 scope
return hit.answer
ans = pipeline.run(query, user_ctx)
cache.put(embed(query), query, ans, deps=ans.used_chunk_ids, scope=user_ctx.scope)
return ans
注意代码里 scope 参数:缓存键必须包含权限/租户维度。两个权限不同的用户问同一个问题,可见文档不同,答案理应不同——缓存若不分 scope,A 部门员工可能拿到按 B 部门权限生成的答案。这是语义缓存事故排行榜第一名,没有之一。
阈值标定的具体步骤
「阈值用真实查询校准」落到操作上是四步。第一步,抽样:从查询日志抽 2000 条(覆盖高频与长尾)。第二步,聚类分组:全量算嵌入,按相似度聚类(阈值粗设 0.9),人工快速过一遍每组——组内问题「会得到相同答案」的记为真重复组。第三步,画分布:真重复组内的两两相似度分布 vs 非重复组间的分布,两条曲线的间隙就是阈值的安全区间。第四步,留出带验证:按选定阈值跑「等价确认」层,统计它的拦截率与误放率,把确认层的判例(被拦的相似对)存档——它们是后续调阈值和改进确认 prompt 的最有价值数据。整个流程半天到一天的工作量,比上线后调阈值试错的成本低得多。另有一个常被漏掉的参数:scope 的粒度(缓存按租户分?按部门分?按用户分?)——粒度越细命中率越低、越安全;把 scope 粒度和阈值一起标定,两个旋钮别分开拧。
流式场景下的部分缓存
答案走流式输出(打字机效果)时,语义缓存有个体验细节要处理:命中缓存直接返回完整答案,未命中走流水线流式生成——两者混在一个接口里,前端要能处理「整段返回」与「流式返回」两种形态。更细的做法是部分缓存:缓存答案的同时缓存它的检索结果部分(命中语义缓存的查询只缓存到检索层,生成仍现跑流式)——省了检索与重排的延迟(几百毫秒到一秒),保留了流式的即时感,代价是省不掉生成成本。适合「延迟敏感、成本不敏感」的场景(面向外部客户的客服);反过来内部工具「成本敏感、延迟不敏感」就全量缓存直返。两种形态按流量性质选,别一刀切。
缓存层的监控指标
语义缓存要进监控的四个数:命中率(按日趋势——突降常对应查询分布变化或失效机制误杀,突升要警惕禁缓存规则漏网);等价确认的拦截率(粗筛命中但确认层拦下的比例——高企说明阈值过松,这是误答风险的前兆信号);失效量(因文档更新被清的条目数——与内容更新频率对照,比例异常说明依赖追踪有漏);缓存答案的负反馈率 vs 未缓存答案的负反馈率(两者对比是缓存质量的终极裁判——缓存答案的负反馈显著更高,说明缓存在系统性供给过期或错配的答案,立即收紧阈值并审计禁缓存规则)。四个指标里最后一个最硬:缓存的目的的省钱,但如果省钱的代价是答案质量下滑,它就是在用信任换成本——这笔账必须有人盯着。
收益怎么算:先测重复率再动工
上语义缓存前先回答一个问题:你的查询重复率够高吗。方法:攒两周真实查询,两两算嵌入相似度(抽样即可),统计「超阈值且等价确认通过」的比例。经验上,客服类、内部 IT 支持、政策问答场景的重复率可达三到五成,缓存收益巨大;开放域、探索型的查询流(研究助手类产品)重复率可能不足一成,缓存投入产出比尴尬。命中率乘以单次流水线成本就是每天省下的钱,再乘以平均省下的延迟就是体验收益——两个数字算完再决定动不动工。
分层部署
缓存的部署形态按流量规模分两档。单实例内存缓存:日查询量万级以下,一个带嵌入相似度检索的内存结构(几万条目暴力扫描毫秒级)就够,部署零成本。独立缓存服务:多实例部署或命中率要求高时,缓存独立成服务(向量库或专用缓存组件承载),所有应用实例共享——共享是重点,各实例各存一份语义缓存,命中率互相稀释还可能返回不一致的答案。独立服务要注意两点:延迟预算——缓存查询本身要走一次网络往返加相似度计算,预算控制在 10ms 量级,超过就失去了「比流水线快」的意义;失效广播——文档更新触发的缓存失效要广播到所有实例(发布订阅或版本号轮询),单实例失效是缓存一致性的经典死角。
与多轮对话的交互
语义缓存和多轮对话的配合有一个结构性错位要处理:多轮里的追问天然不重复——「那它呢」的改写结果依赖具体会话,语义缓存对这类查询的命中率近乎为零,硬要缓存反而污染缓存库(存进大量一次性查询)。分工方案:单轮 FAQ 型查询走答案缓存(「报销比例是多少」这类独立完整的问题,是缓存的主战场);多轮追问走检索缓存(缓存「改写后查询 → 检索片段」的映射,片段可复用性高于答案——同主题追问命中同一批文档的概率远高于同一答案)。实现上在缓存入口加一个路由信号:查询理解层输出「是否独立完整」(多轮场景下 condense 的副产物),独立查询进答案缓存,依赖会话的查询进检索缓存或不缓存。缓存与多轮的边界划清,两边的命中率统计才有意义。
小结
语义缓存的定位一句话:它省的不是模型的钱,是「重复劳动」的钱。三层缓存键(精确打底、语义主攻、等价确认兜底)、三条禁缓存红线(时间敏感、个性化、权限维度必进键)、一条失效生死线(chunk 级依赖追踪,退而求 TTL),账面收益用真实查询的重复率预估。做对了它是成本结构里最便宜的杠杆之一;做错了(特别是漏了权限 scope),它是生产事故的稳定供应商。
参考资料
- GPTCache(GitHub: zilliztech/GPTCache) —— 开源语义缓存的实现参考(嵌入相似度 + 缓存管理)
- 本系列前篇:《增量更新与模型迁移》《权限感知检索》《RAG 的成本账》《查询路由》
读者留言
COMMENTS 暂无还没有留言,来说第一句?