一次缓存策略误判带来的延迟问题
补档说明:本文属于「AI 工程落地周记」系列,计划发布时间为 2025-08-30 10:20。当前先保留为草稿,后续补充真实案例、代码片段和复盘细节后再发布。
很多人做 AI 服务优化时,一看到延迟上来,第一反应就是“加缓存”。这当然不是错,但问题在于:缓存并不是天然减延迟,它只是在某些命中条件下减延迟。
前阵子我就遇到过一次很典型的反效果。团队本来是想给一条 AI 处理链降一点时延,结果缓存加上去之后,p95 反而更高了。
后来排查下来,问题不是 Redis 慢,也不是代码写挂了,而是我们缓存错了层。
当时做了什么
那条链路大概是:
- 拉用户上下文
- 检索知识片段
- 拼 Prompt
- 调模型生成结果
为了减少重复生成,我们当时把“最终模型输出”做了缓存,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 时:
- 先算 key
- 先查 Redis
- miss 后再走原链路
- 最后还要回写缓存
如果这一步做得很重,本质上就是在主链路前面多串了一个同步步骤。
所以不是所有缓存都该放在请求主路径上。
我现在判断“该不该缓存”先问三个问题
- 这个对象在短时间内真的会重复出现吗?
- 它的 key 维度会不会多到几乎无法命中?
- miss 时额外开销是否足够小?
只要这里有两个答案不乐观,我就不会轻易把它放到主请求缓存里。
一个更稳的经验
缓存最适合的不是“最贵的结果”,而是“最稳定、最可复用的中间产物”。
很多团队本能会去缓存最贵的那一步,也就是模型输出。但在 AI 业务里,最贵不等于最适合缓存。
越接近用户个体、越依赖当前会话的结果,越容易命中困难、收益有限。
总结
这次缓存策略误判带来的延迟问题,最后让我更确定了一件事:缓存优化不是把一切昂贵步骤都包起来,而是挑出真正重复、真正稳定、真正值得复用的层。
缓存加对了,是在删掉重复劳动;缓存加错了,就是在主链路前面再挂一个低命中的开销节点。
