语义缓存实战:相似度阈值、错答风险与命中率对账
结论先放这儿
LLM 按次计费,但用户问的问题高度重复。精确缓存只认一模一样的输入,改个标点就失效,命中率惨不忍睹。语义缓存把问题转成向量,用相似度判断命中,能接住大量换说法的重复提问,真金白银地省钱。但它带来一个新风险,相似的问题被返回了不合适的答案。这篇讲清楚阈值怎么定、什么场景敢用。
精确缓存与语义缓存的差别
| 维度 | 精确缓存 | 语义缓存 |
|---|---|---|
| 命中条件 | 输入完全一致 | 相似度超过阈值 |
| 命中率 | 低,改一个字就 miss | 高,同义改写也能命中 |
| 主要风险 | 几乎没有 | 相似但意图不同的问题被错答 |
| 额外成本 | 几乎为零 | embedding 调用 + 向量库运维 |
精确缓存是免费的保险。语义缓存是真金白银换命中率,但风险也随之而来。
一个最小可用的实现
import time class SemanticCache: def __init__(self, threshold=0.92, ttl=600): self.items = [] # (vec, question, answer, ts) self.threshold = threshold self.ttl = ttl def lookup(self, vec): now = time.time() self.items = [it for it in self.items if now - it[3] < self.ttl] best, best_sim = None, 0.0 for it in self.items: sim = cosine(vec, it[0]) if sim > best_sim: best, best_sim = it, sim if best is not None and best_sim >= self.threshold: return best[2] return None def put(self, question, vec, answer): self.items.append((embed_vec(question), question, answer, time.time()))工程上别自己造向量库,用 FAISS 或者带向量插件的 Redis 都行。这段代码只是把逻辑摊开,命中就返回旧答案,未命中就调模型并写回缓存,TTL 保证知识更新后旧答案会过期。
阈值怎么定
阈值是拿风险换命中率。
- 阈值 0.97,接住的基本是原句复述,命中率趋近精确缓存,但错答概率也趋近于零
- 阈值 0.92,能接住大量改写,错答率开始爬升
- 阈值 0.85,命中率漂亮,但「退货流程」和「退款政策」这类相似句会互相串
建议用真实业务问题跑一遍评测集,画出命中率与错答率的曲线,再选拐点附近的值。
什么场景敢用
并不是所有场景都值得上语义缓存。客服寒暄答错了没人在意,价格、库存、合规类问题答错就是事故。常见的折中是白名单机制,涉及敏感字段的问题不走语义缓存,直接透传给模型。
写在最后
落地顺序建议这样,先上精确缓存,几乎零成本零风险;命中率不够,再上语义缓存;上了语义缓存,就配一套抽检机制盯着错答率。缓存省的是真金白银,但答错一次的代价,可能抵得上几百次命中省下来的钱。把错答率也纳入监控口径,才算真正把语义缓存用明白。