
为多语言 RAG 挑选并验证嵌入模型:BGE-M3、multilingual-e5 与 OpenAI 嵌入的实测对比流程
为多语言 RAG 挑选并验证嵌入模型:BGE-M3、multilingual-e5 与 OpenAI 嵌入的实测对比流程
RAG 的检索质量首先取决于嵌入模型这把「尺子」是否贴合你的语料。本文按提供形态、语言覆盖、维度与成本三条线索梳理 OpenAI text-embedding-3、BGE-M3、multilingual-e5、paraphrase-multilingual-MiniLM 等常见选择,给出前缀、归一化、重建索引的正确用法,并用一段可运行的 Python 脚本演示如何在自己的数据上以 Hit Rate、MRR 比较候选模型。
RAG 的检索质量在很大程度上由嵌入模型(embedding model)的性质决定。嵌入模型是衡量文本语义接近程度的「尺子」:如果这把尺子本身不适合你的文档,那么无论分块策略和检索方式怎么优化,真正正确的片段也不会排到前面。选型时常见的困境是「到底该选哪一个」——本文按提供形态、语言覆盖、维度与成本三条线索梳理常见模型的定位,并给出在自有数据上做评估的具体步骤,帮助你把候选缩小到两三个,再用自己的评测集做最终决定。
准备工作
在动手比较之前,先把下面几件事确认清楚。这些前置条件决定了你能否公平地比较不同模型。
1. 明确数据能否离开你的环境
这是第一个分叉点,也是很多团队真正的决策依据。如果文档中包含内部规章、个人信息或其他不宜外传的内容,提供形态往往比模型分数更重要。
- API 型(OpenAI、Cohere、Google 等):无需自己运维,质量稳定。代价是需要把数据发送到外部,按用量计费,并且存在模型停止提供(deprecation)的风险。
- 开源型:下载权重后在本地或自有服务器上运行。数据不出环境,推理成本相对固定;代价是要自备 GPU 与内存,更新和监控也由自己负责。
2. 准备运行环境
本文的对比脚本基于 sentence-transformers,它可以直接加载 Hugging Face 上的开源嵌入模型。需要准备:
- Python 环境,以及
sentence-transformers、numpy两个包; - 能够下载模型权重的网络与磁盘空间(不同模型体积差异较大);
- 若使用 API 型模型,需要相应的 API 密钥,并按该服务商的当前文档配置客户端。
安装命令:
pip install sentence-transformers numpy
3. 准备评测用的文档与问题
评估必须发生在你自己的数据上。公开榜单(如 MTEB)只能用来缩小候选范围,不能保证模型在你的日文或中文内部文档上同样表现良好。建议准备:
- 一批来自真实业务的文档片段,覆盖你实际会检索的内容类型;
- 约 50 条贴近真实用户提问方式的问题,其中混入同义改写和表记差异(例如同一概念的不同写法);
- 为每个问题标注出正确的片段(正解 chunk)。
4. 固定其他变量
比较模型时,分块方式和 top-k 必须保持一致。否则你测到的差异可能来自分块,而不是模型本身。
操作步骤
第一步:按提供形态筛掉一半候选
先回答「数据能不能发出去」。不能发出去,就只在开源模型里选;可以发出去,再考虑 API 型带来的运维便利。这一步通常就能把候选清单砍掉一半。
第二步:了解常见模型的定位
| 模型 | 形态 | 维度 | 定位 |
|---|---|---|---|
| text-embedding-3-small | API | 1536(可缩短) | 通用、易用 |
| text-embedding-3-large | API | 3072(可缩短) | 重视质量时的选择 |
| BGE-M3 | 开源 | 1024 | 多语言(含日文),支持长文本;除稠密向量外还能处理稀疏向量与多向量 |
| multilingual-e5-large | 开源 | 1024 | 多语言;查询与文档需要使用不同的前缀 |
| paraphrase-multilingual-MiniLM-L12-v2 | 开源 | 384 | 轻量,适合做原型验证 |
需要特别留意「名字相似但目标语言不同」的模型。例如 bge-large-zh-v1.5 是中文场景的常见选择,但并不适合日文文档。日文用途应优先考虑 BGE-M3 这类多语言模型。另外,维度数可能因发布版本而异,使用前务必查看模型卡(model card)确认。
第三步:从学习数据和模型卡判断语言适配
「支持多语言」这个说法并不保证每种语言都被充分训练。学习数据在不同语言之间的分布是不均衡的,在英文基准上得分高的模型,未必在日文上同样有效。逐项确认以下内容:
- 学习数据是否包含目标语言,是否在该语言的检索任务上做过评估;
- 最大输入长度——同样的内容,日文往往比英文占用更多 token,因此要按 token 数而不是字符数确认;
- 是否需要查询前缀与文档前缀。
multilingual-e5系列规定查询加query:、文档加passage:,漏加会与训练时的输入格式不一致,导致性能下降; - 许可证是否允许商用。
第四步:权衡维度与成本
维度直接关系到存储量和延迟。以 float32 保存、10 万个 chunk 为例:
- 1024 维:约 410MB
- 1536 维:约 614MB
- 384 维:约 154MB
查询时的相似度计算量也随维度线性增长。但维度高并不等于质量高:检索质量由学习数据和训练目标决定,通用基准上表现好的模型,在你的内部术语和语言习惯上排名可能反转。不要预设「维度越大质量越高」,而要用后面的评估来验证。
另外,OpenAI 的 text-embedding-3 系列可以通过 dimensions 参数缩短输出维度,从而压低存储量。缩短后对质量的影响,需要在你自己的数据上确认。各模型的价格、配额与可用维度请以服务商官网当前信息为准。
第五步:正确使用模型——前缀、归一化、重建索引
同一个模型,用法错了同样出不了效果。三类典型失误:
- 前缀用错:把查询用的格式加到文档上,或反过来,输入形式与规定不符会让相似度失真。
- 归一化不一致:只在建索引时做归一化(或只在查询时做),相似度的尺度就变了。保存前和检索前必须施加相同的处理。
- 换模型后没有重建索引:换模型意味着向量空间本身变了。如果只把查询侧换成新模型,就是在比较两个不同空间里的值,检索结果没有意义。更换时必须对所有 chunk 重新做嵌入。
第六步:用脚本做小规模对比
下面这段脚本用同一批文档和同一个查询,把候选模型并排比较。注意每个模型对应的(查询前缀,文档前缀)是分别指定的。
from sentence_transformers import SentenceTransformer
import numpy as np
docs = [
"リモートワークは週3日までです。",
"経費精算の締め切りは月末です。",
"交通費は領収書を添付して申請します。",
]
query = "在宅勤務は週に何日まで認められていますか。"
# モデルごとに(クエリ用接頭辞, ドキュメント用接頭辞)を指定する
models = {
"BAAI/bge-m3": ("", ""),
"intfloat/multilingual-e5-small": ("query: ", "passage: "),
}
for name, (q_prefix, d_prefix) in models.items():
model = SentenceTransformer(name)
doc_vecs = model.encode(
[d_prefix + d for d in docs],
normalize_embeddings=True,
)
query_vec = model.encode(
q_prefix + query,
normalize_embeddings=True,
)
scores = doc_vecs @ query_vec
print(f"\n{name}({doc_vecs.shape[1]} 次元)")
for i in np.argsort(-scores):
print(f" {scores[i]:.3f} {docs[i]}")
这段代码只用于缩小候选范围。几条结果不足以判定优劣,真正的结论要来自几十条以上、贴近实际使用的提问,并用正解 chunk 上的 Hit Rate 或 MRR 来衡量。
一个完整示例
下面把前面的步骤串成一个可执行的最小流程:从候选模型出发,用带正解标注的小评测集算出 Hit Rate 与 MRR,据此决定最终采用哪个模型。
准备评测数据
把文档片段放进一个列表,再为每条问题标注它对应的正解片段下标。问题要混入同义改写和表记差异,例如把「リモートワーク」问成「在宅勤務」。
docs = [
"リモートワークは週3日までです。",
"経費精算の締め切りは月末です。",
"交通費は領収書を添付して申請します。",
"有給休暇は入社半年後から付与されます。",
"経費の申請は専用システムから行います。",
]
# (質問, 正解チャンクのインデックス)
eval_set = [
("在宅勤務は週に何日まで認められていますか。", 0),
("経費精算はいつまでに済ませる必要がありますか。", 1),
("交通費を申請するとき何を添付しますか。", 2),
("有給はいつからもらえますか。", 3),
("経費はどこから申請しますか。", 4),
]
计算 Hit Rate 与 MRR
Hit Rate 衡量正解片段是否出现在前 k 条结果中;MRR 是正解排名倒数的平均值,排名越靠前分数越高。两个指标都在 top-k 一致的前提下比较。
from sentence_transformers import SentenceTransformer
import numpy as np
TOP_K = 3
models = {
"BAAI/bge-m3": ("", ""),
"intfloat/multilingual-e5-small": ("query: ", "passage: "),
}
for name, (q_prefix, d_prefix) in models.items():
model = SentenceTransformer(name)
doc_vecs = model.encode(
[d_prefix + d for d in docs],
normalize_embeddings=True,
)
hits = 0
rr_sum = 0.0
for question, gold_idx in eval_set:
query_vec = model.encode(
q_prefix + question,
normalize_embeddings=True,
)
scores = doc_vecs @ query_vec
ranked = np.argsort(-scores)[:TOP_K]
if gold_idx in ranked:
hits += 1
rank = int(np.where(ranked == gold_idx)[0][0]) + 1
rr_sum += 1.0 / rank
n = len(eval_set)
print(f"{name}({doc_vecs.shape[1]} 次元)")
print(f" Hit Rate@{TOP_K}: {hits / n:.3f}")
print(f" MRR@{TOP_K}: {rr_sum / n:.3f}")
解读结果并做决定
把候选模型逐个跑一遍,记录维度、Hit Rate 与 MRR。选择时同时考虑三件事:
- 检索指标是否明显优于其他候选;
- 维度带来的存储与延迟成本是否可接受;
- 提供形态是否满足数据合规要求。
如果两个模型指标接近,优先选维度更低、运维更简单的那一个。如果指标差距明显,再回到模型卡确认语言覆盖和许可证,然后固定该模型,并对全部 chunk 重新做一次嵌入。
注意事项
- 不要用公开榜单直接下结论。MTEB 之类的基准适合缩小候选范围,但不能保证模型在你的内部文档上表现一致。最终判断必须来自自有评测集。
- 评测集规模要够。几条查询的结果不足以区分模型,建议至少准备约 50 条贴近真实使用的问题,并覆盖同义改写与表记差异。
- 比较时固定分块与 top-k。否则测到的差异可能来自分块策略而非模型。
- 前缀不能省。
multilingual-e5系列要求查询加query:、文档加passage:,漏加会与训练时的输入形式不一致,性能下降。 - 归一化前后一致。建索引与查询必须使用相同的归一化处理,否则相似度尺度不一致。
- 换模型必须全量重建索引。向量空间随模型改变,只更新查询侧会让检索结果失去意义。
- 维度以模型卡为准。同一模型的不同发布版本维度可能不同,使用前确认。
- 注意语言错配。名字相近的模型可能面向不同语言,例如面向中文的模型不适合日文文档。
- 确认许可证。开源模型在商用前要核对许可证条款。
- 易变信息以官网为准。模型价格、配额、可用维度、是否停止提供等,请以服务商或模型发布方的当前信息为准。
嵌入模型没有「永远最优的那一个」。可行的顺序是:先按数据能否外发决定提供形态,再通过模型卡确认语言适配并把候选缩到两三个,最后用自己的评测数据做决定。守住这个顺序,就能避免「先随便选个有名的模型然后一直用下去」这类失误。