RAG 阈值固定在了绝对分值上,而 Embedding 升级却悄然改变了分布
一个 RAG 流水线在发布时设置了 0.4 的重排序器(reranker)分数阈值。任何低于该分数的片段都会从提示词(prompt)中被剔除。半年后,一次常规的索引重建将嵌入模型(embedding model)更换为同系列的一个更新版本——变更日志称这是一次透明的升级。两天后,回答的相关性下降了 6%。团队将责任归咎于 LLM,进行了模型对比测试(bake-off),却发现没有任何候选模型能挽回损失,结果花了一个季度去追踪一个根本不存在于他们所对比模型中的回归问题。
回归问题就出在门控(gate)环节。重排序器——未做变动,相同的模型版本(checkpoint),相同的权重——现在正在对一组不同的候选集进行评分。新的嵌入模型将不同的数据块(chunks)拉入前 50 名,重排序器根据自身的校准对它们给出了更低的分数,而 0.4 的门控剔除的候选者比前一周增加了 37%。0.4 这个数字没变,但 0.4 所代表的含义变了。
只有输入保持不变,常量才成其为常 量
阈值是一种耦合。当你写下 if score > 0.4 时,你是在断言 score 的分布足够稳定,以至于 0.4 将继续代表你最初调整它时的含义。只要 score 上游的任何环节都没有变化,这个断言就成立。
在一个两阶段检索流水线中,score 是由交叉编码重排序器(cross-encoder reranker)产生的。重排序器的输入是由第一阶段检索器产生的候选集。第一阶段检索器的行为取决于嵌入模型。嵌入模型的更新周期并不掌握在重排序器团队手中。而阈值是由第三个团队调整的,他们既不负责嵌入模型,也不负责重排序器。
依赖链如下:嵌入模型 → 候选集构成 → 重排序器输入分布 → 重排序器输出分布 → 门控通过率。任何一个节点的改变都会向下游传播。末端的常量只有在它上方的每个节点都保持不变时,才真正是个常量。
大多数团队在理论上都知道这一点。失败发生在抽象的知识未能转化为部署时的检查。嵌入模型的升级表现为一次常规的索引重建。发布说明写着“我们更换了更新的版本,评分质量更高”。重排序器团队没有收到告警。阈值负责人没有收到告警。门控的通过率悄无声息地下降了,因为流水线中没有任何环节输出“高于门控的候选者比例”这一指标,而原本能捕获这一问题的告警也并不存在。
为什么即使模型没变,分数分布也会发生变化
“我没改重排序器,所以它的分数不可能改变”这种直觉近乎正确,却又错得危险。作为一个函数的重排序器没有变,但输入变了。
交叉编码器接收 (query, document) 对并返回某个范围内的相关性分数。分数取决于这一对输入。当嵌入模型改变时,针对给定查询的前 50 名候选集会发生偏移。其中一些新候选者是旧嵌入模型排名较低的块——它们可能是主题相关而非主题核心的块,或者是来自长文档的块,这些长文档的嵌入现在在向量空间中的聚类方式发生了变化。
重排序器根据自身的校准对这些新候选者进行评分。如果新候选者偏向于更难的情况——即重排序器不太确定的输入对——那么每个候选者的分数分布就会下移。中位数下降,直方图倾斜。以前得分 0.55 的块仍在前 50 名中;但新加入的块得分仅为 0.25。0.4 阈值下的通过率骤降。
从业者在为生产环境调整阈值时,对这种情况的描述较为缓和。Cohere 的文档建议选取 30–50 个具有边缘相关文档的代表性查询,并对重排序分数取平均值以得出阈值——这一过程产生了一个针对特定语料库和检索设置进行校准的数值。该过程并没有“当检索设置改变时该怎么办”的概念。它产生一个数字,团队将该数字写入配置文件,无论产生该数字的条件是否依然成立,该数字都成了代码库中的一个常量。
最近的研究,如 MAIN-RAG (Liu et al., 2025),直接在检索循环中构建了自适应过滤,根据每个查询的分数分布动态调整相关性阈值。核心思路是一致的:阈值应该是分布的函数,而不是你刻在流水线里的死 数字。
诊断之所以困难,是因为缺失了指标
当回答相关性下降,团队开始调查时,显而易见的嫌疑对象是模型。评估集针对 LLM 运行。LLM 的分数在评估集上看起来正常,因为评估集有其自身的规范上下文。评估集并不测试检索路径,因此检索部分的回归问题不会在那里显现。
下一个嫌疑对象是嵌入模型。有人注意到索引已重建,于是运行了仅针对检索的评估——衡量针对已标记查询-文档集的 recall@50。Recall@50 看起来没问题。新的嵌入模型拉入前 50 名的相关文档数量与旧模型大致相同。第一阶段检索器履行了它的职责。
重排序器最后才被盘查,因为它并没有被改动过。团队调取了过去一个月的重排序器平均分数,发现它在索引重建当天从 0.51 降到了 0.36,原因至此变得显而易见。诊断耗费了数周,因为没有仪表盘显示门控前的分数直方图。当直方图发生变化时,没有触发任何告警。团队的监控涵盖了延迟、错误率和端到端回答质量。本能捕获这一问题的中间指标——固定时间窗口内的重排序器分数分布——却没有人想到去埋点监控,因为似乎没有什么能改变它。
这是更深层次的失败:一个流水线的监控指标反映了其设计者预见的失效模式。设计者预见到重排序器可能会被更换,嵌入模型可能会被更换,阈值可能会调整错误。但他们没有预见到这三者中的一个会悄无声息地改变另一个。监控体系中的空白,恰恰对应了设计者未曾点明的依赖关 系。
除了在配置文件中写死一个数字,还能构建什么
填补这一差距的模式并不深奥。它们大多在于一种自律:将阈值视为推导值(derived)而非声明值(declared)。
基于百分位数的门控,而非绝对分数。 将 score > 0.4 替换为 score in top-X% of the candidate set 或 score > p70 of the last N queries' scores。从构造上讲,百分位门控能自适应分布偏移(distribution shift)。即使底层数据发生漂移,门控的严苛程度相对于当前数据仍保持恒定。其代价是门控不再承诺固定的质量底线——如果所有候选者都很差,门控仍会允许表现最好的那一小部分通过——因此百分位门控通常需要配合一个绝对分数的底线作为安全网。
在每次上游变更时重新校准阈值。 将阈值调优过程植入到任何上游组件的部署流程中。如果 Embedding 模型更新了,重排序器(reranker)的阈值应作为部署的一部分,针对预留集(held-out set)自动重新调优。阈值存在于由 CI 步骤重写的配置文件中,而不是由人工编辑的配置文件中。这要求预留集必须存在、具有代表性,且评分成本低——这三个前提条件往往会悄然失效。
监控门控前的分数分布。 将重排序器的门控前分数直方图作为一等指标(first-class metric)输出。针对滚动基准线进行 Kolmogorov-Smirnov(KS)检验或 PSI(群体稳定性指标)对比。当两次部署间的 KS 统计量超过配置值时触发告警。KS 检验能检测累积分布函数的偏移,非常适合比较两个经验直方图;PSI 是一种粗粒度的分箱比较,更容易设置告警,但对细微偏移较不敏感。选择其中一种并进行度量,将其告警视为阻断部署的信号,而非简单的告警(paging)信号——目标是在症状出现前捕捉到偏移,而不是对其进行响应。
对门控通过率进行回归测试。 维护一套固定的代表性查询,在每次部署时通过流水线进行端到端运行。断言通过门控的数据块(chunks)数量保持在一个范围内——例如,基准线的 ±10%。针对门控行为的测试可以捕捉到这种情况:即使门控或上游组件都没有报告明显问题,上游变更也已经在门控之下改变了分布。
架构层面的认知
这里的教训不仅限于重排序器。任何下游常量取决于上游分布的流水线都具有相同的形态。例如:固定在上一季度流量尾部百分位数的延迟预算;基于去年标签快照训练的欺诈模型的置信度阈值;根据供应商旧故障率调整的重试策略;或者根据分词器(tokenizer)已升级的模型校准的成本上限。在每种情况下,配置文件中看起来像常数的数字,实际上都是配置文件之外分布的函数。
架构上的改进是让这种依赖关系显性化。要么移除常量——将其替换为根据当前数据重新计算的函数;要么让那些会导致常量失效的上游变更在不重新调优的情况下无法部署。中间路径,即“我们会盯着它并在出问题时做出反应”,正是导致长达六周性能退化的原因。盯着监 控的团队总是盯着他们预期会变动的东西。而真正变动的,往往是那个本应是常量的值。
在 RAG 流水线中,这个常量几乎总是门控。导致它变动的几乎总是团队在升级上游组件时没有通知下游。补救措施是停止在配置文件中写入绝对分数,或者——如果你必须这样做——将每个上游组件的部署与每个下游阈值的重新调优挂钩。这两者都不怎么光鲜,但都比花一个季度去追踪一个没人改动过的数字所导致的性能退化要划算得多。
- https://docs.cohere.com/docs/reranking-best-practices
- https://nickberens.me/blog/understanding-rag-score-thresholds/
- https://fireworks.ai/blog/Understanding-Embeddings-and-Reranking-at-Scale
- https://galileo.ai/blog/mastering-rag-how-to-select-a-reranking-model
- https://machinelearningmastery.com/detecting-handling-data-drift-in-production/
- https://www.evidentlyai.com/blog/data-drift-detection-large-datasets
- https://towardsdatascience.com/understanding-kolmogorov-smirnov-ks-tests-for-data-drift-on-profiled-data-5c8317796f78/
- https://arxiv.org/pdf/2501.00332
- https://arxiv.org/html/2511.09803v2
