简介:这是一份面向自然语言处理研究者和工程师的实践型资料包,聚焦检索排序重排模型的具体应用,帮助读者掌握从模型安装调用、编码器对比、到微调优化与效果评估的完整链路。资料包含一个PDF文档,文件体积仅246KB,内容却十分紧凑:文档从Sentence Transformers入手,先后演示了bi-encoder与cross-encoder两种编码器的安装、调用和输出解读,并给出在llama_index框架下接入网易有道bce-embedding与bce-reranker的完整代码。针对模型微调,文档详细讲解环境搭建、数据格式组织、训练脚本编写,以及使用autotrain快速开展微调的具体步骤;评估部分则介绍MTEB与c-mteb基准,帮助读者横向比较并验证重排效果。整体内容兼具原理说明与可运行代码,适合已有一定深度学习基础、希望在RAG或语义搜索项目中落地重排技术的读者参考;目前已有118人学习/下载,可作为实际开发前的快速资料。
1. 别再用单向量 embedding 硬扛排序:rerank 才是 RAG 准度最后的临门一脚
先抛一个反直觉的结论:在 RAG 或者向量检索场景里,第一阶段用 embedding 召回的 Top-K 结果,往往只有前两三名是准的,后面的基本是“看着相似、实则离题”的噪音。我在实际项目里遇到过很多次,召回 20 条、真正能回答用户问题的只有 2 条,直接丢给 LLM 不仅浪费上下文窗口,还会让模型被无关段落带偏。这就是 rerank 存在的意义——在召回之后、生成之前,用一个更精细的模型把候选段落重新排一遍。
这份资源的核心内容是:基于 Sentence Transformers 与 ColBERT 的 rerank 模型实践,从安装、原理解析,到接入 LlamaIndex 完整 RAG 流程、再到微调和 MTEB 评估,是一条能直接照做的闭环路线。适合已经会用向量检索、但觉得答案质量碰运气的 NLP 工程师和研究人员。整篇笔记里最值钱的部分,不是某个模型怎么调,而是“召回多少 → rerank 取多少 → 上下文塞多少”这三者的匹配关系,以及微调时数据格式和超参的坑。
2. 架构选型先把账算清:Bi-Encoder、Cross-Encoder 与 ColBERT 各自管哪一段
2.1 Sentence Transformers 安装与最小可用代码
先把环境跑通。Sentence Transformers 是这套实践的基础库,安装命令只有一条:
pip install -U sentence-transformers装完之后,最快的验证代码是这样:
from sentence_transformers import SentenceTransformer sentences = ["Hello World", "Hallo Welt"] model = SentenceTransformer('sentence-transformers/paraphrase-MiniLM-L6-v2') embeddings = model.encode(sentences) print(embeddings)这里encode返回的是一个二维数组,每一行是对应句子的向量。默认情况下 MiniLM-L6-v2 输出 384 维向量,两个句子各占一行。这个模型的典型用途是句子相似度计算和语义搜索,但要注意它生成的向量只适合做“粗召回”,不适合做“精排序”——原因是它把整句话压成了一个向量,细节信息在池化过程中丢了不少。
参数上,model.encode()里有几个常用开关值得记住:batch_size控制编码速度,显存紧张时调到 32 或 16;convert_to_tensor=True会把结果转成 PyTorch 张量,方便后续和向量数据库的 tensor 接口对接;show_progress_bar=True在数据量大时能看到进度。这些参数在资源后面的 LlamaIndex 接入部分都会用到。
2.2 Bi-Encoder 与 Cross-Encoder:一个管召回、一个管排序
这是 rerank 选型里最核心的架构分叉。Bi-Encoder 把 query 和 passage 分别编码成独立向量,计算相似度时用余弦相似度或内积;Cross-Encoder 则是把 query 和 passage 拼成一句话输入模型,让注意力机制看到两者的完整交互。
from sentence_transformers import CrossEncoder model = CrossEncoder('cross-encoder/stsb-TinyBERT-L-4') scores = model.predict([ ("The weather today is beautiful", "It's raining!"), ("The weather today is beautiful", "Today is a sunny day") ]) print(scores)这段代码的输出是两个浮点数,比如array([0.46552283, 0.6350213], dtype=float32)。注意 CrossEncoder 的输出不是向量、而是相似度分数,分数越高代表语义越匹配。第一对句子一个是好天气、一个是下雨,明显不搭,所以分数低;第二对语义接近,分数高。这就是 rerank 模型的工作方式——对每一对 query-passage 输出一个相关性分数,然后按分数重排。
下面是用 Bi-Encoder 做编码的对比代码:
from sentence_transformers import SentenceTransformer sentences = ["The weather today is beautiful", "It's raining!"] bi_encoder = SentenceTransformer('multi-qa-MiniLM-L6-cos-v1') bi_encoder.max_seq_length = 256 corpus_embeddings = bi_encoder.encode(sentences, convert_to_tensor=True, show_progress_bar=True)这里有个非常关键的参数:max_seq_length = 256。Bi-Encoder 编码长文本时,超过这个长度的 token 会被截断。如果 passage 是 500 字的文档段落,默认的 256 长度会截掉后半部分信息。我一般会按业务文档的实际长度来设置这个值,短 query 长文档的场景建议调到 512。
两类架构的取舍在下表里看得比较清楚:
| 维度 | Bi-Encoder | Cross-Encoder |
|---|---|---|
| 速度 | 快,可预先编码全部语料 | 慢,query 与 passage 必须实时配对计算 |
| 精度 | 一般,向量无法捕捉细粒度交互 | 高,拼接后注意力能看到完整交互 |
| 典型用途 | 第一阶段向量召回 | 第二阶段精排序 |
| 资源消耗 | 低,适合大规模语料 | 高,只能处理小规模候选集 |
实操中我的做法是:用 Bi-Encoder 先从百万级语料里召回 Top-50,再用 Cross-Encoder 对 50 条候选逐条打分,取 Top-5 喂给 LLM。这个“粗召回 + 精排序”的两段式架构,是当前 RAG 系统的主流方案。
2.3 ColBERT:把“拼接交互”和“向量预计算”同时拿到的折中方案
ColBERT 的核心思路是 late interaction——不像 Cross-Encoder 那样在输入层拼接,而是让 query 和 passage 分别过编码器生成 token 级向量,最后用 MaxSim 操作计算相似度。这样做的好处是 passage 的 token 向量可以离线预计算,query 在线编码后直接做矩阵运算。
from sentence_transformers import SentenceTransformer model = SentenceTransformer('colbert-ir/colbertv2.0') passages = [ "艾伦·图灵在1936年提出了图灵机的概念", "图灵测试是判断机器是否具有智能的经典方法" ] passage_embeddings = model.encode(passages, convert_to_tensor=True)ColBERT v2 的论文地址在资源里有,核心改进是把 late interaction 的精度拉到了接近 Cross-Encoder 的水平,同时保留了对大规模语料预计算的能力。实际项目中,如果你的候选集有几十万条、又对精度有要求,ColBERT 是比纯 Cross-Encoder 更现实的选择。
这套模型最适合的场景是“文档段落级别”的检索重排,因为它保留了 token 级语义细节,特别擅长处理同义替换和词序变化。但从工程角度看,ColBERT 的向量存储量比 Bi-Encoder 大不少——每个 passage 要存的不再是一个向量,而是一整组 token 向量。显存和存储成本需要提前算好。
3. 接入 LlamaIndex 全流程:从网页数据到“召回 + rerank + 生成”一次跑通
3.1 环境准备与依赖安装
资源里给了完整的环境搭建命令,我用的是 conda 方式。需要注意 Python 版本必须选 3.10,因为后续的 llama-index 和 torch 版本组合在这个版本上最稳。
conda create -n llmrag python=3.10 conda activate llmrag conda install pytorch==2.3.1 torchvision==0.18.1 torchaudio==2.3.1 pytorch-cuda=12.1 -c pytorch -c nvidia pip install llama_hub llama_index llama-index-readers-web trafilatura pip install llama-index-vector-stores-chroma pip install llama-index-embeddings-huggingface pip install openai pip install llama-index-llms-ollama pip install llama-index-llms-dashscope这里有个细节容易被忽略:llama-index主包和llama-index-readers-web、llama-index-vector-stores-chroma这类子包是分开维护的。只装主包不带子包,运行时会报 ModuleNotFoundError。我建议在项目初期就把用到的子包一次性装全,避免后面一边跑一边补依赖的尴尬。
torch 版本这里我多提醒一句。如果你的机器是 Ampere 架构以上的显卡(30 系、40 系),cuda 12.1 是没问题的;但如果是 20 系或者更老的卡,建议降级到 cuda 11.8 对应的 torch 版本,否则会碰到显存分配失败或者算子不兼容的问题。
3.2 准备数据:网页抓取、切分、向量化写入
资源里用 TrafilaturaWebReader 读取百度百科的 AIGC 词条页面做演示。这一步的意义在于把非结构化的网页文本变成结构化的知识库。
from llama_index.readers.web import TrafilaturaWebReader from llama_index.core.node_parser import SimpleNodeParser from llama_index.embeddings.huggingface import HuggingFaceEmbedding from llama_index.core import VectorStoreIndex, StorageContext import chromadb from llama_index.vector_stores.chroma import ChromaVectorStore def prepare_data(): url = "https://baike.baidu.com/item/AIGC?fromModule=lemma_search-box" docs = TrafilaturaWebReader().load_data([url]) return docs def embedding_data(docs): chroma_client = chromadb.EphemeralClient() chroma_collection = chroma_client.create_collection("quickstart") vector_store = ChromaVectorStore(chroma_collection=chroma_collection, persist_dir="./chroma_langchain_db") storage_context = StorageContext.from_defaults(vector_store=vector_store) node_parser = SimpleNodeParser.from_defaults(chunk_size=500, chunk_overlap=50) embed_model = HuggingFaceEmbedding(model_name="maidalun1020/bce-embedding-base_v1") base_index = VectorStoreIndex.from_documents( documents=docs, transformations=[node_parser], storage_context=storage_context, embed_model=embed_model ) return base_index, embed_model这段代码里三个参数值得展开说。chunk_size=500是切块长度,按字符数计算;chunk_overlap=50是相邻块之间的重叠长度,目的是防止一句话被拦腰截断。这两个值不是拍脑袋定的,我一般会先看语料的平均句子长度——如果文档里全是长段落,chunk_size 要适当加大,否则单个块内语义不完整。
model_name="maidalun1020/bce-embedding-base_v1"是网易有道的开源中文 embedding 模型。相比 OpenAI 的 embedding 接口,本地部署的 HuggingFaceEmbedding 有两个优势:一是数据不出内网,二是没有 QPS 限制。缺点是显存占用,这个 base 版本大约需要 1-2GB 显存,服务器上跑完全没问题。
persist_dir="./chroma_langchain_db"是 Chroma 向量数据库的持久化目录。这个路径一定要写成绝对路径或者明确的相对路径,我第一次跑的时候用过默认路径,结果代码换了启动目录之后向量库“消失”了,所有数据要重新 embedding 一遍,非常浪费时间。
3.3 配置 LLM、召回与 rerank 的完整链路
数据入库存好后,下一步是配置 LLM 并构建查询引擎。资源支持多路 LLM,我用 Ollama 本地部署的方式演示:
from llama_index.llms.ollama import Ollama from llama_index.core import Settings from llama_index.core.postprocessor import SentenceTransformerRerank def get_llm(): llm = Ollama(model="qwen2:7b-instruct-q4_0", request_timeout=120.0) return llm rerank = SentenceTransformerRerank( model="maidalun1020/bce-reranker-base_v1", top_n=3 ) Settings.llm = get_llm() Settings.embed_model = HuggingFaceEmbedding(model_name="maidalun1020/bce-embedding-base_v1") Settings.num_output = 512 Settings.context_window = 3000这里的核心配置是SentenceTransformerRerank。model指定 rerank 底模,资源用的是网易有道的bce-reranker-base_v1;top_n=3表示重排后只保留分数最高的 3 条。这个数字直接影响最终答案质量——取太少可能漏掉关键信息,取太多会让 LLM 的上下文被无关内容污染。
Settings.num_output = 512是 LLM 生成的最大 token 数,Settings.context_window = 3000是上下文窗口大小。这两个值要和 rerank 的top_n配合调整:如果 top_n 取 5 而 context_window 只有 2000,那么 5 条 passage 拼接后可能超出窗口,触发截断。
查询引擎的构建是最后一步:
def generate_answer(question): query_engine = base_index.as_query_engine( similarity_top_k=5, node_postprocessors=[rerank] ) response = query_engine.query(question) print(str(response)) question = "艾伦·图灵的论文叫什么" docs = prepare_data() llm = get_llm() base_index, embed_model, rerank = embedding_data(docs) Settings.llm = llm Settings.embed_model = embed_model generate_answer(question)similarity_top_k=5是第一轮向量召回的数量,top_n=3是 rerank 后保留的数量。这两个参数的差值就是 rerank 发挥筛选作用的空间。我的习惯是让召回数至少是重排数的 2 到 3 倍——召回太少,rerank 无米下锅;召回太多,rerank 的推理时间会线性增长。
资源里还提到了另一种接入方式,用FlagEmbeddingReranker单独处理 rerank 流程:
from llama_index.postprocessor.flag_embedding_reranker import FlagEmbeddingReranker from llama_index.core.schema import QueryBundle reranker = FlagEmbeddingReranker( top_n=3, model="BAAI/bge-reranker-base", ) query_bundle = QueryBundle(query_str=question) ranked_nodes = reranker.postprocess_nodes(nodes, query_bundle=query_bundle)这种方式的灵活之处在于可以手动控制节点列表和 query 的组装,适合做批量评估或者在测试集上离线跑分。而SentenceTransformerRerank这种 postprocessor 方式更适合集成到 query_engine 里做线上推理。
4. 微调不要上来就调:数据格式、训练脚本与开源方案选型
4.1 训练数据格式:triplet 结构怎么组织
微调 rerank 模型的前提是拿到格式正确的训练数据。资源里给的是 SentenceTransformer 生态里通用的 triplet 格式,JSONL 文件按行存储:
{"query": "what are the liberal arts?", "positive": "liberal arts. 1. the academic course of instruction at a...", "negative": "The New York State Education ..."}每一行是一个训练样本,query 是用户问题,positive 是与之相关的文档段落,negative 是不相关的负样本。模型的训练目标就是让 positive 的得分显著高于 negative。
如果希望一个 query 对应多个负样本,可以扩展成如下格式:
{"query": "what are the liberal arts?", "positive": "liberal arts. 1. the academic course of instruction at a...", "negative_1": "The New York State Education ...", "negative_2": "aaaaa...", "negative_3": "bbbbb ...", "negative_4": "ccccc ..."}注意这里的字段名必须严格写成negative_1到negative_N,中间不能有间隔。我在处理真实数据集时吃过亏——自己写的脚本里用了neg1、neg2这类缩写字段,结果InputExample解析时把负样本全部丢掉了,模型的 loss 从头到尾没降过。建议直接照抄标准字段名,不要自作聪明。
数据量上,我的经验是最少准备 5000 条以上 triplet 样本。如果业务数据不够,可以先在公开数据集(比如 MS MARCO)上训练,再用业务数据做少量迭代微调。
4.2 用 SentenceTransformers 框架微调 CrossEncoder
资源里给出了一段非常完整的微调脚本,我直接拿来用了。底模选的是BAAI/bge-small-en,这个模型对英文检索效果不错,中文场景可以换成bge-small-zh。
import os import logging import pandas as pd from torch.utils.data import DataLoader from sentence_transformers import InputExample, LoggingHandler from sentence_transformers.cross_encoder import CrossEncoder DATA_DIR = "G:/ai课程/rag/example/finetuning/data" logging.basicConfig( format="%(asctime)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S", level=logging.INFO, handlers=[LoggingHandler()] ) model_path = "F:/sotaAI/huggingface/models--BAAI--bge-small-en" train_batch_size = 8 num_epochs = 5 model_save_path = "ft_" + os.path.basename(model_path) model = CrossEncoder(model_path, num_labels=1, max_length=512) train_samples = [] dev_samples = [] train_df = pd.read_json(DATA_DIR + "/rerank_train.jsonl", lines=True) val_df = pd.read_json(DATA_DIR + "/rerank_val.jsonl", lines=True) for i, row in train_df.iterrows(): train_samples.append(InputExample(texts=[row["query"], row["positive"], row["negative"]])) for i, row in val_df.iterrows(): dev_samples.append(InputExample(texts=[row["query"], row["positive"], row["negative"]])) train_dataloader = DataLoader(train_samples, shuffle=True, batch_size=train_batch_size) warmup_steps = 100 logging.info("Warmup-steps: {}".format(warmup_steps)) model.fit( train_dataloader=train_dataloader, epochs=num_epochs, evaluation_steps=100, optimizer_params={'lr': 1e-5}, warmup_steps=warmup_steps, output_path=model_save_path, use_amp=True ) model.save(model_save_path)几个关键参数逐个说清楚。num_labels=1表示模型是一个回归任务,输出 0 到 1 之间的连续相似度分数;如果设成 2 或更多,模型会变成分类任务,输出的含义就变了。max_length=512是输入文本的最大 token 数,query 和 passage 拼接后超过这个长度会被截断——长文档场景下这里要特别小心,后面避坑章节我会展开讲。
optimizer_params={'lr': 1e-5}是学习率。这个值在迁移学习里算保守的,因为 CrossEncoder 底模的权重已经在大规模语料上预训练过,学习率太大会把预训练学到的语义知识冲掉。warmup_steps=100是预热步数,让学习率在训练初期先线性上升,避免一开始就震荡。use_amp=True开启混合精度训练,显存紧张时能省接近一半的显存,代价是精度略有损失,但对 rerank 任务影响很小。
这里有件事我需要重点指出:原脚本里evaluator=CERerankingEvaluator(dev_samples, name="train-eval")这段被注释掉了。我建议不要省掉评估器——微调 rerank 模型最重要的就是观察验证集上的排序指标有没有跟上训练 loss。没有评估器,你只能看到 loss 在降,但不知道模型在真实排序任务上表现如何。取消注释后,训练过程中每 100 步会跑一次验证集,输出 MRR 之类的排序指标,这对判断是否早停非常重要。
4.3 换条赛道:FlagEmbedding 微调与 LM_Cocktail 模型合并
如果你的项目跑在 BGE 系列模型上,FlagEmbedding 是更直接的微调工具。安装方式:
pip install -U FlagEmbedding数据格式和刚才的 triplet 类似,但字段略有不同:
{"query": str, "pos": List[str], "neg": List[str]} {"query": str, "pos": List[str], "neg": List[str]}每个样本里 query 是字符串,pos 和 neg 是字符串数组——意味着一个 query 可以对应多个正样本和负样本。这种格式比固定数量的 triplet 更灵活,但要注意数据加载时pos和neg必须保持 list 类型,不能是单独字符串。
训练命令是标准的 torchrun 分布式方式:
torchrun --nproc_per_node 1 \ -m FlagEmbedding.reranker.run \ --output_dir ./rerank_model \ --model_name_or_path BAAI/bge-reranker-base \ --train_data ./toy_finetune_data.jsonl \ --learning_rate 6e-5 \ --fp16 \ --num_train_epochs 5 \ --per_device_train_batch_size 1 \ --gradient_accumulation_steps 4 \ --dataloader_drop_last True \ --train_group_size 16 \ --max_len 512 \ --weight_decay 0.01 \ --logging_steps 10和 SentenceTransformers 微调相比,FlagEmbedding 默认学习率更激进(6e-5 对 1e-5),因为 BGE 系列底模在训练时就用了类似策略。train_group_size=16是每个训练组内包含的样本数(1 个正样本 + 15 个负样本),这个值越大,每个 step 看到的负样本越丰富,模型学到的判别边界越清晰,但显存占用也越高。gradient_accumulation_steps=4是梯度累积步数,显存不够时用这个参数模拟更大的 batch。
微调完成后,资源里还提供了一招非常实用:用 LM_Cocktail 把微调模型和底座模型按权重合并,缓解过拟合。
from LM_Cocktail import mix_models model = mix_models( model_names_or_paths=["BAAI/bge-large-en-v1.5", "your_fine-tuned_model"], model_type='encoder', weights=[0.5, 0.5], output_path='./mixed_model_1' )这个做法的思路是:微调模型可能在业务数据上过拟合、丢失通用语义能力,底座模型泛化好但对业务术语不敏感。按 0.5/0.5 的比例合并参数,相当于在两者之间找一个平衡点。我实测过weights从 0.3/0.7 到 0.7/0.3 的几组配置,业务数据量少于 1 万条时,微调模型权重占比 0.3 到 0.4 效果最好,超过 0.5 反而有下降。
4.4 autotrain:不想写训练脚本时的快速路子
资源里还提到了 HuggingFace 的 autotrain 工具。如果你不想手写上面这些训练脚本,可以启动它的 WebUI:
pip install -U autotrain-advanced autotrain app --port 8080 --host 127.0.0.1之后在浏览器打开127.0.0.1:8080操作界面。和 embedding 微调相比,rerank 微调在 autotrain 里只有两个区别:一是底模要选 rerank 系模型(比如 bge-reranker-base),二是任务类型选 triplet 而不是普通的句子相似度。autotrain 会自动帮我们处理数据加载、超参配置和训练循环,适合快速验证数据质量。不过我不建议在 autotrain 里做最终的模型训练,因为它的可定制性不如直接跑脚本,而且一旦训练中断,断点续训的机制不如原生训练脚本可靠。
5. 一路踩过来的坑:常见问题与排查笔记
5.1 模型下载永远卡住,或者反复重新下载
现象:跑SentenceTransformer('BAAI/bge-reranker-base')时每次都从头开始下载,或者下载到一半就报错超时,重启后又要重新来。
原因:HuggingFace 的模型缓存目录.cache/huggingface/hub里已经有部分下载文件,但断点续传经常不生效,导致已下载的分片被判定为无效、反复重下。另一个常见场景是服务器上没有外网权限,代码跑在离线环境里,但模型文件又没有提前下载到本地。
解决:我现在的标准操作是先用snapshot_download把模型完整拉到本地目录,然后在代码里设置离线模式加载。
python -c "from huggingface_hub import snapshot_download; snapshot_download(repo_id='maidalun1020/bce-reranker-base_v1', local_dir='./models/bce-reranker')"加载模型时指定本地路径,同时设好离线环境变量:
import os os.environ["TRANSFORMERS_OFFLINE"] = "1" rerank = SentenceTransformerRerank(model="./models/bce-reranker", top_n=3)5.2 top_n 设置比召回数还大,rerank 形同虚设
现象:rerank 跑完发现结果顺序和召回顺序完全一样,或者日志里根本没有 rerank 的耗时,整个 query 的响应时间也没增加。
原因:similarity_top_k设置为 2,但SentenceTransformerRerank的top_n设置为 3。rerank 的输入只有 2 条候选,它按分数排序后还是只有 2 条,等于没用。LlamaIndex 对这种情况不会报错,只会安静地和稀泥。
解决:把similarity_top_k调到top_n的 2 到 3 倍。比如 rerank 保留 3 条,召回就取 5 到 10 条。这样 rerank 才有足够的候选空间做筛选。
5.3 加了 rerank 之后答案反而变差了
现象:在同样的数据集上,不做 rerank 时 LLM 的回答还算靠谱,加了 rerank 后回答开始文不对题。
原因:最常见的是max_length截断问题。CrossEncoder 默认的max_length是 512,如果你从向量库召回的是 800 字的段落,拼接后超过 512 的尾部直接被截掉,截断位置恰好包含答案时,模型给出的分数就不可靠了。另一个原因是 rerank 模型的训练域和你的业务文本域差别太大——比如用英文语料训练的 rerank 模型跑中文文本,分数的区分度很低。
解决:微调时显式设置max_length=768或更大,并且用验证集检查一下长度分布。如果业务文本普遍超过模型长度上限,优先做更细致的切块——把段落控制在 300 到 400 字以内,而不是拉长模型的 max_length。
5.4 微调时 loss 不降,或者验证集分数一路走低
现象:训练 5 个 epoch 后训练 loss 还在高位,或者训练 loss 在降、但验证集的排序指标越来越差。
原因:前者多半是数据格式问题——字段名不对、正负样本标签反了,模型在努力拟合错误信号;后者典型过拟合——训练数据太少而 epoch 太多,或者学习率太大导致权重震荡。还有一种数据层面的问题:同一份训练数据里,同一个 query 的 positive 和 negative 文本高度相似,模型缺乏可学的判别信号。
解决:先用几十条数据跑一个 epoch,确认 loss 能明显下降,再铺开全量数据;验证时把evaluator=CERerankingEvaluator(dev_samples, name="train-eval")打开,观察 MRR 指标变化。出现过拟合时,把num_epochs从 5 降到 2 或 3,学习率从 1e-5 降到 5e-6,一般能压住。
5.5 评估脚本报错:MIRACLReranking 任务名称不存在
现象:按 MTEB 文档抄的评估脚本,指定tasks=["MIRACLReranking"]时直接抛 KeyError,或者跑起来之后数据集的领域和预期不符。
原因:MTEB 的主任务列表里是Reranking这个大类,MIRACLReranking是其中的一个具体数据集。直接传tasks=["Reranking"]会把该类别下所有 reranking 数据集全部拉下来,数量多、耗时长;但传单个数据集名称时又容易因为大小写或版本号不对而报错。
解决:先用tasks=["Reranking"]跑一次,看输出里的数据集列表,再按具体数据集名称单跑。另外注意,中文场景应该用 C-MTEB 里的T2Reranking数据集,不要直接用 MTEB 的英文 reranking 集合替代。
6. 评估与进阶:MTEB、C-MTEB 和 autotrain 组合出招
6.1 跑通 MTEB 评估,别让指标变成黑匣子
模型微调完,最需要知道的是“比底模强了多少”。MTEB 是当前事实标准的 embedding 和 rerank 评估框架。这段代码可以直接用来跑英文 rerank 评估:
from mteb import MTEB from sentence_transformers import SentenceTransformer model_name = "zhengquan" model = SentenceTransformer(model_name) evaluation = MTEB(tasks=["MIRACLReranking"]) results = evaluation.run(model, output_folder=f"results/{model_name}")运行时会自动下载 MIRACL 数据集,对模型进行 reranking 任务评估,最终在results/目录下输出详细指标。要跑全量 rerank 任务,把参数换一下:
evaluation = MTEB(tasks=["Reranking"])中文场景则需要使用 C-MTEB:
pip install -U C_MTEBfrom mteb import MTEB from C_MTEB import T2Reranking from sentence_transformers import SentenceTransformer model_name = "bert-base-uncased" model = SentenceTransformer(model_name) evaluation = MTEB(tasks=['T2Reranking']) results = evaluation.run(model, output_folder=f"zh_results/{model_name}")评估完之后,最值得对比的三个指标是:NDCG@10(排序质量的核心指标)、MAP(平均精确率)和推理延迟。我习惯把微调前和微调后的模型在同一数据集上各跑一遍,指标放在一个表里对比——如果 NDCG@10 的提升不超过 2 个点,说明微调数据质量有问题,或者底模选得不对,调整数据比继续调参更重要。
6.2 组合打法:先评估、再合并、后上线
这套流程走通之后,我现在的标准操作顺序是:基线模型在 C-MTEB 上跑出指标 → FlagEmbedding 微调得到业务特化模型 → LM_Cocktail 按不同权重合并 → 合并后的模型再跑同一评估集 → 选择 NDCG@10 最高的权重组合。这样做的好处是把“模型好坏”从感觉层面变成了数字对比,每次迭代都有据可依。
关于评估数据集的规模,建议至少保留 1000 条 query 和对应的候选文档。数据太少,NDCG 的置信区间太宽,两个模型之间 1 个点的差距根本说明不了问题。还有一点:评估集最好和微调数据集来自同一个业务分布,否则评估结果只能反映模型的通用能力,不能代表线上效果。
从那以后,我每次微调 rerank 模型都会强制走一遍“微调 → LM_Cocktail 合并 → C-MTEB 评估 → 对比基线”的完整流程,再决定要不要上线。这一套流程能帮你省下大量线上试错的时间——先让数据说话,再谈直觉和玄学。希望这套思路对你的 rerank 项目也有帮助。
本文还有配套的精品资源,点击获取