Skip to content

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="今天天气真好..."      → 推远

具体来说,模型在训练时会同时看到:

  1. 一个锚点(Anchor):比如"如何做红烧肉"
  2. 一个正样本(Positive):与锚点语义匹配的文本,如"红烧肉的做法步骤"
  3. 多个负样本(Negative):与锚点语义不匹配的文本,如"今天天气真好""Python 入门教程"

模型的损失函数(通常是 InfoNCE)会同时做两件事:

  • 把锚点和正样本在向量空间中的距离缩小
  • 把锚点和所有负样本的距离拉大

经过数亿条文本对的训练,模型学会了把语义信息"编码"到向量空间的各个维度中。最终,每个维度都隐含地代表了某种语义特征——比如某个维度可能隐约对应"是否与食物相关",另一个维度可能对应"是否是疑问句"——但这些维度并非人类可以直观解释的,它们是模型自动学到的隐含表示。


6.2.3 相似度度量:如何计算两段文字"有多像"

有了向量,下一个问题就是:怎么衡量两个向量之间的"距离"? 这在 RAG 检索中至关重要——系统需要根据这个距离排序,找出最相关的文档。

常用的度量方式有三种:

度量方式公式取值范围特点
余弦相似度cos(A,B) = A·B / (A×
欧氏距离d(A,B) = √Σ(Ai-Bi)²[0, +∞)几何直线距离,需归一化
点积A·B = Σ(Ai×Bi)(-∞, +∞)计算最快,但受向量长度影响

为什么余弦相似度最常用? 因为在语义检索中,我们关心的是两段文字的"方向"是否一致,而不是向量的"长度"。打个比方,两个人站在不同距离外描述同一栋楼,虽然声音大小不同(向量长度不同),但他们指向的方向是一致的(余弦相似度高)。余弦相似度忽略了长度,只保留方向信息,因此更适合比较语义相似性。

下面通过代码来理解余弦相似度的计算过程:

python
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-small512 / 153662.3$0.00002性价比之王
text-embedding-3-large256 / 1024 / 307264.6$0.00013最高精度

核心优势

  • 支持 Matryoshka 表示学习:同一个模型可以输出不同维度(256、512、1536),维度越低检索越快
  • 多语言支持好,中文表现不错
  • 无需自部署,API 调用简单,几行代码即可集成
python
# 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.51024领先中文专精,开源可部署
bge-m31024领先多语言 + 8192 token 长度
bge-small-zh-v1.5512良好轻量快速,适合资源受限

核心优势

  • 中文最强:在中文检索、分类等任务上表现优异,尤其在古文、专业术语等领域
  • 开源可本地部署,数据不出域,满足隐私合规要求
  • bge-m3 支持超长文本(8192 token),适合处理长文档而无需分块过细
python
# 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-instruct3584基于 Qwen2,指令感知
gte-large-zh1024中文优化
Qwen3-Embedding-8B4096最新旗舰,多语言 + 重排序

核心优势

  • 基于 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 两个模型分别对同一批文本进行向量化,然后对比检索效果。

python
# 安装依赖: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 明显大于 sim2

6.2.7 实战:维度选择实验

接下来做一个有趣的实验:同一段文本,用不同维度生成 Embedding,观察存储大小和精度的变化。

python
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-3OpenAI 的 Embedding API,支持 Matryoshka 多维度输出,性价比高,适合快速原型
BGE 系列智源开源,中文能力最强,支持本地部署,数据不出域
GTE/Qwen3阿里达摩院,基于 Qwen 大模型,指令感知,2025 年多个基准 SOTA
维度选择数据量大用低维(256-512),数据量小用高维(768-1536)。Matryoshka 截断是大规模场景的实用优化
混合检索向量检索擅长语义匹配,关键词检索擅长精确匹配,生产环境推荐两者结合

关键认知

  1. Embedding 是 RAG 流水线的"翻译器"——把人能读懂的文字翻译成机器能比较的数字。
  2. 模型选择没有银弹:快速原型用 OpenAI,中文本地部署用 BGE,追求极致用 Qwen3。
  3. 维度选择是工程权衡:不是越高越好,而是与数据量和性能需求匹配。
  4. 代码中务必注意归一化:BGE 默认归一化后点积等于余弦相似度,OpenAI 则不一定。

延伸阅读

  1. OpenAI Embeddings 官方文档 — text-embedding-3 系列的使用指南与 API 参考
  2. BGE GitHub 仓库 — BGE 系列模型,包含使用教程和评测结果
  3. Qwen3 Embedding 论文 — 2025 年 Qwen3 Embedding 系列,介绍最新 SOTA 嵌入模型
  4. MTEB Leaderboard — Massive Text Embedding Benchmark,权威 Embedding 模型排行榜
  5. Sentence-Transformers 文档 — 最流行的 Embedding 模型 Python 库,支持 BGE/GTE 等模型

有了 Embedding 模型,我们可以把任意文本变成向量。但在实际 RAG 系统中,我们面对的往往是一篇完整的文档——可能是一份几十页的 PDF、一篇长文、甚至一本电子书。我们无法把整篇文档编码成一个向量,因为长文档涵盖多个主题,单个向量无法精确表达。这就引出了下一节的主题:6.3 文档分块(Chunking)——如何把长文档切成大小合适的片段,让每个片段都能被 Embedding 模型准确编码,是 RAG 效果好坏的分水岭。