目标:用“可复用的键设计 + 分层缓存 + 新鲜度控制”提升命中率,并确保一致性可控。
🎯 文章目标
- 给出 LLM 响应缓存的最小可行方案(键、TTL、分层)
- 提供工程实现样例(Redis + 语义/精确混合)
- 输出观测指标与常见坑
📚 背景/前置
- 缓存类型:
- 客户端/边缘(CDN)/网关/服务内/向量检索层
- 业务特性:
- AIGC 响应非确定性、提示稍变即不同;需“归一化 + 预算驱动”的缓存策略
🔧 核心内容
1) 键设计(Key Design)
- 精确键(Deterministic):hash(model, system, tools, prompt_norm, params)
- 维度建议:
- model + version、temperature/top_p、工具清单/顺序、系统提示,prompt 归一化(去空白/数值统一/排序占位)
- 命名示例:llm:resp:v1:{model}:
2) 分层缓存
- L1:进程内(LRU,容量小,命中快)
- L2:Redis(主缓存,带 TTL 与标签)
- L3:边缘/CDN(GET 场景、只读接口)
3) 新鲜度与一致性
- TTL:按任务与模型区分(如 extract 1h、chat 10m、plan 2m)
- Stale-While-Revalidate(SWR):过期后先回旧值,后台刷新
- 失效策略:知识库更新、模型版本切换、敏感策略变更要主动失效(tag/版本号)
4) 语义缓存(可选)
- 对 prompt 做 embedding,相似度>阈值命中缓存;配合 exact 缓存作为二级命中
- 风险:语义近似≠需求相同;适用于“问答/检索答复”而非“代码/结构化生成”
💡 实战示例:Node.js + Redis
javascript
import crypto from 'crypto';
import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL);
function normalizePrompt(p){
return p.trim().replace(/\s+/g,' ').slice(0, 8000);
}
function cacheKey({model, system, tools, prompt, params}){
const payload = JSON.stringify({model, system, tools, prompt: normalizePrompt(prompt), params});
const sha = crypto.createHash('sha256').update(payload).digest('hex');
return `llm:resp:v1:\${model}:\${sha}`;
}
async function getOrGen(ctx, gen){
const key = cacheKey(ctx);
const ttl = ctx.type==='extract'?3600: (ctx.type==='chat'?600:120);
const cached = await redis.get(key);
if (cached) return { from:'cache', data: JSON.parse(cached) };
const out = await gen();
// 附带元数据:tokens/成本/模型/时间
const value = JSON.stringify({ ...out, meta: ctx.meta });
await redis.set(key, value, 'EX', ttl);
return { from:'origin', data: JSON.parse(value) };
}
失效:按标签/版本
javascript
// 写入时同时记录标签集合,便于批量失效
await redis.set(key, value, 'EX', ttl);
await redis.sadd(`llm:tag:\${ctx.kbVersion}`, key);
// 知识库更新后:
const keys = await redis.smembers(`llm:tag:\${newVersion}`);
if (keys.length) await redis.del(keys);
📊 指标看板(速查)
- 命中率(overall/L1/L2)、平均节省 tokens/费用
- 新鲜度:stale 命中占比、回源率、SWR 时延
- 失效效果:标签失效的覆盖率与耗时
- 误命中/不一致事件:来源、频次、恢复时长
🧪 踩坑与经验
- 键未包含 system/tools/params:轻微变更导致“伪命中”
- 忘记归一化 prompt:空白/排序变动导致大量未命中
- 相似度阈值过低:语义缓存产生错误答案
- 未记录 tokens/费用:难以证明收益
📎 参考与延伸
- Cache invalidation strategies, Stale-While-Revalidate
- 语义缓存:embedding + ANN 的实践文章
💭 总结
- 用“正确的键 + 分层 + 新鲜度控制 + 观测”,把缓存收益具体化、可控化