Skip to content

目标:用“可复用的键设计 + 分层缓存 + 新鲜度控制”提升命中率,并确保一致性可控。

🎯 文章目标

  • 给出 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 的实践文章

💭 总结

  • 用“正确的键 + 分层 + 新鲜度控制 + 观测”,把缓存收益具体化、可控化