2522 字
约 8 分钟
12
S20-A27 · 语义缓存:为什么"同样的问题"两次答案不一样

上了语义缓存之后,报表很好看:命中率 62 %,模型调用成本降了 40 %。两周后客服开始收到奇怪投诉:"同一个问题,上午问和下午问,答案不一样";更麻烦的是有人发现"问 2025 版的退款政策,回答的是 2024 版"。

这两类投诉指向同一个根因:语义缓存把"精确匹配"换成了"概率匹配"。精确缓存的错误只有一种——miss(没命中,回源)。语义缓存的错误多了一种——错命中:它认为两个问题"意思差不多",于是把上一题的答案原样给出。而"意思差不多"这个判断,本身就是一个模型输出的近似值。


1. key 从字符串变成了向量

维度 精确缓存 语义缓存
key 归一化后的字符串哈希 问题向量 + 相似度阈值
命中判定 完全相等 cos(θ) ≥ 阈值
错误类型 只有 miss 错命中(最危险)
失效单位 单个 key 一个邻域(近似问题一片)
命中率 低(措辞变化就 miss) 高(措辞自由)

第四行是很多人没算到的账:精确缓存失效时清掉一条;语义缓存里一条答案可能服务几十个"近似问题",一条错了就是一片错了。

// 一次典型的错命中路径
缓存中: "2024 年退款政策是什么" → 答案 A(7 天无理由)
用户问: "2025 年退款政策是什么" → 向量相似度 0.97(只差一个年份数字)
命中 → 返回答案 A  ← 措辞几乎相同,实体不同
// 相似度模型对"年份数字"不敏感,对"句式"很敏感

2. 阈值是一条交换曲线,不是调参

阈值越高,命中率越低、正确率越高;反之亦然。关键是这条曲线的拐点在哪里,以及拐点之后错命中的代价能不能承受。

阈值 命中率(经验值) 错命中率 适用
0.95 20–30 % 很低 强合规、不能出错
0.90 45–55 % 低(需实体校验) 大多数业务
0.85 60–70 % 中(同义不同实体高发) 低风险问答
0.80 75 %+ 高 不建议
// 只看命中率会误导,三个数要一起看
$ curl -s localhost:9000/metrics | grep -E 'cache_hit|cache_miss|entity_reject'
// cache_hit 62%  cache_miss 28%  entity_reject 10%   ← 第二道闸挡掉的那部分
// 若 entity_reject 接近 0 而投诉在涨,说明错命中根本没被拦住

第二行是实践中的常用起点,但它必须配实体校验才安全。第三行就是"命中率 62 % 但那两类投诉"的位置——数字好看,代价转移到了客服。


3. 三类错命中,判据在"实体是否一致"

类型 例子 修法
同义不同实体 2024 版 vs 2025 版;A 产品 vs B 产品 实体校验:抽取关键实体必须相等
时间敏感 "现在几点开门"、"今天有什么活动" 不缓存,或极短 TTL
权限不同 员工问 vs 客户问同一个问题 权限维度进 key
个性化 "我的订单到哪了" 完全不缓存

第一行的修法很具体:在相似度之外,再跑一次轻量实体比对——把问题里的数字、版本号、产品名、地区抽出来,要求完全一致才允许命中。成本远低于一次模型调用,但能挡掉绝大多数错命中。

// 两道闸:向量相似度 + 关键实体一致
def cache_lookup(q):
    v = embed(q)
    cand = index.search(v, k=3, threshold=0.88)
    for c in cand:
        if extract_entities(c.question) == extract_entities(q):   // 第二道闸
            return c.answer
    return None                                                   // 回源
// 判据:实体抽取宁可漏,不可错——漏了只是多花一次调用

4. 什么不该进语义缓存

内容 为什么 处理
含时间敏感词(今天/现在/最新) 答案会过期 正则拦截 + 不缓存
含用户私有数据 越权泄露 用户维度进 key 或整体禁用
有副作用的回答("已帮你下单") 重放有副作用 只缓存"信息类"回答
高度个性化(我的、我的订单) 无法跨用户复用 单用户缓存
结论依赖实时数据 立刻过期 极短 TTL 或不缓存

