18.05-语义缓存

要点

  • 确定性缓存只匹配「字面完全相同」的 prompt——「什么是 Cloudflare」和「介绍一下 Cloudflare」会被当成两个不同的请求
  • 语义缓存用 embedding 的向量相似度做匹配——意思相同就能命中,不受字面差异影响
  • 实现思路:把 prompt 的 embedding 存进 Vectorize,新请求先查向量索引,相似度超过阈值就返回缓存的答案
  • 阈值是关键参数——太高(0.98)命中率低,太低(0.85)会返回错误答案。一般 0.92-0.95 起步
  • AI Gateway(第 19 篇)自带语义缓存功能,可以零代码开启
  • 语义缓存不适合需要精确性的场景(代码生成、数字计算)——相似但不同的 prompt 可能需要完全不同的答案

1. 为什么需要语义缓存

上一篇讲了确定性缓存——用 prompt hash 做 key,完全相同才能命中。这有个明显的局限:

用户 A:「什么是 Cloudflare」
用户 B:「介绍一下 Cloudflare」
用户 C:「Cloudflare 是做什么的」

这三个问题意思几乎一样,但字面完全不同。确定性缓存会算出三个不同的 hash,全部 miss,调三次 LLM。

语义缓存能识别「这三个问题是同一个意思」——用 embedding 的向量距离做匹配,相似度超过阈值就命中。

2. 工作原理

语义缓存分两个阶段:

2.1 写入阶段

prompt → embedding → 存进 Vectorize,同时把 prompt 和答案存进 KV
async function storeSemanticCache(
  env: { AI: Ai; CACHE_INDEX: Vectorize; CACHE_KV: KVNamespace },
  prompt: string,
  answer: string
) {
  // 1. 生成 prompt 的 embedding
  const { data } = await env.AI.run('@cf/baai/bge-base-en-v1.5', {
    text: [prompt],
  })
  const embedding = data[0]
 
  // 2. 生成唯一 ID
  const id = crypto.randomUUID()
 
  // 3. 把 embedding 存进 Vectorize
  await env.CACHE_INDEX.upsert([{
    id,
    values: embedding,
    metadata: { prompt },  // 把原文也存进去,便于后续排查
  }])
 
  // 4. 把答案存进 KV,key 用 Vectorize 的 ID
  await env.CACHE_KV.put(`ans:${id}`, answer, {
    expirationTtl: 86400,  // 24 小时过期
  })
}

2.2 查询阶段

新 prompt → embedding → 查 Vectorize → 最高相似度 > 阈值?→ 返回缓存答案
async function semanticLookup(
  env: { AI: Ai; CACHE_INDEX: Vectorize; CACHE_KV: KVNamespace },
  prompt: string,
  threshold = 0.95
): Promise<{ hit: boolean; answer?: string; score?: number }> {
  // 1. 生成新 prompt 的 embedding
  const { data } = await env.AI.run('@cf/baai/bge-base-en-v1.5', {
    text: [prompt],
  })
 
  // 2. 查向量索引
  const result = await env.CACHE_INDEX.query(data[0], { topK: 1 })
 
  if (result.matches.length === 0) {
    return { hit: false }
  }
 
  const topMatch = result.matches[0]
 
  // 3. 相似度不够,不算命中
  if (topMatch.score < threshold) {
    return { hit: false, score: topMatch.score }
  }
 
  // 4. 命中,从 KV 取答案
  const answer = await env.CACHE_KV.get(`ans:${topMatch.id}`)
  if (!answer) {
    // Vectorize 里有但 KV 里没有(可能过期了),删除 Vectorize 记录
    await env.CACHE_INDEX.delete([topMatch.id])
    return { hit: false }
  }
 
  return { hit: true, answer, score: topMatch.score }
}

3. 整合到 AI 查询接口

把语义缓存包进 LLM 调用:

// src/lib/semantic-llm.ts
export async function callLLMWithSemanticCache(
  env: Env,
  prompt: string
): Promise<{ answer: string; source: 'cache' | 'llm'; score?: number }> {
  // 1. 先查语义缓存
  const lookup = await semanticLookup(env, prompt, 0.95)
 
  if (lookup.hit && lookup.answer) {
    return { answer: lookup.answer, source: 'cache', score: lookup.score }
  }
 
  // 2. 没命中,调 LLM
  const result = await env.AI.run('@cf/meta/llama-3.1-8b-instruct', {
    messages: [{ role: 'user', content: prompt }],
  })
  const answer = (result as { response: string }).response
 
  // 3. 写入语义缓存(异步,不阻塞返回)
  env.waitUntil(storeSemanticCache(env, prompt, answer))
 
  return { answer, source: 'llm' }
}

在 Hono 路由里使用:

app.post('/api/ai/query', async (c) => {
  const { prompt } = await c.req.json()
 
  const result = await callLLMWithSemanticCache(c.env, prompt)
 
  return c.json({
    answer: result.answer,
    source: result.source,  // 告诉前端是缓存还是 LLM
    score: result.score,    // 调试用:相似度
  })
})

4. 阈值调优

阈值是语义缓存最关键的参数。

4.1 阈值太高(比如 0.98)

  • 命中率低——几乎只有字面完全相同才命中
  • 失去了语义匹配的意义
  • 几乎退化成确定性缓存

