用户搜“忘了密码怎么找回”,库里有两段文字:

  • A:“在登录页选择重置密码,通过验证后设置新密码。”
  • B:“修改头像前,请先输入密码。”

两段都出现了“密码”,但只有 A 回答了问题。Embedding 检索(retrieval)想学会这种区别:不只看词有没有出现,还要看这段话能不能满足查询。 这不表示模型一定会排对;上面是我们希望检验的相关性判断,不是实测输出。

如果只是想知道两个模型差在哪儿,读前 3 节即可。准备接入项目,再看输入与索引;想检查实现,直接去小实验。不用先弄懂每一个模型家族。

1. 先看共同点:文档提前算,问题来了再算

选择稠密检索(dense retrieval)路径时,两者都能把一段文本变成一个向量。文档先编码并放进索引;查询来了,编码查询,再找相近的文档向量。这就是上一篇的双塔检索。用同一个模型分别编码两边,也叫双塔(dual-encoder),并不要求加载两套不同权重。

离线:A「重置密码的方法」 → 文档向量 A ┐
      B「修改头像的要求」 → 文档向量 B ┴→ 文档索引

请求:「忘了密码怎么找回」 → 查询向量 → 查索引 → 候选 A、B → 重排

这里换 embedding 模型,是在改变文本如何变成向量,不是把检索替换成聊天生成。也别把 embedding 模型和同家族的 reranker 混在一起:后者通常要同时看问题和候选(candidate),用来对较小的一批结果精排(ranker)。

2. 先把具体型号对齐

这一页固定比较 Qwen/Qwen3-Embedding-0.6B 和 BAAI/bge-m3,不是整个 Qwen 或 BGE 家族。比如 BGE v1.5 和 BGE-M3 的长度与输入约定就不一样。

核对项 Qwen3-Embedding-0.6B BGE-M3
主体结构 Qwen3 解码器,因果注意力 XLM-RoBERTa 编码器(encoder),双向注意力
一段文本怎样得到 dense 向量 取最后有效 token 的最终隐藏状态(hidden state) 取开头分类标记 CLS 的最终隐藏状态
完整 dense 维度 1024;支持 MRL 缩维 1024;不要默认支持任意截维
公布的输入上限 32K tokens 8192 tokens
检索输入约定 query 带任务指令;document 不带该指令 官方 M3 用法不要求给 query 加指令
本页比较的输出 单个 dense 向量 dense;也可另开 sparse 与 multi-vector 路径

架构与维度可核对 Qwen 配置和 BGE-M3 配置;输入与使用方式见 Qwen 模型卡和 BGE-M3 模型卡。这些是具体版本的公开能力,不是本站测速。

为什么一个取末尾,一个取开头?在因果注意力(causal attention)中,末尾可以看到前面的内容;开头位置看不到后面的整句话。双向 encoder 的 CLS 则能聚合后面的内容。把一串 token 的状态变成一个向量,这一步叫池化(pooling),要沿用训练时的约定,不是看哪个位置方便就拿哪个。也不能仅凭“decoder”或“encoder”判断检索质量。

比如 query 是“密码怎么找回”,任务指令还可以说明“查找能直接回答用户问题的帮助文档”。这是告诉模型怎样表示这句话,不是让它先写出答案。两者最终都要经过针对表示学习的训练;直接从普通聊天模型中抽一个 token,不等于得到同样的检索器。

为什么会有这两种设计?

BGE-M3 在 2024 年提出时,想解决的不只是单个榜单上的分数:语言、输入长度和检索方式往往需要分别适配。M3 尝试把这几个需求放进同一个模型。因此,要理解它为什么保留三种输出,先看这个问题,而不是只看 dense 向量。原论文 §1、§3 · 官方代码

M3-Embedding(BGE-M3)的多阶段训练和自知识蒸馏。
原文图M3-Embedding(BGE-M3)的多阶段训练和自知识蒸馏。图源:Chen et al., 2024 · M3-Embedding · Figure 2 · CC BY 4.0 · 转为 WebP

