用户搜“忘了密码怎么找回”,库里有两段文字:
- 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 · 官方代码

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。
实际换模型当然不只是旋转,这个例子只说明一件事:维度相同,不代表坐标含义相同。 新模型应配套重建文档向量和索引;若确实要混用,必须有经过训练与验证的兼容方案,不能靠维度检查放行。
建议在索引旁保存一份可比较的配置:
模型与 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
- 两个向量对应分量相乘再求和,双塔的打分方式