跳到主要内容

一次缓存策略误判带来的延迟问题

· 阅读需 5 分钟
一介布衣
全栈开发者 / 技术写作者

补档说明:本文属于「AI 工程落地周记」系列,计划发布时间为 2025-08-30 10:20。当前先保留为草稿,后续补充真实案例、代码片段和复盘细节后再发布。

很多人做 AI 服务优化时,一看到延迟上来,第一反应就是“加缓存”。这当然不是错,但问题在于:缓存并不是天然减延迟,它只是在某些命中条件下减延迟。

前阵子我就遇到过一次很典型的反效果。团队本来是想给一条 AI 处理链降一点时延,结果缓存加上去之后,p95 反而更高了。

后来排查下来,问题不是 Redis 慢,也不是代码写挂了,而是我们缓存错了层。

当时做了什么

那条链路大概是:

  1. 拉用户上下文
  2. 检索知识片段
  3. 拼 Prompt
  4. 调模型生成结果

为了减少重复生成,我们当时把“最终模型输出”做了缓存,cache key 里塞了很多维度:

  • 用户 ID
  • 会话 ID
  • 最近对话摘要
  • 检索文档 ID 列表
  • Prompt 版本
  • 模型版本

从逻辑上看很严谨,因为这样最不容易误命中。
问题是,它也几乎保证了命中率不会高。

指标是怎么暴露问题的

上线后那几天,我们看到一组很别扭的数据:

缓存命中率      7%
平均响应时间 +180ms
p95 +620ms
Redis 请求量 明显上升
模型调用次数 几乎没怎么下降

这组数字已经很说明问题了:

  • 命中率太低,缓存收益很少
  • 每次请求还要先构建复杂 key、查一次缓存
  • miss 之后照样走原始完整链路

也就是说,缓存层不是在帮我们省掉重计算,而是在大多数请求上多加了一段前置开销。

根因不是“缓存没用”,而是“缓存对象选错了”

后来我们把链路拆开看,发现真正相对稳定、适合缓存的根本不是最终答案,而是中间层:

1. 检索结果比最终答案更稳定

同一类问题在短时间内,相关文档集合往往比最终生成文本更可复用。

2. Prompt 模板渲染结果比个性化回答更稳定

如果最终输出强依赖用户上下文、会话状态和临时目标,那它天然更难复用。

3. 高个性化结果强行缓存,只会让 key 爆炸

key 一旦包含太多高变维度,缓存就只是一个“低命中数据库”,不是性能优化层。

我后来怎么改

我们把缓存从“最终输出层”挪到了“相对稳定的中间层”,大致变成这样:

const retrievalCacheKey = [
'retrieval',
queryNormalization(userQuery),
knowledgeBaseVersion,
].join(':')

const promptFragmentCacheKey = [
'policy-fragment',
policyVersion,
locale,
].join(':')

最终回答不再强求直接缓存,而是缓存:

  • 查询归一化后的检索结果
  • 稳定的规则片段
  • 一些高复用的系统模板

这之后,延迟下降就明显自然多了,因为我们终于在复用“真的会重复出现的东西”。

一个经常被忽略的副作用:缓存也会带来串行等待

还有一个后来才看清的点是,缓存层本身也会改变请求时序。

例如你在冷启动 miss 时:

  1. 先算 key
  2. 先查 Redis
  3. miss 后再走原链路
  4. 最后还要回写缓存

如果这一步做得很重,本质上就是在主链路前面多串了一个同步步骤。
所以不是所有缓存都该放在请求主路径上。

我现在判断“该不该缓存”先问三个问题

  1. 这个对象在短时间内真的会重复出现吗?
  2. 它的 key 维度会不会多到几乎无法命中?
  3. miss 时额外开销是否足够小?

只要这里有两个答案不乐观,我就不会轻易把它放到主请求缓存里。

一个更稳的经验

缓存最适合的不是“最贵的结果”,而是“最稳定、最可复用的中间产物”。

很多团队本能会去缓存最贵的那一步,也就是模型输出。但在 AI 业务里,最贵不等于最适合缓存。
越接近用户个体、越依赖当前会话的结果,越容易命中困难、收益有限。

总结

这次缓存策略误判带来的延迟问题,最后让我更确定了一件事:缓存优化不是把一切昂贵步骤都包起来,而是挑出真正重复、真正稳定、真正值得复用的层。

缓存加对了,是在删掉重复劳动;缓存加错了,就是在主链路前面再挂一个低命中的开销节点。