4.2 阈值太低(比如 0.85)

  • 命中率高,但会把「意思不同但相似」的 prompt 当成相同
  • 例如:「怎么取消订单」和「怎么创建订单」相似度可能 0.88,但答案完全相反
  • 用户拿到错误答案,体验极差

4.3 怎么定阈值

没有银弹。建议:

  1. 起步 0.95:大多数场景下这是安全和收益的平衡点
  2. 准备一个评测集:收集 100-200 对「应该是相同意思」和「应该不是相同意思」的 prompt 对
  3. 跑测试:在不同阈值下看命中率和误命中率
  4. 按场景调整
    • 客服问答(事实类):0.92-0.95
    • 代码生成:0.97-0.98(甚至不用语义缓存)
    • 闲聊:0.90 可以接受(答案多样性要求低)

4.4 评测示例

// 评测脚本
const testCases = [
  { a: '什么是 Cloudflare', b: '介绍一下 Cloudflare', shouldMatch: true },
  { a: '怎么取消订单', b: '怎么创建订单', shouldMatch: false },
  { a: 'Python 怎么读文件', b: 'Python 读取 txt 文件的方法', shouldMatch: true },
  { a: '北京今天天气', b: '北京明天天气', shouldMatch: false },
]
 
async function evaluateThreshold(threshold: number) {
  let correct = 0
  for (const { a, b, shouldMatch } of testCases) {
    const { hit, score } = await semanticLookup(env, b, threshold)
    const matched = score !== undefined && score >= threshold
    if (matched === shouldMatch) correct++
  }
  return correct / testCases.length
}

跑多个阈值,找准确率最高的那个。

5. 语义缓存 vs 确定性缓存

维度确定性缓存语义缓存
匹配方式prompt hash 完全相同embedding 向量相似度
延迟0.01ms(KV 读取)10-50ms(embedding + 向量查询)
命中率低(字面必须相同)高(意思相同即可)
准确性高(完全匹配)依赖阈值
成本高(每次查询要算 embedding)
实现复杂度中(需要 Vectorize + KV)

选择建议:

  • 高精确场景(代码生成、数字计算):用确定性缓存或不用
  • 事实问答场景(客服、知识库):用语义缓存
  • 两者结合:先查确定性缓存(快),miss 了再查语义缓存(慢但全)
async function callLLMSmart(env: Env, prompt: string) {
  // L1:确定性缓存(hash 匹配)
  const exact = await exactCacheLookup(env, prompt)
  if (exact.hit) return { answer: exact.answer, source: 'exact-cache' }
 
  // L2:语义缓存(相似度匹配)
  const semantic = await semanticLookup(env, prompt, 0.95)
  if (semantic.hit) return { answer: semantic.answer, source: 'semantic-cache' }
 
  // L3:调 LLM
  return { answer: await callLLM(env, prompt), source: 'llm' }
}

6. AI Gateway 的语义缓存

如果你用了 Cloudflare AI Gateway(第 19 篇),它自带语义缓存功能,不需要自己实现:

// 用 AI Gateway 的语义缓存
const gateway = env.AI.gateway('my-gateway')
 
const result = await gateway.run(
  '@cf/meta/llama-3.1-8b-instruct',
  { messages: [{ role: 'user', content: prompt }] },
  {
    gateway: {
      semanticCache: {
        enabled: true,
        threshold: 0.95,  // 相似度阈值
        maxTTL: 86400,    // 缓存 24 小时
      },
    },
  }
)

AI Gateway 自动处理 embedding 计算、向量存储、相似度查询。你只需要开启开关。

优势:

  • 零代码实现
  • 自动调优(AI Gateway 会根据使用数据优化)
  • 统一的监控面板

劣势:

  • 不如自实现灵活(阈值策略、缓存失效等)
  • 依赖第三方服务

7. 风险与注意事项

7.1 过时答案

语义缓存的 24 小时 TTL 可能让过时答案被反复返回。例如:

  • 用户问「今天天气」→ LLM 回答「晴天」
  • 24 小时内另一个用户问「天气怎么样」→ 命中缓存,还是返回「晴天」
  • 但外面已经下雨了

防御:对时间敏感的查询不要用缓存,或者缩短 TTL。

7.2 上下文丢失

多轮对话不适合用语义缓存——每一轮的 prompt 依赖前面的上下文。单独缓存一轮会破坏对话连贯性。

变通:把多轮对话的「摘要」做缓存,而不是单轮。

7.3 缓存污染

恶意用户可以故意构造相似但不同的 prompt,让缓存存进错误答案,然后其他用户命中这些错误答案。

防御:

  • 限制单用户写入缓存的频率
  • 对低置信度(score 接近阈值)的命中做二次验证
  • 定期清理低质量缓存

8. 小结

语义缓存用 embedding 的向量相似度做匹配——意思相同就能命中,不受字面差异影响。实现需要 Vectorize(存 embedding)+ KV(存答案),阈值 0.95 起步,按场景调整。

跟确定性缓存的对比:

  • 确定性:快、准、命中率低
  • 语义:慢一点、略不准、命中率高

两者可以叠着用——先查确定性缓存(快),miss 了再查语义缓存(慢但全)。

AI Gateway 自带语义缓存,零代码开启。自实现更灵活但复杂。

风险:过时答案、上下文丢失、缓存污染。按场景评估是否值得。

下一篇讲 AI 响应缓存——更具体的 LLM 响应缓存策略,包括 prompt hash、temperature 影响、流式响应缓存等。