AB
AiBoss
チュートリアル

为多语言 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-smallAPI1536(可缩短)通用、易用
text-embedding-3-largeAPI3072(可缩短)重视质量时的选择
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: ,漏加会与训练时的输入形式不一致,性能下降。
  • 归一化前后一致。建索引与查询必须使用相同的归一化处理,否则相似度尺度不一致。
  • 换模型必须全量重建索引。向量空间随模型改变,只更新查询侧会让检索结果失去意义。
  • 维度以模型卡为准。同一模型的不同发布版本维度可能不同,使用前确认。
  • 注意语言错配。名字相近的模型可能面向不同语言,例如面向中文的模型不适合日文文档。
  • 确认许可证。开源模型在商用前要核对许可证条款。
  • 易变信息以官网为准。模型价格、配额、可用维度、是否停止提供等,请以服务商或模型发布方的当前信息为准。

嵌入模型没有「永远最优的那一个」。可行的顺序是:先按数据能否外发决定提供形态,再通过模型卡确认语言适配并把候选缩到两三个,最后用自己的评测数据做决定。守住这个顺序,就能避免「先随便选个有名的模型然后一直用下去」这类失误。