Qwen3 Embedding 的 2025 年报告,则利用 LLM 同时做表示骨干和合成数据来源,再经过弱监督训练、高质量数据微调(fine-tuning)与 checkpoint merging。它不是简单把 encoder 换成 decoder;数据和训练配方也是设计的一部分。原论文 §3.2–3.3 · 官方代码

这些背景解释了设计取向,不代表后来发布的模型必然更适合你的项目。跨论文的分数也不能直接拿来证明架构优劣:数据、模型规模、输出路径和评测设置都可能不同。实际选型仍要做第 7 节的同条件实验。

数据从哪儿来,模型到底学什么?

不能把区别简化成“BGE 只做掩码预测,Qwen 才会学语义”。骨干的预训练(pre-training)和后来的检索训练是两件事;两份报告都用了对比学习(contrastive learning),也都用了合成数据,只是做法不同。

训练环节 Qwen3 Embedding BGE-M3
初期检索训练 大量合成配对数据提供弱监督 从多语言语料提取配对关系,先训练 dense 路径
后续训练 高质量监督数据加筛选后的合成数据 标注与合成数据、困难负例,联合训练三条路径
额外做法 微调后的多个 checkpoint 做 merging 三条路径的组合分数作为自蒸馏(distillation)信号

上表对应 Qwen3 Embedding §3.1–3.3与 BGE-M3 §3.1、§3.3,不是对整个模型家族的概括。Qwen 报告里 embedding 的微调仍用对比目标;reranker 才以 yes/no 为生成标签。不能因为两者都叫 fine-tuning,就把训练代码混用。

回到密码例子:可以从一段找回密码的文档生成不同问法,补充真实 query 不够多的问题。但如果生成的问题直接泄露答案,或全是实际用户不会说的话,多出一百万对也未必有用。先抽样检查配对质量,再用没参与生成和筛选的真实查询检验迁移效果。

自蒸馏则是另一种补信号的方法。假设三条路径对同一批候选各有判断,把它们组合成 teacher 分布,让每条路径学习这份软目标。训练时借助其他路径,不等于部署 dense 路径时自动拥有三路搜索。 Teacher 也可能一起犯错,最终仍要回到独立相关性评估。

3. M3 的三种输出,不是三个免费的分数

继续看密码例子:整句话在说什么、是否提到准确的错误码、问题的每个局部要求有没有对应内容,是几种不同的匹配线索。

路径 保留什么 要准备什么
Dense 一段文本的一个向量 向量索引(vector index);容易接现有 ANN 系统
Sparse 文中 token 对应的学习式权重 稀疏索引;不是把 dense 向量里的小数简单置零
Multi-vector 多个 token 的向量 更多存储与匹配计算,或只对已有候选计算

BGE-M3 论文 §3.2给出这三条路径。Sparse 是学习得到的词项权重,不是 BM25 的词频公式。Multi-vector 使用 late interaction:每个 query token 找到最匹配的 document token,再汇总。它保留更细的匹配信息,但仍不是两段文本联合编码的 cross-encoder。

即使共享一次编码,额外输出仍有存储、传输和检索成本。只调用 dense 接口,不会自动建立另两个索引,也不会自动获得三路融合的效果。

到这里,先记住:两者都有单向量路线;M3 还提供其他匹配方式,Qwen 这里支持任务指令和可选维度。选谁,要看你的查询、文档和资源,而不是名字新不新。 后面的内容主要是把这些约定落实到实现。

4. 输入、pooling 和索引得一起对上

最容易忽略的不是模型大小,而是“你实际上喂了什么”。Qwen 的 query 模板包含 Instruct: 和 Query:;官方示例让 document 保持原文。BGE-M3 不沿用这套模板。把两者的 prompt、pooling 和 padding 拼着用,shape 可能全对,分数却已经不是原来的含义。

Padding:末尾不一定是最后一个字

下面的方块表示已经完成分词和特殊 token 处理的有效位置,不是原始中文的逐字 tokenization。

批处理序列 attention mask 应取的位置(从 0 开始)
[有效1, 有效2, 有效3, PAD, PAD] [1, 1, 1, 0, 0] 2
[PAD, PAD, 有效1, 有效2, 有效3] [0, 0, 1, 1, 1] 4

