周六下午,我盯着终端里的日志,上面的数字难看得很。第 16 个实验跑完了,F1 依旧在 0.6 附近晃悠。想了很久,最后在技术群里认输:“rerank 配小模型,复现不了那个判定模型,我投了。”
不是标题党,是真的输了。我手上这个任务很常见:给定一个 query 和一段文档,判断文档是不是真正满足这个 query——相关还是不相关,通过还是不通过。本质是一个二分类判定模型,不是召回也不是精排。约束是不能用大模型 API,只能用开源小模型,最好普通显卡就能转起来。当时我把主意打到了 rerank 头上。
逻辑是这样的:rerank 的核心是交叉编码器,query 和文档拼在一起过 Transformer,语义交互吃得很透,在检索里的提分效果肉眼可见。它既然这么懂语义,我为什么不直接拿它的分数当判定分数?再配一个小模型做前置过滤,纯开源、少标注、低成本,这不就齐活了?
理想很丰满,现实很打脸。两天,16 个实验,从 zero-shot 试到微调,从阈值搜索试到分数校准,甚至把高斯过程回归都用上了,最好的成绩也离目标差一截。这篇文章不是教程,是一个完整的失败记录,我会把实验设计、核心现象和底层原因都拆开来讲。如果你也在打“用 rerank 做判定”的主意,看完应该能少走两天弯路。
1. 为什么我会想到用 rerank 做判定模型
1.1 判定模型的本质:要的是及格线,不是名次
判定模型的经典任务就是二分类或多分类,输出的是一个“该样本属于哪个类、满足不满足条件”的绝对判断。拿我的任务举例:判定“文档是否真正回答 query”,模型面对的是 Ground Truth 标签,它学习的最终目标是让“通过/不通过”的决策正确率最大化。这样的模型,天然需要输出一个可以被比较的绝对测度——0.7 的通过概率和 0.3 的通过概率,代表着两种不同的置信程度,而且这两个数字在不同样本之间是可比的。
rerank 呢?它学的是相对序。训练数据长什么样?某一次搜索里,用户点了结果 A 没点结果 B,所以 A 应该在 B 前面;某次问答里,文档 C 比文档 D 更贴合 query,所以 C 排在 D 前面。它的优化目标是 NDCG、MRR 这类排序指标,Loss 也是 pairwise 或者 listwise 的形式。模型关心的是候选集内部“谁在谁前面”,根本不关心第二条文档绝对地“有多好”。
我说得通俗一点。你高中班上有个同学,你只知道他每次模考名次都在你前面,但你不知道他总分是 650 还是 400,也不知道 480 分算不算过一本线。rerank 就是那个只见过名次、没见过分数的考生。你把它拉来当“判定模型”,问它“这条文档有没有资格及格”,它只能给你一个相对位置的感觉,不能给你一个绝对的分数。
1.2 交叉编码器的“语义理解”给了人错误的安全感
既然 rerank 有这个问题,为什么我一开始还是想用它?因为交叉编码器太诱人了。它把 query 和 doc 拼成一个序列,过完整的 Transformer,attention 可以同时看到 query 的每一个 token 和 doc 的每一个 token。对比双塔结构的 embedding 模型,它理解的是真正的语义交互,而不是两个向量的余弦相似度。在很多 RAG 评测里,rerank 能把召回结果从 top-20 提升到 top-5,效果非常猛。
于是我会产生一种错觉:这模型是真的“懂”语义的。既然懂,那它输出一个分数,我为什么不能把它当成置信度?当时的思考特别顺滑:分数高等于更相关等于满足要求;分数低等于不相关等于不满足。这中间只差一个阈值。而阈值这种东西,拿验证集一搜不就有了吗?
这个推理的危险之处在于,它把“模型内部理解能力强”和“模型的输出具有绝对度量意义”划了等号。实际上,交叉编码器理解语义很强,但不代表它被训练成了一个绝对评分器。它的打分函数只要能维持相对顺序就完成任务了,至于分数本身科学不科学、稳定不稳定,训练目标根本不关心。
1.3 任务约束:为什么一开始绕开微调
还有一个现实原因:我没有大量标注数据。这次的判定任务,人工标注样本一共几百条。几百条去微调一个 7B 模型,大概率过拟合;拿去微调 BERT 类模型,效果倒是稳,但当时心里预期是“应该用不上,rerank 直接就够了吧”。这个误判直接导致我头两天在错误的方向上冲。
另外还有一个来自行业氛围的推力。那段时间开源小模型讨论很热,经常有人问“现在开源小模型有好用的么”。确实,Qwen2.5、Llama-3.2 这些,写摘要、做角色扮演、跑函数调用,都已经有模有样。我心里难免会想:小模型加一个聪明的 rerank,强强联合,判定任务还拿不下?后来的事实是:聊天好用和判断靠谱,完全是两码事。
2. 第一天的 6 个实验:从直出到裸奔
2.1 环境与基线配置
有必要先交代一下环境,方便你对照复现。两张 4090,离线推理用 vLLM 起的小模型服务,rerank 用的是 BGE 系列的 cross-encoder。候选小模型我测了 Qwen2.5-1.5B Instruct、Qwen2.5-3B Instruct、Qwen2.5-7B Instruct,以及一个 DeBERTa-v3-base 做微调对照。数据统一走一套 prompt 模板:
你是一个严谨的判定模型。根据问题和文档,判断文档是否真正回答了问题。 只能输出 yes 或 no,不要输出其他内容。 问题:{query} 文档:{doc} 回答:这里有个小细节,我后来发现:模型输出如果不做约束,会给你一堆解释,什么“文档提到了相关内容,因此可以认为……”之类的。解析这些文本做判断,比解析 yes/no 麻烦得多。所以实验里我做了 constrained decoding,强制模型只能从 yes/no 两个 token 里选。用 vLLM 的 guided_decoding 或者 Outlines 库都能实现。这一点算是第一天最明确的收获:生成式小模型做判定,必须锁死输出空间,否则它自己发挥起来,你的解析逻辑根本跟不上。
2.2 实验 1-4:小模型直接判定的表现
第一批实验就是让 1.5B、3B、7B 轮流上阵。结果很有意思,也很有挫败感。
1.5B 的 zero-shot 几乎等于乱猜,验证集 F1 大概 0.52,也就是比抛硬币好一点点。而且它特别喜欢说 yes,一遇到“文档里沾了点边”的情况就放行。3B 稍好一点,F1 将近 0.58,但遇到否定句式、条件约束多的 query 就崩。举个例子,query 是“哪些情况不能提取公积金”,文档逐条列了提取条件,3B 会抓住“公积金可提取”这几个词直接放行,完全没看到“不能”这个否定。
7B 明显正常多了,F1 能到 0.67 左右。但离我预期的 0.85 还是差着一大截。这里我要说一句公道话:7B 的表现让我看到开源小模型确实在进步,但“好用”是相对的,在严谨判定上它的稳定性依然不够。同一个样本,你把 prompt 里“只能输出 yes 或 no”改成“请给出结论”,它的判断就会飘。
few-shot 也试了,给 3B 塞了 5 个手工挑选的示例,F1 勉强涨到 0.61,但是换个示例顺序就掉到 0.55。这种方差让人觉得不踏实:你根本不知道模型学会的是判断逻辑,还是学会了模仿示例的格式和倾向。
2.3 实验 5-6:rerank 裸打分,阈值问题直接暴露
第一批的重头戏是 rerank 裸打分。我用 bge-reranker-v2-m3 给每条样本的 (query, doc) 算一个分数,一开始按直觉设 score 大于 0 判定为相关。结果 F1 只有 0.60 出头,比 7B 还差。于是我做了一件很自然的事:在验证集上搜索最优阈值,把 F1 拉到 0.64。
到这里,问题已经初见端倪。验证集最优阈值是 0.37,可一旦换到测试集,用同一个固定阈值,F1 直接掉到 0.58。我第一反应是“阈值过拟合了”,多搜几个区间、多搞点分位数总行吧?后来才发现根本不是阈值的问题,是分数本身不具备跨集稳定性。同样一句话,在验证集上的分布和在测试集上的分布天差地别,你搜出来的最优阈值只是那几百条样本的巧合产物。
我把第一天的实验结果整理成了表。
| 编号 | 方法 | 关键配置 | 验证集F1 | 现象 |
|---|---|---|---|---|
| 1 | Qwen2.5-1.5B zero-shot | 固定模板 + constrained decoding | 约0.52 | 偏向yes,判断接近随机 |
| 2 | Qwen2.5-3B zero-shot | 同上 | 约0.58 | 否定句式、条件约束理解差 |
| 3 | Qwen2.5-7B zero-shot | 同上 | 约0.67 | 相对正常,稳定性仍差 |
| 4 | Qwen2.5-3B few-shot | 5个示例 | 约0.61 | 示例顺序敏感,方差大 |
| 5 | bge-reranker-v2-m3 | score > 0 | 约0.60 | 直接当判断分数,不如预期 |
| 6 | bge-reranker-v2-m3 | 验证集最优阈值 | 约0.64 | 阈值切到测试集即失效 |
第一天的结论,表面上看起来是“阈值没选好、小模型不稳”,但实际上已经藏着一个更深的问题:rerank 的分数不是一个稳定的绝对度量。只是我当时的认知还不够,把问题归结到阈值上,于是有了第二天那一堆校准和融合实验。
3. 第二天的 10 个实验:校准、融合、微调、锚点,能试的都试了
3.1 实验 7-9:分数融合与真正意义的微调
第二天早上,我先把最直觉的融合方案跑了一遍。小模型生成答案时本身有 logits,rerank 也有分数,两个加权的想法非常自然。实验 7 就是让“小模型 logits 的 sigmoid 值”和“rerank 分数的 min-max 归一化值”做加权求和。权重从 0.1 到 0.9 网格搜索,结果最好 F1 约 0.66。这个数字很令人沮丧:融合的上限基本就是两个模型的 max,加权没有创造出新的能力。
接下来我开始怀疑一个一直没做的事:微调。实验 8 是拿几百条标注数据微调 DeBERTa-v3-base 做二分类。结果它直接跑到 F1 0.78,这个成绩比我前两天所有“聪明”方案都高。我当时脑子里只有一个想法:分类任务就该用分类模型,我在这儿跟排序模型较什么劲?
为了验证这个想法,实验 9 用 Qwen2.5-3B 做了 LoRA 微调,但这次不是生成式微调,而是把模型最后一层换成了二分类头,用交叉熵训练。F1 约 0.72,低于 DeBERTa。数据量太少是一方面,另一个原因我后面会讲:生成式预训练模型的 representation 不一定适合直接接分类头,它习惯的是“预测下一个 token”,不是“把语义压到一个标签上”。
3.2 实验 10-12:锚点对比、特征化和多任务
实验 10 是我当时觉得最有可能突破的方案:锚点对比。既然 rerank 是学 pairwise 的,那我就顺着它的训练方式走。固定一组人工挑选的正例锚点和负例锚点,把待判定文档分别和正锚、负锚组成 pair,用 rerank 分别打分,两个分数的差作为判定依据。逻辑上讲,如果文档和正锚的 pair 得分高,那它应该更接近正类。
这个方案在验证集上 F1 到了 0.66,但有两个致命问题。第一,锚点非常敏感,换一组锚点,分数差能到 0.08。第二,它导致推理时间变成原来的倍数级,每一条样本要跑两次 rerank,线上请求的延迟根本顶不住。这种方案只能算实验室玩具,不能落地。
实验 11 我换了个思路:不把 rerank 分数当最终判决,而是当特征。把 rerank 分数、小模型给出的 logits、文档长度、query 与 doc 的 token 重叠数、关键词命中率等一起喂给一个逻辑回归。F1 到了 0.68,虽然比裸打分好,但也就那样。这至少说明,rerank 分数里确实有判别信息,但它需要被其他特征修正,不能独当一面。
实验 12 是更硬核的做法:把 reranker 的 backbone 拿出来,加一个二分类 head,做多任务学习——一个 head 走原来的 pairwise 排序 loss,一个 head 走 pointwise 分类 loss。F1 到 0.70。这个方案已经跟“用 rerank 做判定”关系不大了,本质是用 rerank 模型的结构微调出一个分类模型。这也印证了一件事:想要绝对判定能力,就得用 pointwise 的监督信号去训,不能指望 pairwise 预训练模型天生具备。
3.3 实验 13-16:校准、集成与最后的认输
接下来的四个实验,我开始跟“分数校准”死磕。实验 13 是动态阈值,不是固定一个数,而是按分数分布取分位数。效果跟固定阈值没有本质区别,测试集该失效还是失效。
实验 14 上了一个更有理论味道的方法:对 rerank 分数做校准。先试了 isotonic regression,效果一般,因为校准样本太少。后来想到高斯过程回归——它在小样本、非线性映射场景下的拟合能力不错,适合把“排序分数”这种形状不明的变量映射到“概率”上。校准完的分数确实看起来更平滑、更像概率了,F1 却几乎没有提升。原因也简单:校准只改变阈值,不改变排名。排序模型还是那个排序模型,校准只是给它穿了一件“概率”的外衣。
实验 15 是集成。我把 bge-reranker-v2-m3、另一个开源的 cross-encoder、还有微调后的 DeBERTa 一起投票,F1 到了 0.79。这个数字终于过了我自己拍脑袋定的 0.75 心理线,但代价是部署三套模型,延迟和成本直接爆炸。而且它已经彻底退出“rerank 配小模型”的框了,真正托底的是那个微调分类模型。
实验 16 是压垮我的最后一根稻草。我违规换了一个更大的模型跑同一条 pipeline,F1 轻松到 0.84 以上,任务难度完全可控。也就是说,不是任务不可做,不是我标注数据不够,是“开源小模型 + rerank”这个组合本身不具备稳定复现判定模型的能力。于是我认输了。
第二天的实验结果我也整理了一下。
| 编号 | 方法 | 关键配置 | 验证集F1 | 现象 |
|---|---|---|---|---|
| 7 | logits + score 加权融合 | 权重网格搜索 | 约0.66 | 上限约等于两者max |
| 8 | 微调 DeBERTa-v3-base | 二分类head | 约0.78 | 分类模型直接碾压各种“聪明方案” |
| 9 | Qwen2.5-3B LoRA 二分类 | 分类head + CE | 约0.72 | 数据少,提升有限 |
| 10 | 正负锚点对比 | 双次rerank | 约0.66 | 锚点敏感、延迟翻倍 |
| 11 | rerank分数做特征 + LR | 手工特征 | 约0.68 | 分数有信息量,但只能当特征 |
| 12 | rerank backbone多任务 | pairwise + pointwise | 约0.70 | 实质已是微调分类模型 |
| 13 | 动态分位数阈值 | 按分布取阈值 | 约0.64 | 不解决跨集失效 |
| 14 | 分数校准 | isotonic + 高斯过程回归 | 约0.65 | 分数变平滑,F1没变 |
| 15 | 多模型集成投票 | 双rerank + DeBERTa | 约0.79 | 托底的是DeBERTa,不是rerank |
| 16 | 大模型同管线 | 7B以上 / API | 约0.84 | 任务可做,路线错了 |
4. 认输的底层原因:排序分数与判定分数之间隔着一条认知鸿沟
4.1 一个让我彻底明白的反直觉现象
在讲数学之前,先讲一个实验现象。有一次我在调试时,拿同一条 (query, doc) 分别放进两个候选集里算 rerank 分数。第一个候选集里全是弱负例——那些明显不沾边的文档;第二个候选集里塞了几个强负例——语义高度相似但没答到点上的文档。结果同一条样本,在弱负例环境里得分 1.2,在强负例环境里得分 2.8。
这个现象当时看得我头皮发麻。如果 rerank 的分数真的是文档和 query 之间的绝对相关度,那它不应该因为周围是谁而改变。但现实是它变了,而且变化很剧烈。这说明它学到的更接近一个“相对竞争力”信号:在一个都强的候选集里,你再好也可能被压在下面;在一个都弱的候选集里,你再差也能出头。
4.2 pairwise 与 pointwise:两种优化目标,两种世界观
从训练目标来解释就更清楚了。排序模型用 pairwise loss 训练时,样本是一个三元组 (q, d+, d-),loss 只在“正样本得分是不是比负样本得分高”上有约束。只要 d+ 的分数大于 d-,loss 就是 0,模型完全没有义务告诉你,d+ 的分数到底是 0.6 还是 0.9。
于是经过训练之后,模型内部可能把所有候选的分数都挤在一个狭窄区间里,只需要通过微小的流动性区分相对次序。你硬要给这些分数设一个绝对阈值,就像要求一个只打过“谁分数高谁赢”的人去当考官,告诉他“80 分算优秀,60 分算及格”——他根本没经历过绝对尺度,怎么可能给得准?
判定模型则不同。pointwise 分类的目标是让预测的概率贴近真实标签,每一条样本都独立施加约束,学出来的分数自然具备绝对意义。一个是“我比他好”,一个是“我到底够不够好”,这就是 rerank 不能直接当判定模型的根因之一。
4.3 校准为什么救不了排序分数
也许你会说,既然分数分布不稳定,那我拿一批标注数据学一个校准函数,把 score 映射到概率,不就行了?实验 14 已经证明这条路走不通,原因有两层。
第一层,校准函数是固定的,但排序分数的分布是随候选集漂移的。今天线上候选集平均质量是 0.6,明天变成 0.8,同一个文档的分数可能从 1.2 飘到 2.8。校准函数是在旧分布上拟合的,到了新分布上当然失效。这就是为什么实验 14 中,即使我用高斯过程回归把校准函数拟得再平滑,F1 依然没有起色。
第二层,排序分数的非线性不是简单的单调变换。有时候强负例会把正例的分数压低,但压低的幅度跟候选集里有多少个强负例有关,不是单个样本能决定的。这导致校准问题本身就有点病构:你要预测的目标是一个受周围样本影响的值,又想拟合出一个与周围样本无关的全局函数,这本身就矛盾。
4.4 顺手聊聊开源小模型的“虚火”
最后说回“现在开源小模型有好用的么”这个流行话题。我的结论是:看场景。写代码、写摘要、做角色扮演,Qwen2.5、Llama-3.2 这些确实好用。但做严谨判定,它们的表现就像一个聪明但爱脑补的实习生——它能给你一个看似合理的答案,却不能保证答案的边界稳定。
尤其生成式模型,它的输出是逐 token 采样的,末尾的分类概率受 prompt 措辞影响极大。我把 prompt 里的“文档是否真正回答了问题”改成“文档是否与问题相关”,1.5B 模型在 200 条样本上就有 30 多条结论翻转。这种敏感性在聊天场景里感受不明显,在判定场景里是致命的。
如果你非要让生成式小模型做判定,我的建议是:constrained decoding 锁死输出、多次采样取众数、在 prompt 里加清晰的判定标准。但这些只是缓解手段,治不了“模型没有稳定绝对测度”的病根。
5. 认输之后的替代路线,以及这趟弯路的真实收获
5.1 任务本质决定技术路线,别跟 loss 对赌
我现在会先问一个问题:你的输出会被怎么使用?如果这是一个“判断文档是否满足条件”的场景,输出会被当作一个独立、可比的置信度来使用,那请直接选 pointwise 分类模型,比如微调 DeBERTa,或者给 7B 模型加分类头做 LoRA。如果这是一个“把候选结果从好到坏排序”的场景,用户只会看 top-k,那 rerank 就是最合适的,直接选它。
两者的分界线不是模型规模,而是优化目标。pairwise 模型永远不能替代 pointwise 模型,就像你让一个教练只看“队伍排名”来判断“这支队伍有没有夺冠实力”,排名能说明一部分问题,但绝不等于绝对实力。
5.2 如果你实在想用 rerank 做判定,至少按这个姿势来
我不拦着你踩坑,但可以把坑填浅一点:
- 把 rerank 分数当特征,不要当结果。喂给 LR、GBDT 这类模型,跟其他特征组合,效果会比裸分数好很多。
- 用锚点对比时,多做鲁棒性测试。换三组以上不同难度的正负锚点,如果结果抖动超过几个点,说明这个方案根本立不住。
- 做分数校准时,优先考虑分位数校准,样本充足用 isotonic regression,样本少可以尝试高斯过程回归。但要记住,校准解决的是“分数形变”,解决不了“分数漂移”。
- 监控候选集分布。如果你线上的候选集质量和训练时不一致,rerank 分数整体漂移几乎必然发生,任何固定阈值和校准函数都会跟着失效。
5.3 开源小模型的正确打开方式
这次实验让我对开源小模型的定位有了更清晰的认识:
- 如果你有 7B 级别可用,优先 7B,别在 1.5B、3B 上硬撑。判定任务对语义边界的要求远高于聊天,参数量的差距会直接体现在稳定性上。
- 小模型最适合的场景是作为粗筛,把明显不对的样本快速过滤掉,把模糊样本送给精排模型或大模型。这个“粗筛加精判”的架构,在成本和效果之间是最优解。
- 几百条样本的小数据场景,经典分类器往往比 LLM 更可靠。DeBERTa 微调在我的任务上赢了 Qwen2.5-3B LoRA,原因就是它本来就是为判别任务设计的,参数效率更高,不容易过拟合。
5.4 最后的一点个人体会
如果把这次踩坑的经验压缩成一句话,我会说:别拿名次当分数。做技术选型之前,先想清楚你要的是绝对判断还是相对排序。如果排错了,后面再多的调参、校准、融合、蒸馏,都是在给一个错误的地基添砖加瓦。
当然也不能说一无所获。至少我确认了三件事:开源小模型在判定任务上的边界在哪里、rerank 分数到底是个什么东西、以及未来遇到类似任务时,我应该从哪里开始做。把这些写出来,希望你能比我少跑两天弯路。