071、Reranker模型与精排
2026/9/17 2:18:19 网站建设 项目流程

071、Reranker模型与精排

老规矩,先说个真事儿。上个月调一个问答系统的线上badcase,用户问“苹果售后怎么预约”,日志里召回阶段明明把“苹果官网预约服务”、“Apple Store天才吧预约”这些文档都拉出来了,可排在第一位的是“苹果公司2023年财报分析”。当时第一反应是embedding模型没调好?结果把召回top20挨个看,发现召回没问题,问题出在排序——我们当时压根没接精排,直接用向量相似度排序,结果“财报”这种词向量上跟“苹果”相关度太高,把真正的售后意图给压下去了。后来我把同一组query和候选文档喂给一个cross-encoder重排,第一位立刻变成了“iPhone电池维修预约攻略”,问题当场解决。这就是Reranker,也就是精排存在的意义。

先说清楚召回和精排的分工。召回阶段为了快,通常用双塔模型把query和doc分别编码成向量,拿余弦相似度捞topK。双塔是个“非交互”结构,query和doc在最后算相似度之前谁都见不到谁,所以它必须把语义压缩成一个固定向量,天然会丢失细节。而Reranker不一样,它把query和doc拼接成一对输入,里面的self-attention会让两者充分交互,能捕捉到“苹果”出现在“售后”语境下该是什么角色。代价就是慢,没法对全量文档跑,只能在召回的几百条里重新排。

Reranker的典型模型叫cross-encoder,名字很直白,就是让两个文本在编码器里交叉。跟双塔的sentence-bert那种bi-encoder相对。用起来倒是不复杂,HuggingFace上随便拉一个模型就行。我这里写个用sentence-transformers的CrossEncoder的代码,注意看注释,都是我踩过的坑。

fromsentence_transformersimportCrossEncoder# 别用太小的模型,比如mini那种,效果跟双塔差不多,白费劲# 我常用的是 bge-reranker-base,或者 mmarco-mMiniLMv2,中文场景看情况model=CrossEncoder('BAAI/bge-reranker-base',max_length=512)defrerank(query,docs,top_k=10):# 这里把query和doc拼成pair,别用字典列表,就用元组列表pairs=[(query,doc)fordocindocs]# 千万!别一次把几千条都塞进去,GPU显存爆了没人给你报销# 我一般分batch,128或者256,看你的卡scores=model.predict(pairs,batch_size=128,show_progress_bar=False)# scores是float数组,值越大越相关,但别直接拿它当概率# 有的模型输出是0~1的sigmoid,有的是logits,别想当然results=list(zip(docs,scores))results.sort(key=lambdax:x[1],reverse=True)return[docfordoc,_inresults[:top_k]]

这段代码看起来简单,实际调的时候有几个地方能让你怀疑人生。第一个是max_length,你以为设个512就完事了?我是做营销文案检索的,有用户把广告词写成长图里的文字堆起来,一个doc超过2000token。设置512后,后面内容直接被截断,导致精排结果跟召回差得离谱。后来我改成动态长度,或者干脆预处理切段再聚合。第二个是score的分布,不同训练好的模型,输出范围完全不一样,有的偏好大数,有的偏小数。你想把精排分数跟业务分数合并时,不归一化就是灾难。我现在的做法是先用softmax把当前batch的精排分数转成概率分布,再跟CTR预估分数加权,不然两个量纲打架。

回归到这个场景,精排不是简单调个接口。它在整个推荐/搜索流程里承担的角色是“最后一公里的纠偏”。召回管广,精排管准。但精排不只有Reranker模型,真正的工业级流程里,精排层通常是“逻辑回归/GBDT + 深度排序模型 + 业务规则”的混合体。Reranker模型只是其中提供语义信号的一路输入,还得把价格、销量、时效、用户画像这些特征拼进去。之前我天真地以为上了cross-encoder就能解决所有排序问题,结果测试集AUC涨了0.03,线上点击率反而跌了。为啥?因为那些业务特征在传统的精排模型里已经排序了,你再加一个语义分,等于是让模型捡了芝麻丢西瓜。

