6.2 Embedding 模型:将文字变成向量
在上一节中,我们建立了对 RAG(检索增强生成)的整体认知:RAG 的核心思路是"先检索、再生成"——从外部知识库中找到相关文档片段,将其拼入提示词,让大语言模型基于这些材料回答问题。整个 RAG 流水线可以概括为五个环节:文档加载 → 文档分块 → 向量化(Embedding)→ 向量检索 → 拼接生成。其中,向量化是连接"人类语言"与"机器检索"的关键桥梁。没有 Embedding,检索系统就无法理解"红烧肉的做法"和"如何做红烧肉"在语义上是高度相关的。
那么,Embedding 到底是什么?它如何把一段文字变成机器可以比较的数字?不同模型之间又该如何选择?本节将深入解答这些问题。在下一节(6.3 文档分块)中,我们将讨论如何把长文档切成合适的片段——分块策略直接影响 Embedding 的效果,两者紧密相连。
6.2.1 什么是 Embedding:给文字装上 GPS
要理解 Embedding,最好的类比就是给每段文字分配一个 GPS 坐标。
想象一张巨大的地图。在这张地图上,每个地理位置都有一组坐标(经度、纬度)。北京天安门的坐标是 (39.91, 116.40),上海外滩是 (31.23, 121.47)。如果两个人的 GPS 坐标很接近,你就能推断他们大概在同一个城市,甚至同一条街上。
Embedding 做的事情与此类似,只不过:
- 地图从二维变成了几百到几千维——不再只有经度和纬度,而是有几百甚至几千个"语义维度"。
- 坐标上的位置不再是地理位置,而是语义位置——意思相近的词或句子,在这张"语义地图"上的坐标也会很接近。
GPS 坐标: 北京 → (39.91, 116.40) ← 2 维
上海 → (31.23, 121.47) ← 距离约 1000km
Embedding: "猫" → [0.12, -0.34, 0.78, ..., 0.05] ← 1536 维
"小猫" → [0.11, -0.33, 0.79, ..., 0.04] ← 距离很近!语义相似
"汽车" → [0.89, 0.42, -0.15, ..., -0.67] ← 距离很远!语义无关在这张"语义地图"上,"猫"和"小猫"几乎站在同一个点上,而"汽车"则远在千里之外。这就是 Embedding 的魔力——它把模糊的语义关系,变成了精确的数学距离。
一句话理解:Embedding 就是给每段文字打上一组数字标签,让计算机能像量地图距离一样,量化"两段话有多相似"。
6.2.2 Embedding 的训练原理:对比学习
现代 Embedding 模型大多基于对比学习(Contrastive Learning) 训练。其核心思想可以用一句话概括:让相似的文本靠近,让不相似的文本远离。
训练目标:让正样本对(相似文本)距离近,负样本对(不相似文本)距离远
正样本:query="如何做红烧肉" ↔ document="红烧肉的做法步骤..." → 拉近
负样本:query="如何做红烧肉" ↔ document="今天天气真好..." → 推远具体来说,模型在训练时会同时看到:
- 一个锚点(Anchor):比如"如何做红烧肉"
- 一个正样本(Positive):与锚点语义匹配的文本,如"红烧肉的做法步骤"
- 多个负样本(Negative):与锚点语义不匹配的文本,如"今天天气真好""Python 入门教程"
模型的损失函数(通常是 InfoNCE)会同时做两件事:
- 把锚点和正样本在向量空间中的距离缩小
- 把锚点和所有负样本的距离拉大
经过数亿条文本对的训练,模型学会了把语义信息"编码"到向量空间的各个维度中。最终,每个维度都隐含地代表了某种语义特征——比如某个维度可能隐约对应"是否与食物相关",另一个维度可能对应"是否是疑问句"——但这些维度并非人类可以直观解释的,它们是模型自动学到的隐含表示。
6.2.3 相似度度量:如何计算两段文字"有多像"
有了向量,下一个问题就是:怎么衡量两个向量之间的"距离"? 这在 RAG 检索中至关重要——系统需要根据这个距离排序,找出最相关的文档。
常用的度量方式有三种:
| 度量方式 | 公式 | 取值范围 | 特点 |
|---|---|---|---|
| 余弦相似度 | cos(A,B) = A·B / ( | A | × |
| 欧氏距离 | d(A,B) = √Σ(Ai-Bi)² | [0, +∞) | 几何直线距离,需归一化 |
| 点积 | A·B = Σ(Ai×Bi) | (-∞, +∞) | 计算最快,但受向量长度影响 |
为什么余弦相似度最常用? 因为在语义检索中,我们关心的是两段文字的"方向"是否一致,而不是向量的"长度"。打个比方,两个人站在不同距离外描述同一栋楼,虽然声音大小不同(向量长度不同),但他们指向的方向是一致的(余弦相似度高)。余弦相似度忽略了长度,只保留方向信息,因此更适合比较语义相似性。
下面通过代码来理解余弦相似度的计算过程:
import numpy as np
def cosine_similarity(a, b):
"""计算两个向量的余弦相似度
参数:
a: 第一个向量,如 [0.12, -0.34, 0.78]
b: 第二个向量,如 [0.11, -0.33, 0.79]
返回:
余弦相似度,取值 [-1, 1],越接近 1 表示越相似
"""
# 第一步:计算点积 a·b = 0.12×0.11 + (-0.34)×(-0.33) + 0.78×0.79
dot_product = np.dot(a, b)
# 第二步:计算各自的模长 |a| 和 |b|
norm_a = np.linalg.norm(a) # √(0.12² + 0.34² + 0.78²)
norm_b = np.linalg.norm(b) # √(0.11² + 0.33² + 0.79²)
# 第三步:余弦相似度 = 点积 / (模长A × 模长B)
return dot_product / (norm_a * norm_b)
# ---- 示例:比较三个词的语义距离 ----
cat_vec = np.array([0.12, -0.34, 0.78]) # "猫"的向量(示意)
kitten_vec = np.array([0.11, -0.33, 0.79]) # "小猫"的向量(示意)
car_vec = np.array([0.89, 0.42, -0.15]) # "汽车"的向量(示意)
print(f"猫 vs 小猫: {cosine_similarity(cat_vec, kitten_vec):.4f}")
# 输出约 0.9998 —— 几乎相同,语义高度相似!
print(f"猫 vs 汽车: {cosine_similarity(cat_vec, car_vec):.4f}")
# 输出约 -0.05 —— 接近 0,语义几乎无关!从结果可以看到:
- "猫"和"小猫" 的余弦相似度接近 1,说明它们在语义空间中方向几乎一致——模型理解到它们说的是同一种东西。
- "猫"和"汽车" 的余弦相似度接近 0,说明它们在语义空间中"方向垂直"——模型认为它们之间没有明显的语义关联。
实用提示:很多 Embedding 模型(如 BGE)在输出向量时已经做了归一化(让模长等于 1)。此时余弦相似度就等于点积,可以直接用
np.dot(a, b)计算,省去了除法的开销。这就是为什么实际代码中经常看到直接用点积的原因。
6.2.4 主流 Embedding 模型对比
截至 2025 年,主流 Embedding 模型可以分为三大阵营:OpenAI API 派、中文开源派、大模型派。各有侧重,适合不同场景。
① OpenAI text-embedding-3 系列
| 模型 | 维度 | MTEB 得分 | 价格(每 1K token) | 特点 |
|---|---|---|---|---|
| text-embedding-3-small | 512 / 1536 | 62.3 | $0.00002 | 性价比之王 |
| text-embedding-3-large | 256 / 1024 / 3072 | 64.6 | $0.00013 | 最高精度 |
核心优势:
- 支持 Matryoshka 表示学习:同一个模型可以输出不同维度(256、512、1536),维度越低检索越快
- 多语言支持好,中文表现不错
- 无需自部署,API 调用简单,几行代码即可集成
# Matryoshka 维度选择示例:用同一模型生成不同维度的向量
from openai import OpenAI
client = OpenAI() # 初始化 OpenAI 客户端,需设置 OPENAI_API_KEY
# 用 text-embedding-3-small 模型,指定输出 256 维向量
# 虽然模型原生支持 1536 维,但可以截断到 256 维使用
response = client.embeddings.create(
model="text-embedding-3-small", # 选择模型
input="这是一段测试文本", # 要编码的文本
dimensions=256 # 只需前 256 维!
)
# 打印实际维度,确认是 256
print(f"向量维度: {len(response.data[0].embedding)}") # 输出: 256什么是 Matryoshka? 俄罗斯套娃(Matryoshka doll)大套小、层层嵌套。Matryoshka 表示学习的灵感正来源于此:高维向量的前几维已经包含了最重要的语义信息,后面的维度是逐步细化的补充。因此截断到低维时,关键语义信息不会丢失。这意味着你可以"一次生成、按需截断",而无需为不同维度分别调用模型。
② BGE 系列(智源研究院)
BGE(BAAI General Embedding)由智源研究院(BAAI)开发,中文能力极强。
| 模型 | 维度 | 中文 MTEB | 特点 |
|---|---|---|---|
| bge-large-zh-v1.5 | 1024 | 领先 | 中文专精,开源可部署 |
| bge-m3 | 1024 | 领先 | 多语言 + 8192 token 长度 |
| bge-small-zh-v1.5 | 512 | 良好 | 轻量快速,适合资源受限 |
核心优势:
- 中文最强:在中文检索、分类等任务上表现优异,尤其在古文、专业术语等领域
- 开源可本地部署,数据不出域,满足隐私合规要求
- bge-m3 支持超长文本(8192 token),适合处理长文档而无需分块过细
# BGE 模型使用示例(本地运行,无需 API Key)
from sentence_transformers import SentenceTransformer
import numpy as np
# 加载 BGE 中文大模型(首次运行会自动下载权重)
bge_model = SentenceTransformer("BAAI/bge-large-zh-v1.5")
# BGE 模型的特殊用法:查询文本需要加前缀
query = "为这个句子生成表示以用于检索相关文章:红烧肉怎么做"
doc = "红烧肉是一道经典中式菜肴,需要五花肉、酱油、冰糖等食材..."
# encode 方法将文本转为向量
# normalize_embeddings=True 对向量做 L2 归一化(模长变为 1)
q_vec = bge_model.encode(query, normalize_embeddings=True) # 查询向量
d_vec = bge_model.encode(doc, normalize_embeddings=True) # 文档向量
# 因为已经归一化,余弦相似度 = 点积,直接用 np.dot 即可
similarity = np.dot(q_vec, d_vec)
print(f"相似度: {similarity:.4f}") # 输出约 0.7~0.9,语义相关③ GTE / Qwen3 系列(阿里巴巴)
GTE(General Text Embeddings)由阿里巴巴达摩院开发,最新 Qwen3 Embedding 在多个基准上达到 SOTA。
| 模型 | 维度 | 特点 |
|---|---|---|
| gte-Qwen2-7B-instruct | 3584 | 基于 Qwen2,指令感知 |
| gte-large-zh | 1024 | 中文优化 |
| Qwen3-Embedding-8B | 4096 | 最新旗舰,多语言 + 重排序 |
核心优势:
- 基于 Qwen 大模型作为骨干网络,语义理解深刻
- 指令感知(Instruction-aware):可在输入前加指令(如"根据语义相似度检索"),让模型针对不同任务调整 Embedding 方向
- 2025 年 Qwen3 Embedding 在 MTEB 等多个基准上达到 SOTA(当前最优)
模型选型速查表
场景 推荐模型
──────────────────────────────────────────────
快速原型、英文为主 → text-embedding-3-small
高精度英文 → text-embedding-3-large
中文场景、本地部署 → bge-large-zh-v1.5
多语言、长文本 → bge-m3
需要指令感知 → gte-Qwen2-7B-instruct
追求极致性能 → Qwen3-Embedding-8B选型建议:如果你刚开始做 RAG 项目,先用
text-embedding-3-small快速跑通原型;如果中文是主战场且需要本地部署,切换到bge-large-zh-v1.5;如果追求极致效果且有足够算力,考虑Qwen3-Embedding-8B。
6.2.5 维度选择策略:精度与成本的平衡术
选好模型后,还要决定用多少维。Embedding 维度直接影响检索精度和存储/计算成本。维度越高,语义信息越丰富,但存储更大、检索更慢。
维度 256: 存储小,速度快,精度约 95%(相比 1536 维)
维度 512: 存储中等,速度中等,精度约 98%
维度 768: 存储较大,速度较慢,精度约 99%
维度 1536: 存储大,速度慢,精度 100%实际选型时,数据量是关键参考因素:
| 数据量 | 推荐维度 | 理由 |
|---|---|---|
| < 10 万条 | 768-1536 | 数据量小,用高精度,存储不是瓶颈 |
| 10-100 万条 | 512-768 | 平衡精度与存储 |
| > 100 万条 | 256-512 | 优先考虑存储和速度 |
| 成本敏感 | 256 | 使用 Matryoshka 截断 |
Matryoshka 技巧:用 text-embedding-3-large 生成 3072 维向量,存储时截断到 256 维,检索精度损失极小(约 2-3%),但存储减少 12 倍,速度提升 10 倍以上。这是大规模检索场景中最实用的优化手段之一。
6.2.6 实战:多模型 Embedding 对比
理论讲完,下面动手实践。我们将用 OpenAI 和 BGE 两个模型分别对同一批文本进行向量化,然后对比检索效果。
# 安装依赖:pip install openai sentence-transformers numpy
import numpy as np # 数值计算库,用于向量运算
import time # 用于计时
from openai import OpenAI # OpenAI Python SDK
from sentence_transformers import SentenceTransformer # 开源模型加载库
# ====== 测试文本 ======
# 查询列表:模拟用户可能提出的问题
queries = [
"如何制作红烧肉",
"Python 编程入门教程",
"2024 年奥运会金牌榜",
]
# 文档列表:模拟知识库中的文档片段
documents = [
"红烧肉是一道经典的中式菜肴,主要食材是五花肉...",
"Python 是一种高级编程语言,由 Guido van Rossum 创建...",
"2024 年巴黎奥运会于 7 月 26 日至 8 月 11 日举行...",
"今天天气很好,适合出去散步", # 干扰项
"机器学习是人工智能的一个分支...", # 干扰项
]
# ====== 1. OpenAI Embedding ======
def get_openai_embedding(text, model="text-embedding-3-small", dims=512):
"""调用 OpenAI API 生成文本向量
参数:
text: 要编码的文本字符串
model: 模型名称,默认 text-embedding-3-small
dims: 输出维度,利用 Matryoshka 特性可指定 256/512/1536
返回:
numpy 数组,长度等于 dims
"""
client = OpenAI() # 初始化客户端,需设置 OPENAI_API_KEY
response = client.embeddings.create(
model=model, # 指定 Embedding 模型
input=text, # 输入文本
dimensions=dims # 输出维度
)
# response.data[0].embedding 是一个浮点数列表
# 转为 numpy 数组方便后续计算
return np.array(response.data[0].embedding)
# ====== 2. BGE 模型(本地运行,无需 API Key)======
# 加载 BGE 中文大模型(首次运行会自动下载约 1.3GB 权重)
bge_model = SentenceTransformer("BAAI/bge-large-zh-v1.5")
def get_bge_embedding(text):
"""使用 BGE 模型生成文本向量
注意:BGE 模型在检索场景下,查询文本需要加特殊前缀:
'为这个句子生成表示以用于检索相关文章:'
但文档端不需要加前缀。这里简化处理。
"""
# normalize_embeddings=True 做归一化,模长变为 1
# 归一化后余弦相似度 = 点积,计算更快
return bge_model.encode(text, normalize_embeddings=True)
# ====== 3. 相似度检索对比 ======
def run_retrieval_test(name, encode_fn, queries, documents):
"""对指定模型进行检索效果测试
参数:
name: 模型名称(用于打印标题)
encode_fn: 编码函数,输入文本返回向量
queries: 查询列表
documents: 文档列表
"""
print(f"\n{'='*50}")
print(f"测试模型: {name}")
print(f"{'='*50}")
for query in queries:
start = time.time()
# 编码查询文本,得到查询向量
q_vec = encode_fn(query)
# 编码所有文档,得到文档向量列表
doc_vecs = [encode_fn(doc) for doc in documents]
# 计算查询与每个文档的相似度(这里用点积)
similarities = [np.dot(q_vec, dv) for dv in doc_vecs]
# 按相似度从高到低排序
ranked = sorted(
zip(documents, similarities), # 将文档与相似度配对
key=lambda x: x[1], # 按相似度排序
reverse=True # 降序排列
)
elapsed = time.time() - start
print(f"\n查询: {query}")
print(f"Top-3 结果 (耗时 {elapsed:.2f}s):")
for i, (doc, score) in enumerate(ranked[:3]):
# 只打印文档前 50 个字符和相似度分数
print(f" {i+1}. [{score:.4f}] {doc[:50]}...")
# ====== 4. 运行测试 ======
# 注意:OpenAI 测试需要设置 OPENAI_API_KEY 环境变量
# 取消下面一行的注释即可运行
# run_retrieval_test("OpenAI text-embedding-3-small",
# get_openai_embedding, queries, documents)
# BGE 模型可以本地直接运行
print("\n" + "="*50)
print("BGE 模型本地测试")
print("="*50)
query = "红烧肉怎么做"
doc1 = "红烧肉是一道经典中式菜肴,需要五花肉、酱油、冰糖等食材..."
doc2 = "Python 是一种编程语言,广泛用于数据科学和 Web 开发..."
# 分别编码查询和两个文档
q_emb = bge_model.encode(query, normalize_embeddings=True)
d1_emb = bge_model.encode(doc1, normalize_embeddings=True)
d2_emb = bge_model.encode(doc2, normalize_embeddings=True)
# 计算相似度(归一化后直接用点积)
sim1 = np.dot(q_emb, d1_emb) # 查询 vs 相关文档
sim2 = np.dot(q_emb, d2_emb) # 查询 vs 无关文档
print(f"查询: {query}")
print(f"与文档1相似度: {sim1:.4f} ← 应该更高(语义相关)")
print(f"与文档2相似度: {sim2:.4f} ← 应该更低(语义无关)")
# 预期输出:sim1 明显大于 sim26.2.7 实战:维度选择实验
接下来做一个有趣的实验:同一段文本,用不同维度生成 Embedding,观察存储大小和精度的变化。
import numpy as np
from openai import OpenAI
client = OpenAI() # 初始化客户端
# 测试文本
text = "人工智能正在改变世界,从医疗诊断到自动驾驶,AI 技术无处不在。"
# 测试不同维度:利用 Matryoshka 特性,同一模型可输出不同维度
dimensions = [256, 512, 768, 1536, 3072]
embeddings = {} # 存储各维度向量
for dim in dimensions:
response = client.embeddings.create(
model="text-embedding-3-large", # 用 large 模型,支持最高 3072 维
input=text, # 输入文本
dimensions=dim # 指定输出维度
)
embeddings[dim] = np.array(response.data[0].embedding) # 存为 numpy 数组
# .nbytes 属性返回数组占用的字节数(每个 float32 占 4 字节)
print(f"维度 {dim:4d}: 向量大小 = {embeddings[dim].nbytes} bytes")
# 模拟分析:维度缩减对精度和存储的影响
print("\n--- 维度缩减影响分析 ---")
print(f"{'维度':>6} | {'存储大小':>10} | {'相对精度':>10} | {'推荐场景':>20}")
print("-" * 60)
# 预估数据(基于 MTEB 基准测试的经验值)
info = {
256: ("1 KB", "~95%", "大规模检索(>100万)"),
512: ("2 KB", "~97%", "中等规模(10-100万)"),
768: ("3 KB", "~99%", "中小规模(1-10万)"),
1536: ("6 KB", "100%", "小规模(<1万)"),
3072: ("12 KB", "100%", "极致精度需求"),
}
for dim in dimensions:
size, acc, scenario = info[dim]
print(f"{dim:6d} | {size:>10} | {acc:>10} | {scenario:>20}")
print("\n结论:256 维仅损失约 5% 精度,但存储减少 12 倍!")6.2.8 常见误区
在实际应用 Embedding 的过程中,有几个常见的"坑"值得特别提醒:
误区一:维度越高越好
很多人觉得 3072 维肯定比 256 维好。但实际上,当数据量不大(比如几千条文档)时,高维度并不会带来明显的精度提升,反而会增大存储开销和检索延迟。维度选择应该与数据量匹配,而不是盲目追求最高维度。
误区二:不同模型的向量可以直接比较
Embedding 向量是模型特定的——OpenAI 生成的向量与 BGE 生成的向量在完全不同的空间中,即使维度相同也不能混用。一个系统中,查询和文档必须使用同一个模型编码。如果要更换模型,整个知识库的向量都需要重新生成。
误区三:余弦相似度越高就是"越相关"
余弦相似度衡量的是语义方向的接近程度,但 0.85 和 0.90 之间的差异并不总是意味着后者明显更相关。在实际 RAG 系统中,通常取 Top-K(如 Top 5 或 Top 10)结果,而不是设定一个绝对阈值。此外,对于某些查询(如多义词查询),高相似度也可能意味着方向跑偏。
误区四:BGE 查询前缀可有可无
BGE 模型在官方文档中明确建议:检索场景下查询端需要加前缀"为这个句子生成表示以用于检索相关文章:",而文档端不需要。省略前缀会导致检索效果下降,尤其是在短查询场景下。这是一个容易被忽略但对效果有实际影响的细节。
误区五:Embedding 可以替代关键词检索
Embedding 擅长语义匹配("红烧肉怎么做" ↔ "猪肉炖煮方法"),但对精确关键词匹配(如产品编号 "SKU-12345"、人名"张三")反而不如传统的 BM25 关键词检索。实际生产系统中,通常采用混合检索(Hybrid Search)——结合向量检索和关键词检索,取两者之长。
6.2.9 本节小结
| 要点 | 说明 |
|---|---|
| Embedding 本质 | 将文本映射到高维向量空间,语义相近的文本向量距离近。可以类比为"给文字装上 GPS 坐标" |
| 对比学习 | 现代模型的训练方法:拉近正样本对、推远负样本对,让向量空间编码语义信息 |
| 余弦相似度 | 最常用的相似度度量,只看方向不看长度,取值 [-1, 1]。归一化后等于点积 |
| text-embedding-3 | OpenAI 的 Embedding API,支持 Matryoshka 多维度输出,性价比高,适合快速原型 |
| BGE 系列 | 智源开源,中文能力最强,支持本地部署,数据不出域 |
| GTE/Qwen3 | 阿里达摩院,基于 Qwen 大模型,指令感知,2025 年多个基准 SOTA |
| 维度选择 | 数据量大用低维(256-512),数据量小用高维(768-1536)。Matryoshka 截断是大规模场景的实用优化 |
| 混合检索 | 向量检索擅长语义匹配,关键词检索擅长精确匹配,生产环境推荐两者结合 |
关键认知:
- Embedding 是 RAG 流水线的"翻译器"——把人能读懂的文字翻译成机器能比较的数字。
- 模型选择没有银弹:快速原型用 OpenAI,中文本地部署用 BGE,追求极致用 Qwen3。
- 维度选择是工程权衡:不是越高越好,而是与数据量和性能需求匹配。
- 代码中务必注意归一化:BGE 默认归一化后点积等于余弦相似度,OpenAI 则不一定。
延伸阅读
- OpenAI Embeddings 官方文档 — text-embedding-3 系列的使用指南与 API 参考
- BGE GitHub 仓库 — BGE 系列模型,包含使用教程和评测结果
- Qwen3 Embedding 论文 — 2025 年 Qwen3 Embedding 系列,介绍最新 SOTA 嵌入模型
- MTEB Leaderboard — Massive Text Embedding Benchmark,权威 Embedding 模型排行榜
- Sentence-Transformers 文档 — 最流行的 Embedding 模型 Python 库,支持 BGE/GTE 等模型
有了 Embedding 模型,我们可以把任意文本变成向量。但在实际 RAG 系统中,我们面对的往往是一篇完整的文档——可能是一份几十页的 PDF、一篇长文、甚至一本电子书。我们无法把整篇文档编码成一个向量,因为长文档涵盖多个主题,单个向量无法精确表达。这就引出了下一节的主题:6.3 文档分块(Chunking)——如何把长文档切成大小合适的片段,让每个片段都能被 Embedding 模型准确编码,是 RAG 效果好坏的分水岭。