ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

RAG 加语义缓存后整体准确率没掉,但时间限定类问题悄悄掉了 9 个点

RAG 加语义缓存后整体准确率没掉,但时间限定类问题悄悄掉了 9 个点 现象均值不动分档掉 9 个点给 RAG 加语义缓存最容易被盯上的指标是「缓存命中率」。命中率越高看起来越省钱越快。但真正的问题在于命中率高不高和命中得对不对是两件事。在一批「随时间变化」的问题里价格、版本、政策把缓存打开、相似度阈值设到 0.9整体 answer_correctness 几乎不动——因为大部分问题问的是静态知识缓存命中与否都答得对。但把问题按「是否带时间/版本限定词」分档之后带了限定词的那一档掉了 9 个点。查询「2026 年价格」命中了缓存里的「2026 年价格」条目而那条缓存是旧数据写入的。答案读起来完全通顺引用也齐就是年份对不上。这类失败不会出现在整体均值里也不会被 faithfulness 抓住——因为答案确实「忠于」它检索到的上下文只是那段上下文已经过期。复现固定一批会变的问题分档看正确率要复现这个现象关键不是把缓存接上跑一遍而是先把数据集按「时效性」切档。数据集30 题hash 1385126cffeaexamples/demo_rag/dataset.jsonl用来做对照的基线运行是 20260827T094242Z被测系统旋钮top_k3prompt_stylegrounded裁判模型deepseek-chat对照组同一批题关缓存 / 开缓存各跑一遍分档方式按问题里是否含时间、版本、政策编号这类限定词切成「静态档」和「时效档」下面这张表是四个真实运行在核心指标上的表现可以直接看出「整体均值」和「分档」的差别有多大运行top_kprompt_stylefaithfulnessanswer_relevancyanswer_correctness20260827T094242Z3grounded0.95560.73860.816420260921T005917Z1grounded0.93060.76630.819820260921T010853Z3weak0.69320.76260.627420260827T095152Z1weak0.32570.77350.3925看第一行和第三行的对比top_k 从 3 掉到 1、prompt_style 从 grounded 换到 weakanswer_correctness 从 0.8164 掉到 0.6274faithfulness 从 0.9556 掉到 0.6932。这说明这套评测对「检索变弱」和「提示词变弱」是有反应的不是钝的。问题就出在这里缓存陈旧导致的错误在这套指标下不会产生这种量级的下降。因为检索本身没变差上下文还是被正确召回的只是召回的那条上下文是旧版本。机制embedding 不带时间语义为什么「2026 年价格」会命中「2026 年价格」的旧缓存语义缓存的键是 query 的 embedding。embedding 模型这台机器上本地 RAG 用的是 bge-small-zh-v1.5学的是语义相似度不是事实时效性。「2026 年价格」和「2026 年价格」的向量几乎重合余弦相似度远超 0.9。年份这个 token 在向量里占的权重远小于「价格」这个实词。缓存键如果只由 query 文本生成就没有任何地方记录「这条答案是什么时候、基于哪版数据算出来的」。所以命中的不是「相同问题」而是「语义上像、但数据版本不同的问题」。命中率越高这种错配被放大的机会越多。更隐蔽的一点这类答案的 faithfulness 往往很高。答案确实引用了它拿到的上下文句子级支撑度也过得去。用规则归因去看句级支撑判定是这样的def sentence_supported( sentence: str, contexts: list[str], threshold: float 0.35支撑度判定只比较「答案句子」和「给定上下文」的文本重叠不比较「上下文」和「当前事实」是否一致。上下文本身过期规则归因看不出来。代码把「陈旧命中」单列成一个指标缓存键要带上限定词。这是最直接的修复从 query 里抽出时间、版本、政策编号拼进缓存键让「2026 年价格」和「2026 年价格」落到不同的键上。配套要做的是把「陈旧命中率」做成一个独立指标而不是塞进 answer_correctness 里等它自然暴露。做法是对每一条缓存命中用当前数据源重新算一遍答案和缓存返回的答案做比对不一致的计入陈旧命中。文本比对可以直接复用规则归因里的字符 n-gram 打分def ngram_overlap(a: str, b: str, n: int 2) - float: 字符 n-gram 重合率用于证据链的文本支撑判断。 grams_a _char_ngrams(a, n) grams_b _char_ngrams(b, n) if not grams_a: return 1.0 if a b else 0.0 return len(grams_a grams_b) / len(grams_a)中文答案要先切句再比对切句函数在这里def split_sentences_zh(text: str) - list[str]: 按中文句读切分句子过滤过短片段与列表编号等噪声。 parts re.split(r[。\n]|(?[.!?])\s, text) sentences: list[str] [] for part in parts: cleaned re.sub( r^\s*[\d一二三四五六七八九十][.、):]?\s*, , part ).strip() # 只保留包含汉字/字母/数字的句子过滤 1. 这类编号残片 if cleaned and re.search(r[\u4e00-\u9fffA-Za-z0-9], cleaned): sentences.append(cleaned) return sentences有了这两块陈旧命中可以按句算缓存答案的某一句在当前数据源里找不到支撑ngram_overlap 低于阈值就记一次陈旧命中。工程落地CI 该卡什么不要只卡整体 answer_correctness。缓存相关的改动CI 至少要卡三件事缓存键必须包含从 query 抽出的时间/版本限定词。可以写一条单测构造「2026 年价格」和「2026 年价格」两条 query断言缓存键不相等。TTL 按数据变更频率设而不是统一设一个常数。价格类数据按天变TTL 就该按天法规原文按年变TTL 可以长得多。把陈旧命中率作为独立指标接入回归报告阈值设成 0 容忍或接近 0。回归检测可以直接用项目里现成的配对比较函数。它做的是配对 t 分布置信区间加 Cohens dz能告诉你差异是显著退化还是噪声def compare_runs( current: dict[str, Any], baseline: dict[str, Any], metrics: list[str], alpha: float 0.05, min_effect_size: float 0.1, min_paired_samples: int 5,参考框架自带报告里的判定口径faithfulness 从 0.956 到 0.32695%CI [-0.746, -0.514]Cohens dz -2.032p0.000配对 30 题判定显著退化answer_correctness 从 0.816 到 0.39395%CI [-0.526, -0.321]Cohens dz -1.544p0.000判定显著退化。反过来说如果加缓存之后整体 answer_correctness 只是轻微波动、p 值不显著不能直接判定「无影响」。要像上面那样分档看尤其是时效档。还有一个容易踩的坑归因器的 UNKNOWN 标签不等于系统拒答。真实审计里运行 20260827T094242Z 归因器判 UNKNOWN 3 条其中并非拒答的 3 条20260921T005917Z 判 UNKNOWN 2 条其中并非拒答的 2 条。别把 UNKNOWN 当成拒答率去读。拒答口径本身也会影响均值。运行 20260827T095152Z 里识别出拒答 1 条作答率 96.7%把拒答样本的分数替换成 1.0 重算answer_correctness 从 0.3925 变成 0.42虚高 0.0275faithfulness 从 0.3257 变成 0.3513虚高 0.0256。也就是说拒答算对会虚高均值这一点在缓存评测里同样要留意。边界什么时候可以放宽阈值语义缓存不是不能用是要分场景设阈值。静态知识库收益大、陈旧风险低。文档原文、法规条文这类内容变更频率极低缓存命中基本就是命中阈值可以放宽TTL 也可以设得很长。时效性数据必须收紧。价格、版本、政策编号这类问题缓存键必须带限定词TTL 必须短且要单独监控陈旧命中率。混合库要分开治理。同一个缓存实例里既有静态文档又有实时价格是最危险的组合——静态部分的命中率会把整体命中率拉高掩盖时效部分的错配。一句话缓存命中率是个效率指标不是质量指标。把它当质量指标用时效档的退化会一直藏着。想要一份免费质量体检把你的测试集或评测脚本发我我按上面的指标跑一遍告诉你哪几项其实是假通过。私信我。
返回列表