两行有同样多的有效 token,所以 mask.sum() - 1 都是 2,只对这里的右填充行取对了位置。hidden[-1] 则只适合左填充行。通用检查是找 mask 中最后一个 1;整行全是 padding 时应报错,不能拿一个无意义向量继续索引。

Qwen 论文把表示位置描述为序列末尾的 EOS;接入时还要核对实际 tokenizer、特殊 token 和封装库的行为。下面的小实验只验证“选中最后有效位置”,不验证真实模型的 tokenization,也不建议自己随手添加 EOS。

同样是 1024 维,为什么不能直接换?

看一个二维例子。旧系统中,问题向量为 $(1,0)$,正确文档 A 是 $(1,0)$,文档 B 是 $(0,1)$;点积(inner product)分别为 1、0。

假设新模型只是把所有向量一起转了 90 度。它自己的检索完全没变:新问题 $(0,1)$ 对新 A $(0,1)$ 得 1,对新 B $(-1,0)$ 得 0。但如果只上线新 query encoder,继续查旧索引,分数会变成 A 为 0,B 为 1。

动画 维度一样,坐标不一定对得上

这张图需要启用 JavaScript。

实际换模型当然不只是旋转,这个例子只说明一件事:维度相同,不代表坐标含义相同。 新模型应配套重建文档向量和索引;若确实要混用,必须有经过训练与验证的兼容方案,不能靠维度检查放行。

建议在索引旁保存一份可比较的配置:

模型与 tokenizer 的 revision
query / document 的模板和特殊 token 处理
pooling、输出维度、归一化、距离函数
切块规则、截断规则、文档快照
向量存储精度、索引参数、编码库版本

不仅换模型需要检查:微调权重、改变切块或文档模板,也可能要求重新编码。发布时 query 服务和索引应成对切换,并保留旧的一对用于回滚。

5. 向量变短,会省在哪里?

MRL(Matryoshka Representation Learning)让模型训练时就照顾向量的前缀表示。对支持它的模型,可以按约定取前若干维,再重新归一化;不是从任意 embedding 中随便抽掉几维都行。

归一化后向量如果是 $(0.6,0,0.8)$,保留前两维得到 $(0.6,0)$,其长度是 0.6,不再是 1。重新归一化后应是 $(1,0)$。忘记这一步,用点积算出的就不再是所保留表示的余弦相似度。前缀恰好全为 0 时,需要明确报错或采用已约定的处理,不能除以 0。

对一百万段文档,只计算连续数组中的向量数据:

存储方式 字节数 约多少 GiB
1024 维,FP32 4,096,000,000 3.815
1024 维,FP16 2,048,000,000 1.907
256 维,FP16 512,000,000 0.477

这里 $1\ \mathrm{GiB}=2^{30}$ 字节,不包含 ANN 图、文档 ID、元数据、备份与工作内存。后两行相差 4 倍的是原始向量体积,不是整个系统成本;只在输出端截维通常也不会让 encoder 前向计算减少 4 倍。

缩维会不会丢掉对任务重要的信息,需要看保留维度下的结果。先测精确向量排序,再测 ANN,才能分开看表示损失与索引近似造成的漏召回。向量索引继续讲存储与近似搜索。

6. 不下载模型,也能先查的几个小实验

这一段只用标准库,检查 pooling、归一化、坐标混用和 late interaction 的计算。它不替代真实模型评测,也不包含训练或 GPU 性能实验。

from embedding_contracts import last_valid_pool, normalized_prefix, mean_maxsim

states = [[9, 9], [9, 9], [1, 0], [2, 0], [3, 4]]
pooled = last_valid_pool(states, [0, 0, 1, 1, 1])
assert pooled == [3.0, 4.0]
assert normalized_prefix(pooled) == [0.6, 0.8]
assert normalized_prefix([0.6, 0, 0.8], 2) == [1.0, 0.0]

score = mean_maxsim([[1, 0], [0, 1]], [[0.8, 0.6], [0, 1]])
assert abs(score - 0.9) < 1e-12