第二行是安全事故级的问题:如果缓存 key 只有问题向量,员工问"我的社保基数"与客户问同一句话会命中同一条答案。权限维度必须进 key,而不是只在检索层过滤——这是两个不同的地方(S20-A22 讲的是检索的权限过滤,缓存是另一处漏点)。

// 缓存 key 的最小组成(少一项就是事故)
semantic_cache:
  key: [question_vector, tenant_id, user_role, locale, policy_version]
  ttl: 3600                 // 与知识库更新周期对齐
  entity_check: true        // 数字/版本/产品名必须相等
  deny_patterns: ["今天", "现在", "最新", "我的"]

5. 失效:为什么 TTL 是唯一可靠的

知识库更新之后,问题向量没变,但正确答案变了。语义缓存的 key 是"问题",它感知不到答案侧的变化。

失效手段 能否感知知识更新 代价
TTL 能(延迟 = TTL) 一段时间内仍答旧内容
知识版本号进 key 能(立即) 更新时缓存全失效(命中率骤降)
答案溯源 → 主动清理 能(需索引) 工程量大
不做失效 不能 长期答旧内容
// 版本切换时的两种策略(同一时间轴)
策略一 版本进 key + 立即全失效:命中率 62% → 3%,成本回到原样
策略二 版本进 key + 旧缓存留到 TTL:命中率 62% → 55% 缓慢回落
// 两种都能保证"新问题用新答案",差别只在旧答案还服务多久

第二行是"立即正确但代价高"的方案:知识库版本一升,全部 key 换空间,等于缓存清空。实践中常见折中:版本号进 key + 旧版本缓存保留到 TTL 自然过期,允许老用户继续命中旧版本一段时间——但这必须是有意识的决定,而不是忘了处理。


6. 排错:五种"上了缓存更糟"

现象 八成原因 处理方向
同问题两次答案不一致 中间发生过回源刷新 缓存写入带 TTL 与版本
问 A 答 B(不同版本/产品) 阈值过低,无实体校验 加实体比对
命中率很高但投诉变多 错命中被当成功 降阈值前先上第二道闸
用户看到别人的数据 权限维度没进 key key 加 tenant/role
知识更新后仍答旧内容 无失效机制 版本号进 key + TTL

第三行是本篇的核心判断:命中率不是这个机制的成功指标。真正要看的是"命中且正确"的比例,以及"错命中导致的返工/投诉"成本——这两个数往往在报表里缺席。


7. 边界:什么情况不值得上语义缓存

场景 为什么不必 替代
QPS 低、调用成本占比小 省下的钱不够维护成本 直接调用
问题高度个性化 命中率天然极低 会话级缓存
答案必须实时 缓存必然过期 实时查询 + 预计算
合规要求可追溯 缓存答案来源模糊 每次都走 RAG 全链路
问题集合很小且固定 精确缓存就够 精确 key

判断口径一句话:当"错命中的代价"能被业务吸收时,语义缓存才成立——信息类问答可以,政策/价格/额度类要加实体校验,涉及权限与资金的干脆不用。而"命中率"只能作为观测值,不能作为验收目标。


8. 自测

  1. 语义缓存与精确缓存的错误类型差别是什么?为什么前者更危险?
  2. 阈值提高会同时改变哪两个指标?拐点之后代价转移到哪里?
  3. "2024 版 vs 2025 版"属于哪类错命中?两道闸的设计是什么?
  4. 为什么权限维度必须进缓存 key,而不是只在检索层过滤?
  5. 知识库更新后,为什么 TTL 是唯一可靠手段?"版本号进 key"的代价是什么?
  6. 什么判据能让你决定"这个场景不该上语义缓存"?

9. 一句话总结

语义缓存把"精确匹配"换成了"概率匹配",于是多出一种错误:错命中。它的安全边界由两道闸决定——阈值(≥0.9)+ 关键实体一致(数字/版本/产品名);而权限、租户、locale 必须进 key,否则是越权事故。失效只能靠"版本号进 key + TTL",因为 key 感知不到答案侧的变化。判据:命中率只是观测值,"命中且正确"才是验收口径;错命中代价吸收不了的场景,不该上这个机制。

S20-A27 · 语义缓存:为什么"同样的问题"两次答案不一样
https://www.clxhxhhr.top/posts/6736/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。