上一首讲完 rerank 的原理:召回之后,把候选段落一条条和 query 拼在一起打分。当时留了个尾巴:官方模型是通用语料训出来的,落到你自己的领域,分数经常不贴身。这篇就把这个尾巴收掉,用 BGE 官方的微调脚本,把 bge-reranker-v2-m3 调成你自己的精排器。
先交代基座情况,数据都来自官方仓库和 Hugging Face API:bge-reranker-v2-m3 由智源(BAAI)发布,底座是 bge-m3,XLM-RoBERTa 架构 24 层,参数量 5.68 亿(官方仓库 safetensors 统计的精确数是 567,755,777),Hugging Face 上下载量 1500 万量级,Apache 2.0 协议允许商用。官方对它的定位是轻量、多语言、好部署,中文英文混合场景都能用,这一点对中文项目很友好,不用再纠结选中文版还是多语言版。
为什么值得微调而不是换更大的模型?我的看法是成本结构不一样。换模型是线性花钱,效果提升还未必贴你的数据;微调是一次性投入,数据攒起来之后模型就长在你的领域上。另一个数字更有参考价值:整套微调流程跑下来,8GB 显存的单卡就够,比调大语言模型便宜一个量级,一张几年前的游戏显卡就能试。
微调的输入长什么样
重排器和 embedding 模型的训练数据格式不一样。embedding 模型(上一首提过,见[向量数据库那篇](https://ai.saiita.com.cn/embedding-vector-database-explained-2026/))学的是「谁和谁像」,输出向量;重排器学的是「query 和这段文字配不配」,输出分数。所以它的训练数据是三元组:query、正例段落、负例段落,一行一个 JSON:
{"query": "怎么降低检索延迟", "pos": ["通过 KV 缓存减少重复计算……"], "neg": ["今天天气不错……"]}
官方 README 对 neg 有句大实话:如果手头没有负例,从整个语料里随机采样一些就行。想效果好,用难负例(hard negatives),官方还给了挖掘脚本:拿 embedding 模型对全库做一轮召回,从 top 2 到 200 的结果里抽负例。原理不复杂:排得越靠前还答非所问的段落,越值得拿去教模型,因为那正是它最容易犯错的类型。随机负例教的是「别选完全不相关的」,难负例教的是「在相似的里面挑对的」,后者才是精排器的核心能力。
一行数据里的正例通常只有一条,就是确定能回答 query 的那段文字。真正的工作量在整理 query:从客服日志、用户搜索词、领域 FAQ 里抽,不用多,几百条真实 query 比几万条生成的假数据管用。
四步跑通官方脚本
第一步,装环境:
pip install -U FlagEmbedding[finetune]
第二步,按上面的格式准备 train.jsonl。负例可以直接从你的业务语料里抽,量级几百到几千条就能看出变化。
第三步,改官方脚本的关键参数。官方给的标准命令里,和显存、效果直接相关的几个:
torchrun --nproc_per_node 1 \
-m FlagEmbedding.finetune.reranker.encoder_only.base \
--model_name_or_path BAAI/bge-reranker-v2-m3 \
--train_data ./train.jsonl \
--train_group_size 8 \
--query_max_len 256 \
--passage_max_len 256 \
--learning_rate 6e-5 \
--fp16 \
--num_train_epochs 2 \
--per_device_train_batch_size 2 \
--gradient_checkpointing \
--output_dir ./my-reranker
第四步,加载训练出的 checkpoint 打分。官方 FlagReranker 的用法,把模型路径换成你的输出目录即可:
from FlagEmbedding import FlagReranker
reranker = FlagReranker('./my-reranker', use_fp16=True)
scores = reranker.compute_score([["query", "段落一"], ["query", "段落二"]])
参数怎么改,账在这里
几个参数的取舍,官方默认值背后的逻辑:
- train_group_size 8:每个 query 配 8 个段落(1 正 7 负),单卡显存紧张就降到 4,负例少了效果略降,账好算。
- query_max_len / passage_max_len:官方例子给 256/256,你的文档切块如果更长,往 512 提,显存占用跟着涨。这两个值直接决定一条训练样本的长度上限,切得越狠训练越快但信息损失越大。
- learning_rate 6e-5:官方默认,别上来就动它。重排器是分类头结构,这个学习率在多个官方示例里一致,动之前先跑一轮 baseline。
- num_train_epochs 2:数据几千条时 2 轮够看效果,过拟合的信号是训练分数涨、验证集合上的排序反而掉。拿验证集上的前 3 命中率当指标,比看 loss 直观。
- knowledge_distillation:如果你手里有更强模型给每条样本打的分数(pos_scores/neg_scores 字段),打开它做知识蒸馏,等于让小模型学大模型的打分偏好,官方数据格式里专门留了这两个字段。
显存不够还有一档:参数量降到 2.7 亿的 bge-reranker-base,效果换速度和内存,本地小机器也能跑。
顺带说一句,如果你要调的是 gemma 或 minicpm 底座的解码器类重排器,官方脚本带 use_lora 选项(rank 32 / alpha 64 的默认组合,之前算过 [LoRA 的 rank 怎么选](https://ai.saiita.com.cn/lora-rank-param-guide-2026/)),但那是 2B 量级模型的玩法,显存需求另算。日常项目里,5.68 亿的 v2-m3 微调完已经能应付绝大多数中文场景,先跑通小模型再考虑上大的。
训完怎么验
拿一小批真实 query,分别用微调前后的模型给同一批候选打分,人眼看前 3 条的变化。训练时用的 query 都来自同一领域,验证集要留出一部分没参与训练的,不然你看到的提升是背题。具体做法:随机抽 50 个线上真实 query,人工标注每条的正确段落,跑微调前后两个版本的命中率,提升超过 5 个百分点就值得上线灰度。
两个常见的翻车点。一是全靠生成数据:拿大模型生成的 query 配真实段落,分布和真实用户差得远,训出来线上效果掉,宁可人工写一百条。二是切分太碎:段落切得只剩一两句话,正例本身信息量不足,模型学到的都是表面特征。切块策略之前在检索系列里写过,100 到 300 字带一点上下文重叠是比较稳的起点。
放回链路里看
微调这步做了之后,检索链路里变化的只有精排这一段,前面的向量召回(之前写过 [RAG 和长上下文怎么选](https://ai.saiita.com.cn/rag-vs-long-context-vs-finetune-2026/))不动,所以出了问题也容易定位:分数不对就查重排器,召回不全就查 embedding 模型。这个解耦是这套架构最大的工程好处,每一环都能单独换、单独验。
上线方式也简单:训练产物就是标准的模型目录,加载方式和原模型一样,服务端把模型路径换掉就行。想更稳,新旧两个重排器并行跑一段时间,对比线上点击率再切流量。
先拿几百条数据跑一轮看看,值不值,跑完就有数。