语义缓存按含义不按字符串:向量匹配减半 API 调用成本
语义缓存将查询向量化并用余弦相似度检索,相同语义的不同表述共享缓存,直接降低 LLM 调用成本。
语义缓存将查询向量化并用余弦相似度检索,相同语义的不同表述共享缓存,直接降低 LLM 调用成本。
普通缓存以精确字符串作为 key,因此,“How do I reset my password?”和“I forgot my password, how can I change it?”会被视为两个不同的请求,两次都会调用模型——尽管它们的答案完全相同。Semantic caching 则以语义作为 key:它会把传入的查询嵌入为向量,计算其与先前已回答查询之间的余弦相似度;如果最相近的查询超过阈值,就直接返回缓存的答案——也就是一次 HIT——而不再调用模型,从而同时降低成本和延迟。我构建了一个实时缓存,可以在你输入时对每条缓存记录进行评分。下面是它的工作原理。
Embedding 会把文本映射为向量,让语义相近的内容在向量空间中彼此靠近。真实系统会调用 embedding 模型;这个演示则使用了一个极小的、确定性的概念词袋:转成小写、删除停用词,再把同义词归并为同一个 token。这样无需联网,也足以让不同的改写表达产生重叠。
const SYN = { forgot:'reset', change:'reset', delivery:'shipping', price:'cost' };
function embed(text){
const v = new Set();
for (const w of text.toLowerCase().match(/[a-z]{2,}/g) || [])
if (!STOP.has(w)) v.add(SYN[w] || w); // synonyms collapse to one concept
return v; // a sparse meaning vector
}
使用两个 embedding 之间夹角的余弦值进行比较:1.0 表示语义相同,0 表示毫无共同之处。先对查询进行 embedding,找到与它最相似的缓存记录,只有当相似度超过阈值时才返回该记录。高于阈值 → HIT(不调用模型);低于阈值 → MISS(调用模型并插入新的 Q→A,这样下一次遇到类似改写时就能命中)。
let best = null, bestSim = -1;
for (const e of cache){ const s = cosine(q, e.embedding);
if (s > bestSim){ bestSim = s; best = e; } } // nearest neighbour
return bestSim >= threshold ? { hit:true, answer:best.answer } : { hit:false };
阈值设得太高,几乎相同的问题也会 MISS,导致你支付不必要的费用。阈值设得太低,只因共享一两个单词,本不相关的问题也可能发生碰撞,于是系统会信心十足地返回错误的缓存答案——也就是 false hit。拿“What is the capital of Germany?”去匹配一份客服 FAQ 缓存,就是典型的陷阱。应该使用带标签的数据集选择阈值,在 false hit 为零的前提下尽可能提高命中率;同时保留一个灰色区间,当系统不确定时,宁愿付费调用模型,也不要冒险返回错误答案。
if (sim >= SERVE) return 'HIT'; // 0.83: confidently reuse
if (sim < CALL) return 'MISS'; // 0.60: clearly new -> call model
return 'MISS_SAFE'; // grey zone: pay rather than risk a wrong answer
每避免一次模型调用,就能实实在在地省下费用,并拿回原本消耗的延迟。这个演示中的玩具模型估算每次命中大约可节省 0.0021 美元、约 900 毫秒和约 380 个 token;如果命中率能稳定在 60%,那么对于重复问题,推理账单大约也能减少 60%。而且随着缓存逐渐预热,命中率只会继续上升。
if (hit){ hits++; costSaved += 0.0021; msSaved += 900; }
const hitRate = hits / queries; // rises as traffic repeats
因为 Semantic caching 按语义匹配,所以它能够识别不同的改写表达——这正是它与 prompt caching(第 31 天)的区别。prompt caching 只能复用完全相同的共享前缀所对应的 prefill/KV。两者可以叠加使用:Semantic caching 在 HIT 时会完全跳过模型调用,而 prompt caching 则可以降低那些仍然需要执行的模型调用成本。
在生产环境中,你需要把演示用的玩具 embedder 换成真正的模型,并将线性扫描替换为 ANN 向量索引;同时加入 TTL 和 LRU 淘汰机制,让价格、营业时间、政策等可能过期的答案及时失效——而且绝不能缓存个性化回复或对时间敏感的回复。
我现在牢牢记住的结论是:真实流量中,很大一部分其实只是少数几个问题的不同说法,所以不要为同一个问题付两次钱——缓存语义,使用经过调优的阈值进行门控,然后看着推理账单中的一大块成本消失。
嵌入一个问题,看它如何通过余弦相似度与缓存匹配,再拖动阈值,观察如何在命中与 false hit 之间进行权衡:https://dev48v.infy.uk/ai/days/day49-semantic-caching.html
如需采取进一步措施,你可以考虑屏蔽此人和/或举报滥用行为。