最后一行怎么来的?两个 query 向量与两个 document 向量的相似度为:

相似度 文档 token 1 文档 token 2 该 query token 的最大值
查询 token 1 0.8 0 0.8
查询 token 2 0.6 1 1

取每行最大值后求平均,得到 $(0.8+1)/2=0.9$。它不要求两个词按相同顺序出现,也不要求每个 query token 匹配到不同的文档 token(几个 query token 可以落在同一个文档 token 上);它仍可能漏掉否定、顺序和跨段关系。

完整实现见 embedding_contracts.py。上述 import 需要把 04-search/code 加到 Python 路径;从仓库根目录可以直接运行完整示例和测试:

python3 04-search/code/embedding_contracts.py
python3 -m unittest discover -s site/tests -p 'test_embedding_contracts.py'
Last valid: [3.0, 4.0]
Mixed spaces: [0.0, 1.0]; rebuilt: [1.0, 0.0]
Mean MaxSim: 0.9

数值代码只演示约定;真实 BGE-M3 的 token 投影、特殊 token 排除与 attention mask 仍要用匹配版本的实现。mean_maxsim 的时间为 $O(QDd)$,其中 $Q,D$ 是有效 token 数、$d$ 是向量维度;这里逐行计算最大值,不创建整张 $Q\times D$ 分数表,但输入表示本身也要占内存。

7. 真要选型,先做一个能解释结果的比较

先比较两者的 dense-only 路径。之后再给 M3 加 sparse 或 multi-vector;否则你无法判断提升来自 encoder,还是来自额外一条检索路线。

问题 实验怎么做 能回答什么
表示本身好不好? 同一语料与标注,精确搜索,固定 K 暂时排除 ANN 的近似误差
模型有没有用上细节? 对比“重置密码”与“不能重置密码”等小改写,另看真实查询 给出排错线索,不把少数手造题当总分
长上下文值不值得? 同一原文,记录各 tokenizer 的实际 token 数、截断率与证据是否保留 区分读到了更多内容与更会匹配
多路检索是否有用? 固定最终候选总数,记录独有相关结果、去重和重排成本 不是单纯因为多拿了结果
能否部署? 同一硬件与流量条件下记录 query 延迟、吞吐、显存、重建耗时 在具体 workload 下比较成本

两套 tokenizer 的“1000 tokens”不一定覆盖同样多的原文。可以先用同样的文档片段做内容可比实验,再各用推荐配置做部署实验;两组结果分开报告。任务指令、输出维度、融合权重在开发集上选择,最终测试集不要反复拿来调参。

还要看平均分下面的差别:短 query、中文、英文、跨语言、代码标识符、长文档,是否有某一类明显退步。人工相关性标注与点击都可能不完整,未标为相关不等于确认不相关。排行榜帮助选候选模型,不能替你完成这些检查。

如果现有 dense 系统已经能找到大多数有效证据,先查遗漏、切块和重排,不必为了换新模型重建整个系统。如果经常遗漏准确名称或错误码,可以先加 BM25作为对照。需要多路结果时,再看融合与重排,把收益和成本一起算清楚。

本页术语 13
检索 retrieval
从外部语料里找出相关内容
稠密检索 dense retrieval
用向量相似度而非关键词匹配来检索
双塔 dual-encoder
用户和内容各一个编码器,映到同一向量空间靠内积召回
候选 candidate
召回阶段捞出来、等待精排挑选的内容
精排 ranker
第二阶段,对少量候选做充分交互后精细排序
编码器 encoder
把原始输入压成向量的那一半模型
隐藏状态 hidden state
递归模型在时间步之间传递的压缩表示
微调 fine-tuning
在预训练权重上继续训练以适配具体任务
预训练 pre-training
先用大量数据学习通用表示和能力,再适配具体任务
对比学习 contrastive learning
拉近正样本对、推开负样本对来学习表示
蒸馏 distillation
用一个模型的输出训练另一个通常更小的模型
向量索引 vector index
预先算好的内容向量集合,支持近似最近邻查询
点积 inner product
两个向量对应分量相乘再求和,双塔的打分方式