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 怎么定阈值
没有银弹。建议:
- 起步 0.95:大多数场景下这是安全和收益的平衡点
- 准备一个评测集:收集 100-200 对「应该是相同意思」和「应该不是相同意思」的 prompt 对
- 跑测试:在不同阈值下看命中率和误命中率
- 按场景调整:
- 客服问答(事实类):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 影响、流式响应缓存等。