所以现在我的习惯是,先用Reranker模型在召回结果上做一个独立打分,然后把打分结果作为特征,喂给下游的LGBM或者深度模型。这个方案在技术叫“feeding the ranker”,好处是后续模型能学到“语义相关度”跟业务指标的非线性关系,而不是粗暴加和。

调试Reranker还有一个隐藏坑:负样本的构造。cross-encoder训练时,正样本好找,通常是点击过的query-doc对,负样本呢?你要是随机采样,模型很快就能学会“只要不相干就是负样本”,但它学不会“两个都相关但一个更相关”。这种困难负样本,得从召回里挑那些分数高但没被点击的样本,或者用另一个模型排出来的topK里人为标记错误项。我有次偷懒,用BM25的top结果当负样本,训出来的模型把“苹果”全当成了水果,线上查“苹果手机壳”差点翻车。

再聊一下延迟问题。cross-encoder慢是绕不开的,尤其公司没有GPU推理集群的时候。CPU上跑一个base模型,平均一个pair大概20毫秒,看起来不高,但一次检索可能召回500条,那就是10秒,直接超时。抠门老板又不肯买卡,怎么办?几种土办法,第一个是缩小召回数,top100以内,因为精排提升主要靠前几十位的判别力,太靠后本来就不该被看到。第二个是模型蒸馏,拿大模型给一个小模型打标签,让student学teacher的输出,改成一个小cross-encoder,速度能快3倍,但性能保留七八成。第三个是跟粗排配合,先用双塔或者更轻量的模型排两百条,再让Reranker只精排前五十。这个过程千万别省,我见过最惨的一次,直接对全量一万条跑Reranker,结果线上QPS掉到了个位数,监控拉红。

还有个值得记一记的技巧:精排分数不要全局归一化,要按query归一化。同一个query下分数高低才有比较意义,跨query比较没道理。我一开始直接对全库文档做softmax,等于把不同分布的高分凑一块,效果反而乱。后来改成每个query的topK内做z-score,再跟业务分数相乘,整体指标稳定多了。

写到这里,想起上周同事问我,“既然Reranker这么好,能不能直接用Reranker来做召回?”我劝他别动这个心思,除非你把底层的乘积量化、ANN、IVF那些全扔掉,否则Reranker做召回的成本能让你老板看到电费单当场心梗。召回要的是快,召回10万个候选花50毫秒,Reranker重排100个花30毫秒,这个比例才是健康的。

至于选什么模型,我跟几个朋友交流下来,小数据集上bge-reranker-v2-m3表现挺稳,中文任务尤其好;英文场景的ms-marco系列跑不了太远的领域。领域数据多的话,强烈建议在开源模型基础上继续预训练,因为通用模型对垂直行业的术语和用户口语化表达往往力不从心。比如“耳机掉水里了”这种话,“防水”和“进水保修”在通用语义里可能相距十万八千里,但在你的产品场景里就是强相关。你不好好喂数据,Reranker就只是个花瓶。

最后说个个人经验,写下来也是提醒自己。别拿到一个新模型就直奔调参。先找20个线上badcase,看召回对不对、精排对不对、业务规则到底卡在哪。我那次“苹果售后”的问题,如果早做这一步,也许根本不用上Reranker,直接在业务层加上“意图分类”规则就能解决。但另一方面,规则只能处理已知问题,而Reranker能兜住那些你没想到的语义变形。我的建议是,每一层都用最简单的方式解决那个层该解决的问题,召回保召回,精排保精排,别跨越层级微操。精排模型上线前,务必做线上A/B测试,别信离线AUC,离线指标提升并不等于线上点击率一定涨。这是无数人用头发换来的教训。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询