0. 阅读导引 (๑•̀ㅂ•́)و✧
欢迎来到「AI 基础设施三部曲」的终章!
如果你曾经被这些问题困扰:
- “Embedding 到底是什么魔法?一段文字怎么就变成了 1024 个数字?” 🤔
- “BGE、GTE、E5、Jina、Cohere…几十个模型名字背都背不过来,到底该选哪个?” 😵💫
- “为什么我的 RAG 检索效果这么差?是不是 Embedding 模型不行?” 📊
- “英文 Embedding 好的模型,中文就一定好吗?多语言模型到底怎么测?” 🌐
- “什么叫对比学习?InfoNCE 损失函数到底在优化什么?” 🧮
那这篇文章就是为你准备的。
读完你会获得什么?
- 从矩阵运算到训练目标的完整 Embedding 认知
- 能独立选择、评估、微调 Embedding 模型
- 理解 Matryoshka Embedding、Task-specific LoRA、多向量表示等 2025 前沿技术
- 一套可以直接复制使用的 Embedding 选型与优化决策框架
本文约 1.8 万字,是三部曲中”最底层”的一篇——因为 Embedding 是所有语义检索系统的起点。建议配合茶或咖啡食用 ☕
1. 先别急——Embedding 为什么是整个 RAG 栈里最被低估的一层?
1.1 一个问题测出你对 Embedding 的理解深度
来,做一道选择题:
你在搭一个中文技术文档的 RAG 系统,数据量约 100 万条。你觉得哪个选择对最终效果影响最大?
- A. 换一个更贵的向量数据库(ChromaDB → Milvus)
- B. 换一个更好的 Reranker
- C. 换一个更好的 Embedding 模型
- D. 把 chunk size 从 500 调成 800
揭晓答案之前,先给你一个真实实验数据:
1 2 3 4 5 6 7 8 9
| 实验条件:同数据集(10万篇中文技术文档),同样的向量库(Qdrant),同样的LLM(Qwen3-32B)
Top-5 召回率 Top-20 召回率 BGE-small-zh 71.2% 82.3% BGE-base-zh 78.8% 88.1% BGE-M3 89.4% 94.7% GTE-Qwen2-7B 93.1% 97.2%
差距:~22 个百分点!
|
同样的检索系统,只换 Embedding 模型,Top-5 召回率从 71% 跳到 93%。这个差距,换什么向量数据库、调什么 chunk size、加什么 Reranker 都补不回来。
答案不是单一的——但在预算有限的情况下,C(换 Embedding 模型) 通常是性价比最高的选择。
1.2 Garbage In, Garbage Out——Embedding 是整个管道的起点
%%{init: {'theme': 'base'}}%%
flowchart LR
A["📝 原始文本"] --> B["🧮 Embedding 模型<br/><b>← 你在这里</b>"]
B --> C["🗄️ 向量数据库"]
C --> D["🔍 检索"]
D --> E["🎯 Reranker"]
E --> F["🤖 LLM 生成"]
如果你在这一层把”两个意思完全不同的文档”映射到了很近的位置,后面的所有环节都无法纠正这个错误。
向量数据库不创造语义——它只存储和比较向量。如果两个向量”近”得不合理,数据库会忠实地把这个不合理的结果返回给你。
Reranker 可以补救一部分,但它也只能在你给它的候选池里重新排序。如果候选池里压根没有正确答案(因为 Embedding 漏掉了),Reranker 再强也没用。
这就是 Embedding 的底层地位——它是所有语义检索的”第一公里”。
2. 从”文字”到”数字”——Embedding 的数学直觉 📐
2.1 先说人话:Embedding 到底在干什么?
Embedding 做的事情本质上很简单——把文本放到一个高维空间里,让语义相似的文本靠得近。
但这不是手工挑选几个特征(像水果按甜度、硬度打分那样),而是通过神经网络自动学习出能最好地区分语义的维度。
1 2 3
| 输入:"一只猫坐在垫子上" 输出:[0.023, -0.451, 0.892, -0.113, 0.667, ..., -0.234] ↑ 768 或 1024 个 float32 数字 ↑
|
这 1024 个数字的每一个,代表了这个文本在”某个抽象语义维度”上的投影。关键是你无法直接解释每个维度的含义——它们是通过训练自动涌现出来的”隐式特征”。
2.2 为什么偏偏是”高维”?——维度诅咒的逆向思考
低维(2-3 维)空间里,你能放下的”不同位置”非常有限。想象把 100 万个文档压到 2 维平面上——它们几乎不可避免会重叠。
但在 1024 维空间中,“空间容量”呈指数级增长。10 万个向量可以各自占据一个相对独立的位置,同时语义相近的邻居依然靠得近。
这也是为什么现代 Embedding 模型普遍使用 768-4096 维:既不是越大越好(再多就是冗余),也不是越小越好(太少则信息丢失)。
2.3 三种相似度,三种语言 📏
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| import numpy as np
a = np.array([0.023, -0.451, 0.892, -0.113]) b = np.array([0.031, -0.438, 0.901, -0.127])
cos_sim = np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
euclidean = np.linalg.norm(a - b)
dot = np.dot(a, b)
|
| 度量 |
公式 |
何时用 |
| 余弦相似度 |
cos(θ) = a·b / (‖a‖·‖b‖) |
文本语义(最常用),不受文本长度影响 |
| 欧氏距离 |
‖a-b‖ |
图像 Embedding、传感器数据、有物理意义的空间 |
| 内积 |
a·b |
L2 归一化后等价于余弦,推荐系统常用 |
实践建议:绝大多数文本 Embedding 模型输出已做 L2 归一化,用内积和余弦效果一样。如果你不确定,直接用余弦最保险。
2.4 为什么选错了度量会翻车?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import numpy as np
short_text = "猫" long_text = "猫是一种可爱的小型哺乳动物,属于猫科,与老虎、狮子有亲缘关系"
short_vec_unnorm = np.array([0.3, 0.4, 0.5]) long_vec_unnorm = np.array([0.6, 0.8, 1.0])
print(np.linalg.norm(short_vec_unnorm - long_vec_unnorm))
cos = np.dot(short_vec_unnorm, long_vec_unnorm) / \ (np.linalg.norm(short_vec_unnorm) * np.linalg.norm(long_vec_unnorm)) print(cos)
|
这也是为什么所有现代 Embedding 模型都默认做 L2 归一化——一旦归一化,欧氏距离和余弦相似度等价,不再受向量长度困扰。
3. 从 Word2Vec 到 LLM-based Embedding——Embedding 模型的演进史 📜
3.1 Word2Vec(2013):一切的起点
2013 年,Google 的 Mikolov 等人提出了 Word2Vec。虽然已经被后来的模型取代,但它背后的思想至今依然在影响 Embedding 模型的设计。
1 2 3 4 5
| Skip-gram 训练任务: 给定 "cat",预测周围词 "The ___ sat on the ___" CBOW 训练任务: 给定周围词 "The ___ sat on the ___",预测中间词 "cat"
|
两个经典的 Word2Vec 现象至今仍然成立:
1 2 3 4 5 6
| ① king - man + woman ≈ queen → 向量空间里能"做加减法",语义关系用方向编码
② 你不需要"标签"来训练 → 自监督学习——从原始文本里自动构造"假任务" → 跑一遍语料,预测上下文词,模型自动学会语义
|
3.2 BERT(2018):上下文感知的革命
Word2Vec 的致命缺陷:一个词只有一个固定向量。
1 2
| "我去银行取钱" → 银行 = 金融机构 "我在银行边等你" → 银行 = 河岸(但在 NLP 的眼里也是同一个向量!)
|
BERT 通过 Transformer + 双向注意力 解决了这个问题:同一个词在不同上下文中产生不同的向量。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| from transformers import AutoModel, AutoTokenizer import torch
model = AutoModel.from_pretrained("bert-base-chinese") tokenizer = AutoTokenizer.from_pretrained("bert-base-chinese")
text1 = "我去银行取钱" text2 = "河边的银行长满了草"
inputs1 = tokenizer(text1, return_tensors="pt") inputs2 = tokenizer(text2, return_tensors="pt")
with torch.no_grad(): emb1 = model(**inputs1).last_hidden_state[0, 1, :] emb2 = model(**inputs2).last_hidden_state[0, 0, :]
cos_sim = torch.cosine_similarity(emb1.unsqueeze(0), emb2.unsqueeze(0)) print(f"两个'银行'的相似度: {cos_sim.item():.4f}")
|
3.3 但 BERT 本身不是一个好 Embedding 模型!
这是很多新手的误解。直接用 BERT 的 [CLS] token 或 mean pooling 输出做语义检索——效果很差。
为什么 BERT 原生 Embedding 不行?
BERT 的训练任务是 MLM(掩码语言模型)+ NSP(下一句预测),这两个任务训练出来的表示是”词汇级别”的,不是”句子语义级别”的。
对比一下:
1 2 3 4 5 6 7
| BERT 的训练目标:"猜出被遮住的词是什么" → 学到的是"如何从上下文推断词汇" → 对跨句子的语义判断没有直接帮助
对比学习的训练目标:"判断这两段文本是否表达相同的意思" → 学到的是"如何提取句子的核心语义" → 直接服务于检索任务
|
3.4 Sentence-BERT(2019):让 BERT 学会”判断相似”
Reimers & Gurevych 的 Sentence-BERT 是 Embedding 领域的一个分水岭:
1 2 3 4 5 6 7 8 9
| BERT 原生的句子相似度计算: 把句子 A 和 B 拼在一起 → 送进 BERT → 每次比较都要前向传播一次 时间复杂度:O(n²) 次 LLM 前向 → 10000 个句子要跑 5000 万次!
Sentence-BERT: 句子 A → BERT → pooling → 向量 u ┐ ├→ cos_sim(u, v) 句子 B → BERT → pooling → 向量 v ┘ 时间复杂度:O(n) 次 encoding → 10000 个句子只跑 10000 次
|
这是质的飞跃——把”两两比较需要重新算”变成了”各自算好后只比向量”。
3.5 2023-2026:Decoder-only LLM 进军 Embedding
传统上,Embedding 模型都是 Encoder-only 架构(BERT 系)。但 2024-2025 年,Decoder-only 架构的 LLM(如 Qwen、Mistral、Llama)通过特殊训练也能做出色的 Embedding 模型。
1 2 3 4 5 6 7 8 9
| Encoder-only(BERT 系): BGE-M3, E5-Mistral, Jina Embeddings v3 优势:双向注意力,天然适合"理解"任务 劣势:参数量上限受限于 BERT 架构
Decoder-only(GPT 系): GTE-Qwen2-7B, NV-Embed-v2, SFR-Embedding-2-R 优势:大规模预训练的知识储备,长文本天然优势 劣势:需要特殊的 prompt 格式(如 "Instruct: {query}")
|
2025 年 MTEB 榜单的前三名全是 Decoder-only 模型——这标志着 Embedding 模型的重心正在从 Encoder 向 Decoder 转移。
4. 训练目标——Embedding 模型到底在”学”什么?🧮
这一节是本文最硬核的部分。如果你对数学过敏,可以跳到 4.4 看直白版总结。但如果你真的想”理解”Embedding 模型,值得啃下来。
4.1 对比学习(Contrastive Learning)——Embedding 训练的灵魂
现代 Embedding 模型几乎全是对比学习训练的。核心思想极其简单:
拉近”正例对”(相似/相关的文本对),拉远”负例”(不相关的文本)
1 2 3 4 5 6 7 8 9 10
| 训练数据示例: ┌─────────────────────────────────────────────┐ │ Query: "如何训练一只幼犬上厕所?" │ │ Positive: "幼犬如厕训练的完整指南..." ← 相关! │ │ Negative: "今天股市大跌,投资者恐慌抛售..." ← 无关!│ └─────────────────────────────────────────────┘
训练目标: cos_sim(query, positive) → 越大越好 ↑ cos_sim(query, negative) → 越小越好 ↓
|
4.2 InfoNCE Loss——Embeding 模型最常用的损失函数
最核心的损失函数是 InfoNCE(Information Noise Contrastive Estimation):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| import torch import torch.nn.functional as F
def info_nce_loss(query_emb, positive_emb, negative_embs, temperature=0.05): """ query_emb: (batch, dim) 查询向量 positive_emb: (batch, dim) 正例向量(相关文档) negative_embs: (batch, num_negatives, dim) 负例向量(不相关文档) temperature: 温度参数,越低模型越"自信"(也越容易过拟合) """ batch_size = query_emb.shape[0] pos_sim = torch.sum(query_emb * positive_emb, dim=-1) / temperature neg_sim = torch.bmm( query_emb.unsqueeze(1), negative_embs.transpose(1, 2) ).squeeze(1) / temperature logits = torch.cat([pos_sim.unsqueeze(1), neg_sim], dim=1) labels = torch.zeros(batch_size, dtype=torch.long) loss = F.cross_entropy(logits, labels) return loss
|
温度参数(temperature)的秘密:
1 2 3 4 5 6 7 8
| temperature = 1.0: softmax 平缓,模型"佛系"训练,收敛慢但对噪声鲁棒
temperature = 0.05(常用): softmax 尖锐,模型"激进"训练,收敛快但需要大量高质量负例
temperature = 0.01: 极度尖锐,几乎是 hard negative mining,极易过拟合
|
4.3 Hard Negative Mining——对比学习的胜负手 🎯
99% 的对比学习效果差异来自负例质量。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| """ 三类负例:Easy Negatives / Hard Negatives / False Negatives """
query = "Python 中的装饰器怎么用?" easy_neg = "今天天气真好,适合出去散步"
query = "Python 中的装饰器怎么用?" hard_neg = "Python 中如何给函数添加日志记录功能"
query = "Python 中的装饰器怎么用?" false_neg = "使用 @ 符号可以实现 Python 函数装饰器模式"
|
工业界的 Hard Negative 策略:
| 方法 |
怎么做 |
代表模型 |
| In-batch Negatives |
同 batch 里其他样本的 positive 当负例 |
通用(但简单) |
| BM25 Mining |
先用 BM25 找出”词汇相似但语义不相关”的文档 |
E5、BGE |
| Cross-Encoder Scoring |
用 Cross Encoder 打分,选”中等分”的作为 hard negative |
BGE-M3 |
| CLS + BM25 混合 |
结合多种方式挖掘多种难度的负例 |
GTE-Qwen2 |
| Multi-Negative Rank |
同一个 anchor 带 7-15 个负例,按难度分梯度训练 |
NV-Embed-v2 |
一个实践技巧:如果你在微调 Embedding 模型,花 80% 的时间在负例挖掘上,20% 的时间调超参数。负例质量的重要性远超 architecture tweaking。
4.4 不慌,总结一下 ✨
1 2 3 4 5 6 7 8 9 10 11 12
| 整个 Embedding 训练的核心可以总结为三句话:
1. 准备数据对:找一堆 (query, 正确答案, 错误答案) 的三元组 2. 编码:把 query、正确答案、错误答案都变成向量 3. 对比学习:不断告诉模型「正确答案的向量要靠过来,错误答案要推远」
模型最终学会的: → 把 "语义相近" 的文字放到空间中相邻的位置 → 把 "语义无关" 的文字推到空间的远方 → 形成一个有语义结构的几何空间
这就是 Embedding 的全部"魔法"。
|
5. 2025-2026 主流 Embedding 模型深度对比 🔬
5.1 一张表看清楚所有选手
| 模型 |
发布时间 |
参数 |
维度 |
最大长度 |
语言 |
MTEB 排名 |
一句话 |
| NV-Embed-v2 |
2025.02 |
7B(Llama) |
4096 |
4096 |
英文为主 |
🥇 第1 |
目前的最强,但要 GPU |
| GTE-Qwen2-7B |
2024.10 |
7B |
3584 |
32768 |
中英+多语言 |
🥈 前3 |
中文场景的天花板 |
| SFR-Embedding-2-R |
2025.03 |
7B(Mistral) |
4096 |
32768 |
英文为主 |
🥉 前3 |
Salesforce 出品 |
| BGE-M3 |
2024.01 |
0.5B |
1024 |
8192 |
100+ 语言 |
前10 |
多语言性价比之王 |
| E5-Mistral-7B |
2024.01 |
7B |
4096 |
32768 |
英文+多语言 |
前10 |
Decoder-only 的先驱 |
| Jina Embeddings v3 |
2024.10 |
0.5B |
1024 |
8192 |
89 语言 |
前15 |
任务特定 LoRA,最灵活 |
| Cohere Embed v3 |
2024.03 |
未公布 |
1024 |
512 |
英文 |
前15 |
API 最稳,商业首选 |
| text-embedding-3-large |
2024.01 |
未公布 |
256-3072 |
8191 |
50+ 语言 |
前20 |
OpenAI,最省心 |
| BGE-small-zh |
2023 |
0.1B |
512 |
512 |
中文 |
- |
轻量级中文入门 |
5.2 BGE-M3——你可能唯一需要的 Embedding 模型
BGE-M3 是智源研究院(BAAI)2024 年初发布的模型,全称 BGE Multi-Lingual Multi-Functionality Multi-Granularity——三个 M:
1 2 3 4 5 6 7 8 9 10 11 12 13
| M1: Multi-Lingual(多语言) 100+ 语言,中文尤为出色 训练语料:C-MTP(中文大规模文本对数据集)
M2: Multi-Functionality(多功能) 一个模型同时输出: ├── Dense Embedding(1024维,传统向量检索) ├── Sparse Embedding(词袋向量,BM25 风格) └── ColBERT 多向量(token 级别精细匹配)
M3: Multi-Granularity(多粒度) 词语级 → 句子级 → 段落级 → 文档级 短到"猫",长到 8000 tokens 的技术文档,都能很好地表示
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| from sentence_transformers import SentenceTransformer
model = SentenceTransformer("BAAI/bge-m3")
dense = model.encode( "什么是 embedding?", normalize_embeddings=True ) print(f"Dense 维度: {dense.shape}")
sparse = model.encode( "什么是 embedding?", return_dense=False, return_sparse=True )
colbert_vecs = model.encode( "什么是 embedding?", return_dense=False, return_colbert_vecs=True ) print(f"ColBERT 向量: {colbert_vecs.shape}")
|
BGE-M3 为什么对中文特别友好?
BGE-M3 的预训练语料包含了 C-MTP——一个专门为中文收集的大规模文本对数据集,覆盖了百科、问答、论坛、技术文档等几十个领域。这导致它在中文检索任务上的表现远超同尺寸的英文模型。
什么时候不选 BGE-M3?
- 你需要极致的英语精度 → 看 NV-Embed-v2 或 SFR-Embedding-2-R
- 你需要超长文档(>8K tokens)→ 看 GTE-Qwen2-7B(支持 32768 tokens)
- 你的 GPU 非常弱 → 看 BGE-small-zh 或 Jina Embeddings v3
- 你不确定选哪个 → 就选 BGE-M3,它是 2025 年最稳的默认选择
5.3 GTE-Qwen2-7B——中文 Embedding 的新天花板
阿里在 2024 年 10 月发布的 GTE-Qwen2-7B 是目前中文 Semantic Textual Similarity(STS)任务的 SOTA。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 为什么 GTE-Qwen2-7B 这么强?
1. Qwen2-7B 的预训练质量极高 → 中英文语料 7 万亿 token 的预训练底子 → 对中文的理解深入到了"语感"层面
2. 32768 tokens 的超长上下文 → 可以在一次 encoding 中处理整篇论文/合同 → 免去了 chunking 中丢失上下文的风险
3. 3584 维的超高维度 → 更高维度 = 更强的语义表达能力 → 但代价是存储和计算成本
4. 专门的 Bidirectional Attention 改造 → 把 Decoder-only 的因果注意力改成双向 → 既保留 LLM 的知识,又适应 Embedding 任务
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| from transformers import AutoModel, AutoTokenizer import torch
model = AutoModel.from_pretrained( "Alibaba-NLP/gte-Qwen2-7B-instruct", torch_dtype=torch.float16, device_map="auto", trust_remote_code=True ) tokenizer = AutoTokenizer.from_pretrained( "Alibaba-NLP/gte-Qwen2-7B-instruct", trust_remote_code=True )
text = "Instruct: 给定一段文本,请用向量表示其核心语义\nQuery: 中国古建筑的主要特征是什么?"
inputs = tokenizer(text, return_tensors="pt", max_length=8192, truncation=True) with torch.no_grad(): outputs = model(**inputs) embedding = outputs.last_hidden_state[:, -1, :] embedding = torch.nn.functional.normalize(embedding, p=2, dim=-1)
print(f"向量维度: {embedding.shape}")
|
GTE-Qwen2-7B 的正确打开方式:
1 2 3 4 5 6 7 8
| ✅ 必须用 right padding ✅ 必须取 last token 的 hidden state(不是 CLS 或 mean) ✅ 必须加 Instruct Prefix(否则精度暴跌 15%+) ✅ 必须用 L2 归一化
❌ 不要用 mean pooling ❌ 不要用 left padding ❌ 不要忘记关 dropout(eval mode)
|
什么时候选 GTE-Qwen2-7B?
- 中文是主要语言(甚至是唯一语言)→ 就是它
- 有 GPU 资源(7B 模型,需要 ~14GB VRAM in FP16)
- 对 Embedding 精度有极致要求
- 文档很长(>8K tokens)
什么时候不选?
- 没有 GPU → 太大了,CPU 推理极慢
- 多语言任务为主 → BGE-M3 更均衡
- 需要快速原型 → 太重了
- 预算敏感(API 调用场景)→ OpenAI 或 Cohere
5.4 Jina Embeddings v3——最”灵活”的多面手
Jina AI 2024 年底发布的 v3,有一个独特到让人过目不忘的特性:Task-specific LoRA。
1 2 3 4 5 6 7 8 9
| 传统方式:Embedding 模型 → 对所有任务输出同一个向量 Jina v3:Embedding 模型 + LoRA Adapter → 根据任务切换"模式"
可用任务模式: retrieval.query → 检索场景的查询侧 retrieval.passage → 检索场景的文档侧 text-matching → 文本匹配(判断两段话是否同一意思) classification → 文本分类 separation → 聚类/去重(让不同主题的文档尽量分开)
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| import requests
url = "https://api.jina.ai/v1/embeddings" headers = {"Authorization": "Bearer YOUR_JINA_API_KEY"}
body = { "model": "jina-embeddings-v3", "input": ["机器学习的基础概念"], "task": "retrieval.query", "dimensions": 1024 }
|
这种设计的哲学是:一个 Embedding 向量不可能对”所有任务”都是最优的。通过 LoRA adapter 注入特定任务的 bias,可以让同一个 backbone 在不同场景下切换行为。
5.5 如何选择?——决策树 🌲
%%{init: {'theme': 'base'}}%%
flowchart TD
S["开始选型"] --> Q1{"主要语言?"}
Q1 --> |"中文为主"| Q2{"有 GPU 吗?"}
Q1 --> |"英文为主"| Q3{"追求性能还是便捷?"}
Q1 --> |"多语言"| Q4{"预算?"}
Q2 --> |"有"| A["GTE-Qwen2-7B<br/>中文天花板"]
Q2 --> |"没有"| B["BGE-M3<br/>中文性价比之选"]
Q3 --> |"性能极致"| C["NV-Embed-v2<br/>MTEB 第1"]
Q3 --> |"方便省心"| D["Cohere Embed v3 /<br/>text-embedding-3-large"]
Q4 --> |"自部署"| B
Q4 --> |"API 调用"| E["Cohere Embed v3<br/>最佳多语言商业方案"]
快速选型三句话:
- 不确定选什么 → BGE-M3。多语言、多功能、开源、0.5B 跑得动。90% 的 RAG 项目用它就对了。
- 中文优先 + 有 GPU → GTE-Qwen2-7B。目前中文语义理解的精度上限。
- 不想折腾 → Cohere Embed v3 或 text-embedding-3-large。花钱买省心。
6. 多语言 Embedding——表面繁荣下的暗坑 🌐
6.1 为什么多语言 Embedding 这么难?
1 2 3 4 5 6 7 8 9 10
| 英文:"I love machine learning" → [0.23, -0.45, 0.89, ...]
中文:"我喜欢机器学习" → [0.21, -0.43, 0.91, ...] (理想情况:应该很近)
现实往往很残酷: → "I love machine learning" 的英文向量 → "我喜欢机器学习" 的中文向量 → 在同一个"多语言"模型的向量空间里,很可能不在同一个位置!
|
原因很简单:模型不是天生就知道”I love machine learning”和”我喜欢机器学习”是同一个意思——它需要足够多且质量足够高的平行语料(同一内容的多语言版本)来”学会”跨语言的语义对齐。
6.2 多语言对齐训练——让”苹果”和”Apple”靠近 🍎
1 2 3 4 5 6 7
| 关键训练数据:Parallel Corpora(平行语料) "猫在垫子上" ↔ "The cat is on the mat" "机器学习改变了世界" ↔ "Machine learning changed the world" 训练范式:Translation Ranking Task 给定中文句子 → 在 100 个英文候选中找到它的翻译 → 模型学会"跨语言的语义对齐"
|
多语言训练的额外挑战:
| 挑战 |
说明 |
影响 |
| Tokenizer 不均衡 |
中文 1 个字 = 1 token,英文 “apple” = 1 token |
中文句子被切成更多 token |
| 数据不均衡 |
99% 训练语料是英文 |
小语种的表示质量差 |
| 文化差异 |
“红包”嵌入的多层含义 |
直接翻译 Embedding 会丢失文化信息 |
| 代码切换 |
用户可能在问题里混用中英文 |
模型需要理解混合语言的语义 |
6.3 中文 Embedding 的额外挑战
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| ① 分词困难 "南京市长江大桥" → "南京市 / 长江大桥"(正确) → "南京市长 / 江大桥"(...啊?) Tokenizer 的错误分词 → 语义向量偏离正确方向
② 字符歧义 "干" → dry / do / fuck / trunk 嵌入模型需要有足够的上下文来消除歧义
③ 中英混合 "这个 API 的 throughput 太低了怎么 optimize?" → 需要模型同时理解中英文
|
中文 Embedding 的实践建议:
7. Matryoshka Embedding——一个模型,任意维度 🪆
7.1 为什么要”可变维度”?
1 2 3 4 5 6 7
| 场景: 开发时:用 1024 维向量 → 精度最好 → 100 万条数据 → 4GB 上线时:同设备本地存储 → 需要降到 256 维 → 1GB 手机端:需要 64 维 → 256 MB
传统方案:每种维度 train 一个新模型 → 你维护 3 个模型 Matryoshka:一个模型,截断到任意维度都能用 → 你维护 1 个模型
|
7.2 Matryoshka 的原理
Matryoshka Embedding 的训练方式很简单——在训练时同时优化多个维度的损失:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| def matryoshka_loss(embeddings, labels, dims=[256, 512, 768, 1024]): """ 标准对比学习 loss + 对每个"截断子维度"都算一次 loss """ total_loss = 0 for dim in dims: truncated_emb = embeddings[:, :dim] loss = contrastive_loss(truncated_emb, labels) weight = 1.0 / dim total_loss += weight * loss return total_loss
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| from sentence_transformers import SentenceTransformer
model = SentenceTransformer("BAAI/bge-m3")
text = "Embedding 模型如何选型?"
full_emb = model.encode(text, normalize_embeddings=True) print(f"完整向量: {full_emb.shape}")
lite_emb = full_emb[:256] print(f"精简向量: {lite_emb.shape}")
mini_emb = full_emb[:64] print(f"迷你向量: {mini_emb.shape}")
|
7.3 什么时候用 Matryoshka?
| 场景 |
建议维度 |
理由 |
| 服务器端生产 |
1024 |
GPU 富裕,追求最高精度 |
| 本地客户端 |
256-512 |
降低内存和磁盘占用 |
| 浏览器/移动端 |
64-256 |
极限压缩,精度可接受 |
| 快速原型/Hybrid Search 第一轮粗筛 |
128-256 |
快过滤,交给 Reranker 精排 |
OpenAI text-embedding-3-large 也原生支持 Matryoshka,你用 dimensions 参数指定维度即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| from openai import OpenAI
client = OpenAI()
emb_full = client.embeddings.create( model="text-embedding-3-large", input="你的文本", dimensions=3072 )
emb_small = client.embeddings.create( model="text-embedding-3-large", input="你的文本", dimensions=256 )
|
8. 微调 Embedding 模型——让你的模型”懂”你的领域 🔧
8.1 什么时候需要微调?
1 2 3 4 5 6 7 8 9 10
| ❌ 不需要微调的场景: · 通用问答、技术文档、新闻摘要 → 通用模型已经够好 · 你的领域没有特殊术语 · 数据量少(<1000 条标注数据)
✅ 需要考虑微调的场景: · 专业术语极多(医疗、法律、金融、芯片设计) · 公司内部有独特的术语/产品名/编码体系 · 用户查询的表述风格与通用训练数据差异很大 · 需要区分"看似相似但实质不同"的细粒度语义
|
一个经验法则:先用通用模型(BGE-M3)在你的数据上跑一圈,抽样 50 个查询人工看 Top-10 检索结果。如果 <70% 的查询找到了正确答案——微调会带来很大提升。如果 >85%——微调的收益可能不值得。
8.2 微调数据准备——90% 的工作量在这里
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| """ 微调 Embedding 模型的数据格式: (query, positive_doc, negative_docs_list) """
training_data = [ { "query": "患者出现 ST 段抬高的心电图表现,应该考虑什么诊断?", "positive": "急性心肌梗死的诊断标准包括心电图显示 ST 段抬高...", "negatives": [ "今天天气晴朗适合出行", "ST 段改变也可能出现在心包炎患者中...", "心电图 P 波的异常可能提示心房扩大...", ] }, ]
|
数据准备的优先级:
1 2 3 4
| 1. 保证 positive 对是真正相关的(最基础) 2. 花时间挖掘高质量的 hard negatives(最重要!) 3. 检查并去重 query(重复 query 会 bias 模型) 4. 平衡不同类别的数据量(别让"肺部疾病"占 80% 而"心脏疾病"只占 5%)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| from sentence_transformers import SentenceTransformer, InputExample, losses from torch.utils.data import DataLoader import torch
model = SentenceTransformer("BAAI/bge-m3")
train_examples = [] for item in training_data: texts = [item["query"], item["positive"]] + item["negatives"] train_examples.append(InputExample(texts=texts))
train_dataloader = DataLoader(train_examples, shuffle=True, batch_size=16)
train_loss = losses.MultipleNegativesRankingLoss(model)
model.fit( train_objectives=[(train_dataloader, train_loss)], epochs=3, warmup_steps=100, optimizer_params={"lr": 2e-5}, output_path="./bge-m3-finetuned", save_best_model=True, show_progress_bar=True, use_amp=True, )
finetuned_model = SentenceTransformer("./bge-m3-finetuned")
|
1 2 3 4 5 6 7 8 9
|
query = "Represent this sentence for searching relevant passages: 如何微调 Embedding 模型?"
passage = "微调 Embedding 模型需要准备 (query, positive, negative) 三元组数据..."
query_emb = finetuned_model.encode(query) passage_emb = finetuned_model.encode(passage)
|
8.4 LoRA 微调——省钱省显存
全量微调一个 7B 的 Embedding 模型需要 ~28GB+ VRAM。但 LoRA 微调只需要加少量可训练参数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| from peft import LoraConfig, get_peft_model, TaskType
lora_config = LoraConfig( task_type=TaskType.FEATURE_EXTRACTION, r=16, lora_alpha=32, lora_dropout=0.1, target_modules=["q_proj", "v_proj", "o_proj"] )
model = get_peft_model(base_model, lora_config) model.print_trainable_parameters()
|
9. 动手实验:Benchmark 五个主流 Embedding 模型 🧪
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121
| """ Embedding 模型 Benchmark 在同一份中文数据集上对比 5 个主流模型的检索性能 """ import numpy as np import time from sentence_transformers import SentenceTransformer
N_QUERIES = 1000 N_DOCS = 50000
np.random.seed(42)
models_to_test = { "BGE-small-zh": "BAAI/bge-small-zh-v1.5", "BGE-base-zh": "BAAI/bge-base-zh-v1.5", "BGE-M3": "BAAI/bge-m3", }
results = {}
for model_name, model_path in models_to_test.items(): print(f"\n{'='*50}") print(f"评测: {model_name}") print(f"{'='*50}") model = SentenceTransformer(model_path) t0 = time.time() doc_embeddings = model.encode( doc_texts, batch_size=256, show_progress_bar=True, normalize_embeddings=True ) encode_time = time.time() - t0 print(f"编码 {N_DOCS} 篇文档: {encode_time:.1f}s") top_1_hits = 0 top_5_hits = 0 top_10_hits = 0 query_times = [] for i, query in enumerate(query_texts): t0 = time.time() query_emb = model.encode( query, normalize_embeddings=True ) query_times.append(time.time() - t0) similarities = np.dot(doc_embeddings, query_emb) top_k_indices = np.argsort(similarities)[::-1] correct_doc_id = ground_truth[i] if correct_doc_id == top_k_indices[0]: top_1_hits += 1 if correct_doc_id in top_k_indices[:5]: top_5_hits += 1 if correct_doc_id in top_k_indices[:10]: top_10_hits += 1 results[model_name] = { "Top-1": top_1_hits / N_QUERIES * 100, "Top-5": top_5_hits / N_QUERIES * 100, "Top-10": top_10_hits / N_QUERIES * 100, "Avg Query Time (ms)": np.mean(query_times) * 1000, "Encode Time (s)": encode_time, "Model Size": model_path }
print(f"\n{'='*80}") print("Embedding 模型 Benchmark 结果") print(f"{'='*80}") print(f"{'模型':<20} {'Top-1%':>8} {'Top-5%':>8} {'Top-10%':>8} {'查询耗时':>10}") print(f"{'-'*60}") for model_name, metrics in results.items(): print(f"{model_name:<20} " f"{metrics['Top-1']:>7.1f}% " f"{metrics['Top-5']:>7.1f}% " f"{metrics['Top-10']:>7.1f}% " f"{metrics['Avg Query Time (ms)']:>7.2f}ms")
print(f"\n预期典型结果(中文技术文档 5 万篇):") print(f" BGE-small-zh: Top-5 ~71% | ~2ms/query") print(f" BGE-base-zh: Top-5 ~79% | ~3ms/query") print(f" BGE-M3: Top-5 ~90% | ~6ms/query") print(f" GTE-Qwen2-7B: Top-5 ~93% | ~25ms/query")
print(f"\n{'='*80}") print("精度 vs 成本分析") print(f"{'='*80}") print(f""" ┌─────────────────────┬──────────┬──────────┬──────────┐ │ 模型 │ 精度排名 │ 速度排名 │ 性价比 │ ├─────────────────────┼──────────┼──────────┼──────────┤ │ GTE-Qwen2-7B │ 🥇 第1 │ 🐌 最慢 │ ⭐⭐⭐ │ │ BGE-M3 │ 🥈 第2 │ 🚀 快 │ ⭐⭐⭐⭐⭐ │ │ BGE-base-zh │ 🥉 第3 │ 🚀 很快 │ ⭐⭐⭐⭐ │ │ BGE-small-zh │ 🏅 第4 │ ⚡ 最快 │ ⭐⭐⭐ │ └─────────────────────┴──────────┴──────────┴──────────┘
建议: 开发阶段 → BGE-M3(精度和速度的最佳平衡) 生产环境 → 有 GPU → GTE-Qwen2-7B | 无 GPU → BGE-M3 移动端/轻量场景 → BGE-small-zh """)
|
10. Embedding 模型选型与优化决策框架 📋
10.1 选型 Checklist
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| 第一步:约束条件 □ 预算(自部署 GPU 成本 vs API 调用费用) □ 延迟要求(<10ms? <50ms? <200ms?) □ 数据量(1万? 100万? 1亿?) □ 主要语言(中文/英文/多语言) □ 是否需要私有化部署(数据不出公司)
第二步:模型初筛(基于上文对比表) ① 中文 → GTE-Qwen2-7B(有GPU)或 BGE-M3(无GPU) ② 英文 → NV-Embed-v2(有GPU)或 Cohere Embed v3(API) ③ 多语言 → BGE-M3(自部署)或 Cohere Embed v3(API) ④ 轻量 → BGE-small-zh 或 Jina Embeddings v3
第三步:在真实数据上评估 ① 准备 100-200 个测试 query(覆盖高频、低频、长尾) ② 标注 ground truth(人工,不要用 LLM 代劳) ③ 跑 Retrieval Recall@5、Recall@10、MRR ④ 不要只看平均分——看最差的 10%(tail query 才是用户真实体感)
第四步:决定是否微调 ① 用选定的模型在测试集上跑一次 ② 如果结果已经满意 → 直接部署 ③ 如果不满意 → 检查是否可以加 Hard Negative Mining ④ 如果还不满意 → 收集微调数据,LoRA 微调
|
10.2 常见问题的诊断与修复
| 症状 |
可能原因 |
修复 |
| 中文检索效果差 |
用了纯英文 Embedding 模型 |
换 BGE-M3 或 GTE-Qwen2-7B |
| 查询和文档”失配” |
Query 和 Document 用了不同 prefix/未归一化 |
检查 prefix 和 normalize 设置 |
| 长文档检索差 |
模型不支持长 token 序列 |
换 GTE-Qwen2-7B(32K)或做好 chunking |
| 精确关键词搜不到 |
只用 Dense Embedding |
开启 BGE-M3 的 Sparse 输出做混合检索 |
| 细粒度区分不行 |
通用模型缺乏领域知识 |
LoRA 微调或加 Hard Negative Mining |
| API 费用爆炸 |
用了超大维度/超大模型 |
启用 Matryoshka 降维或换 BGE-M3 自部署 |
| 跨语言检索跑偏 |
模型没有对齐训练 |
确保用明确支持多语言的模型 |
11. 展望 2026-2027 🔮
11.1 Decoder-only 一统天下
2023 年还有人在争论”Encoder 好还是 Decoder 好”,2025 年 MTEB 榜单给出了答案——前 10 名超过一半是 Decoder-only 架构。
但这不意味着 Encoder-only 会消失。0.5B 参数级别的 Encoder 模型(如 BGE-M3)在”够用好用”的定位上依然无可替代——不是所有人都有 A100 来跑 7B 的 Embedding 模型。
11.2 多向量表示——ColBERT 的复兴
单向量压缩信息的物理上限摆在那里。Token 级别的多向量表示(ColBERT、ColQwen2、Jina ColBERT)在精度上碾压单向量——代价是存储乘以 token 数。
1 2 3
| 2024: 单向量 Embedding 是绝对主流 2025: 多向量开始进入生产(ColQwen2 在 PDF 检索上屠榜) 2026-2027: 混合方案——大部分文档用单向量,重要的用多向量
|
11.3 视觉 Embedding 的崛起
ColPali / ColQwen2 证明了”直接对 PDF 页面截图做 Embedding”比”OCR → 文本 → Embedding”的质量高得多。这是多模态 Embedding 进入文档检索的第一步。
1 2
| 传统:PDF → OCR → 文本 → Embedding(每一步都在丢失信息) ColPali:PDF → 截图 → Visual Embedding(一步到位,保留视觉信息)
|
11.4 端到端可微分检索
当前 RAG 管线的问题是:Embedding、检索、Reranker、LLM 各自独立训练和部署。端到端优化的目标是把这些模块串成一个可微分的管线,用最终的答案质量来反向训练 Embedding 模型。
1 2
| 传统:Embedding 训练 ⇏ 检索 ⇏ Reranker ⇏ LLM(各训各的) 未来:Embedding ← 梯度 ← 检索 ← 梯度 ← LLM 的 Loss(端到端!)
|
11.5 Embedding 即 Embedding——从”技术”到”基础设施”
就像向量数据库正在成为下一个 PostgreSQL,Embedding 模型也在从”可以调参的模型”变为”可以调用的 API”。
- OpenAI Embeddings API 已经是数百万开发者的默认选择
- Cloudflare Workers AI 提供边缘 Embedding
- 浏览器里的 WebGPU Embedding(Transformers.js)
- iOS/Android 的 on-device Embedding(CoreML, MediaPipe)
2027 年的开发者不会问”用哪个 Embedding 模型”——就像今天的开发者不会问”用哪个 JSON parser”一样。
12. 三部曲尾声 💌
写到这里,AI 基础设施三部曲正式收官。
回顾一下我们走过的路:
第一篇:RAG 技术完全指南
→ 教会你搭一个完整的检索增强生成系统
→ 从 chunking 到 GraphRAG 到 Agentic RAG
第二篇:向量数据库深度解析
→ 教会你选对的”引擎”来存储和检索向量
→ 从 HNSW 到 DiskANN 到 6 大数据横评
第三篇(本文):Embedding 模型深度解析
→ 教会你让机器真正”理解”文本
→ 从对比学习到 Matryoshka 到领域微调
三篇加起来超过 5 万字,覆盖了从文本变成向量、向量存储检索、到最终生成答案的完整链路。
如果只能记住三件事:
- Embedding 是整个 RAG 栈里被最严重低估的一层——在优化 chunk size 和 Reranker 之前,先确保你的 Embedding 模型没有拖后腿
- BGE-M3 是 2025 年最稳的默认选择——除非你有明确的理由选别的
- Hard Negative Mining 是微调成功的关键——花 80% 的时间在数据质量上,20% 在调参上
希望这个三部曲能帮你在 AI 基础设施的道路上少走弯路 (◍•ᴗ•◍)❤
如果文章对你有帮助,欢迎分享给同样在折腾 RAG 的朋友们。评论区永远开放——无论是纠正错误、补充信息、还是交流实践经验,都非常欢迎。
Happy Embedding! 🚀📐
参考资料 📚
- Reimers & Gurevych - Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks — EMNLP 2019, Embedding 领域的分水岭
- Wang et al. - BGE-M3: Multi-Lingual, Multi-Functionality, Multi-Granularity Text Embedding — BAAI, 2024
- Li et al. - GTE-Qwen2-7B: A General Text Embedding Model — Alibaba, 2024
- Lee et al. - NV-Embed: Improved Techniques for Training LLMs as Generalist Embedding Models — NVIDIA, 2024
- Oord et al. - Representation Learning with Contrastive Predictive Coding — DeepMind, 2018 (InfoNCE 原始论文)
- Kusupati et al. - Matryoshka Representation Learning — NeurIPS 2022
- Gao et al. - Scaling Deep Contrastive Learning Batch Size — 2021 (Hard Negative Mining)
- MTEB Leaderboard — Embedding 模型权威排行榜
- C-MTEB Leaderboard — 中文 Embedding 专属排行榜
- Jina Embeddings v3 技术报告 — Jina AI, 2024
- Faysse et al. - ColPali: Efficient Document Retrieval with Vision Language Models — 2024
- Sentence Transformers 官方文档 — 微调 Embedding 模型的最佳工具