跳到主要内容

你增加的 Reranker:对召回率的拖累超过了对精准度的提升

· 阅读需 12 分钟
Tian Pan
Software Engineer

离线评估的结果非常明确。在向量搜索的前 50 个结果之上叠加一个交叉编码器(cross-encoder)后,nDCG@5 提升了 4 个点。团队在周二上线了该功能。到了周四,p99 检索延迟已超过 SLO(服务水平目标)700 毫秒,客户成功团队也开始转发空结果页面的截图,而这些页面在旧的流水线下本应是有内容的。真正关键的指标——用户感知的回答质量——下降了。重排序器(reranker)实际上是一个被团队冠以“改进”之名的性能退化,而评估标准则是将这种退化隐藏在众目睽睽之下的幕后黑手。

这是生产环境检索中最常见的失效模式之一,且很少被准确描述为:一个评估缺陷(evaluation bug)。重排序器完成了它的宣传任务:以更细的粒度对前 50 个结果进行了重新排序。问题在于,用于证明其合理性的指标——在无限预算下针对完整重排序列表计算的离线 nDCG——描述的是一个生产系统并不存在的理想世界。在生产环境中,最终输出的答案并非评分最高的重排序列表,而是系统在请求截止时间前所能返回的任何内容。一旦你以此方式重新定义指标,重排序器的贡献就不再是 4 个点的提升,而是一条曲线。

离线评估处于无限预算中,而生产环境则不然

大多数重排序器的评估是在 Notebook 中完成的。加载数据集,生成候选集,重排序器对每个候选进行评分,最后根据最终排名计算 nDCG@5。这里没有时钟,没有超时,也没有“如果重排序器耗时 900 毫秒而请求预算只有 800 毫秒,系统会返回什么”的概念。

生产环境的答案路径具备所有这三个属性。向量搜索在 40 毫秒内返回。交叉编码器在对 50 个候选进行批处理时,稳态下耗时 180 毫秒,而在突发负载导致批处理窗口排队时则需 900 毫秒。编排器有一个截止时间——通常是几百毫秒——超过该时间后,它必须选择截断结果、回退到未排序列表,或者直接失败。这些分支中的每一个都有一个准确率数值,而它们都不是离线 nDCG。

文献中现在对这种差距有一个专门的称呼:延迟条件准确率(latency-conditional precision)。其核心思想是不将准确率计算为一个单一的点,而是将其视为你愿意等待的时间预算的函数。在 50 毫秒时,重排序器的贡献为零,因为它还没有产生输出。在 200 毫秒时,它可能返回部分重排序。在 500 毫秒时,它返回完整的重排序列表。这些点中的每一个都有不同的准确率数值,而生产系统实际上是在对每个请求能到达哪个点的分布进行采样。离线指标是该分布的右边缘,评估时假设每个请求都能达到该点。而生产环境的现实是跨分布的加权平均值,超时导致的回退路径吞噬了左侧尾部。

如果团队在不建模这种分布的情况下根据离线 nDCG 进行发布,那么他们实际上在做一个特定的隐含假设:即生产系统总是能达到右边缘。在突发流量中,面对一个批处理窗口随队列深度呈超线性增长的模型,这种假设是错误的,而且随着你的业务成功,情况会变得越来越糟。你的用户越多,交叉编码器在竞争下就越“热”,最终输出的答案就越频繁地来自于评估从未衡量过的回退路径。

批处理窗口就是架构本身

在生产环境中,交叉编码器重排序器最被低估的一个属性就是它的批处理行为。交叉编码器将查询和候选一起通过 Transformer 运行。为了在 GPU 上提高效率,你会进行批处理——每次前向传递处理 32、64 或 128 个查询-候选对。批处理窗口(batch window)是指推理服务器在启动之前等待组装一个批次的时间。

在低 QPS 时,批处理窗口是长尾延迟的主导因素。服务器在处理前会等待窗口关闭,因此在批处理启动后紧接着到达的查询会面临最坏情况的等待。在中等 QPS 时,窗口饱和且延迟趋于稳定。在突发负载下,当 GPU 仍在处理上一个批次时,窗口就已饱和,队列深度随之增长。此时,边缘请求不仅要等待自己的批次关闭,还要等待前面的每一个批次完成。延迟曲线会瞬间垂直上升。

这就是“p99 延迟超过 SLO 700 毫秒”背后的失效模式。平均值正常,中位数正常,众数也正常。出问题的是突发情况下的上边缘尾部,而这恰恰是用户最没有耐心的一批请求。基于平均情况离线数据调优的重排序器无法告诉你,在你的峰值 QPS 下,它所在的曲线是否存在拐点。你必须进行负载测试,但几乎没人这么做。

回退并非优雅降级

当交叉编码器错过截止时间时,编排器需要一个应对策略。三种常见的策略是:返回向量搜索中未经重排序的前 K 个结果(“原始召回”回退)、返回任何已完成的部分重排序结果(“撕裂列表”回退),或者返回错误(“闭路失败”路径)。每种策略在生产环境中的准确率数值都不同,而大多数团队从未衡量过其中任何一个。

大多数团队认为发生的是“原始召回”回退。从预期来看,这似乎没问题:在重排序器存在之前,单纯的向量搜索就是基准准确率,因此回退只是让超时请求的系统表现回到基准水平。隐藏的问题在于,团队在引入重排序器之前的基准是基于“没有重排序器”的前提调优的。而有了重排序器后,上游系统通常会被重新调优,以检索更多候选、返回更松散的匹配或放宽相似度阈值——因为重排序器会捕获任何准确率缺失。当重排序器超时,原始召回回退所提供的候选集比重排序器出现之前的基准更松散。回退路径上的准确率比没有重排序器的世界更差。

“撕裂列表(torn list)”回退更糟糕。部分重排序返回的是以任意顺序评分的文档。前 10 个重排序候选中的某些可能尚未被处理。排名不再单调。如果不警告下游 LLM 就这样做,团队实际上是在给它喂一个排序信号毫无意义的候选集——LLM 非常看重位置,而此时的位置完全是噪声。这就是产生空结果截图的失效模式:被喂入“撕裂列表”的回答模型无法合成任何连贯的内容,只能无奈退回到“无结果”的响应。

加载中…
References:Let's stay in touch and Follow me for more thoughts and updates