导读:做RAG项目时,你是不是也踩过这样的坑——明明检索召回了一堆文档,大模型却答不到点上?问题大概率出在Embedding身上。这篇从Word2Vec讲到BGE选型,把Embedding的里里外外掰开揉碎,附带几个案例,看完直接上手落地。


一、Embedding到底是个啥?别被术语吓到

说白了,Embedding就是把文字、图片这种人类能懂的东西,转成计算机能算的向量(一堆数字)

举个例子:

  • "苹果" → [0.12, -0.33, 0.87, ...](768维或1024维的向量)
  • "香蕉" → [0.15, -0.28, 0.82, ...]

这两个向量在数学上离得近,说明语义相近。这就是Embedding的魔力——把语义相似性变成了距离计算

1.1 为什么不用独热编码(One-Hot)?

早年间确实这么干过。词表里有10万个词,每个词就是一个10万维的向量,只有一个位置是1,其他全是0。

问题很明显:

  • "苹果"和"香蕉"的One-Hot向量,余弦相似度永远是0
  • 完全表达不出"都是水果"这层关系
  • 维度爆炸,10万维直接劝退

Embedding解决的就是这个痛点——用低维稠密向量编码语义


二、Word2Vec:一切的开始

2013年Google提出的Word2Vec,算是Embedding领域的开山鼻祖。虽然今天做RAG基本不会直接用Word2Vec了,但理解它的原理对后续学习很有帮助。

2.1 核心思想:上下文即语义

Word2Vec的假设很朴素:语义相近的词,经常出现在相似的上下文里

比如"苹果"和"香蕉",前面经常跟着"吃"、"买",后面经常跟着"很甜"、"成熟了"。模型通过预测上下文,慢慢学会把这两个词放到向量空间里相近的位置。

2.2 两种训练方式

模型思路特点
Skip-gram用中心词预测周围词对罕见词效果好,训练慢一些
CBOW用周围词预测中心词训练快,对常见词更稳

实际项目中,如果你要自训练词向量,Skip-gram通常是首选。不过说实话,现在做业务谁还自己训Word2Vec啊,直接拿预训练模型不香吗 😂

2.3 Word2Vec的局限性(重点!)

这里要敲黑板了,Word2Vec有几个硬伤,也是后来BERT、Sentence Embedding崛起的原因:

  1. 一词多义搞不定:"苹果"是水果还是公司?Word2Vec只能给一个向量,强行折中
  2. 没有上下文感知:"我游戏"和"我电话"里的"打",向量完全一样
  3. 只能处理单词:对整个句子做Embedding?没门,得靠后面加池化操作

⚠️ 踩坑提醒:我之前做过一个商品搜索项目,直接用Word2Vec做句子匹配,效果惨不忍睹。后来换了Sentence-BERT,NDCG直接涨了15个点。所以Word2Vec了解原理就行,生产环境做语义匹配请直接上Sentence Embedding


三、从词向量到句向量:Sentence Embedding的进化

3.1 语义空间是什么?

你可以把语义空间想象成一个高维的"地图"。在这个地图上:

  • 距离近 = 语义相似
  • 方向一致 = 语义关系一致

经典的例子是Word2Vec里的类比推理

国王 - 男人 + 女人 ≈ 女王

这在向量空间里是实打实的数学运算,挺神奇的。

3.2 现代Embedding模型怎么工作?

现在的Sentence Embedding(比如BGE、GTE、E5)基本都是基于Transformer的。流程大概是:

  1. Tokenizer分词:把句子切成Token
  2. Transformer编码:经过多层注意力机制,每个Token拿到上下文信息
  3. 池化(Pooling):把所有Token的向量压缩成一个句子向量
    • CLS token:取特殊标记位的向量
    • Mean Pooling:取所有Token的平均(最常用)
    • Last Hidden State:取最后一层

💡 小Tips:BGE默认用的是Last Hidden State的Mean Pooling,效果通常比单纯取CLS好。如果你微调模型,Pool方式值得实验一下。


四、相似度计算:怎么判断两句话像不像?

Embedding搞出来了,怎么比?三种常用方法:

import numpy as np

# 四种相似度计算方法

def cosine_similarity(a, b):
    """
    余弦相似度(最常用)
    结果在 -1 到 1 之间,越接近1越相似
    优点:不受向量长度影响,只关心方向
    """
    return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))

def euclidean_distance(a, b):
    """
    欧氏距离
    距离越小越相似
    适用:聚类、需要绝对距离的场景
    """
    return np.linalg.norm(a - b)

def dot_product(a, b):
    """
    点积(内积)
    特点:简单快速,但受向量模长影响
    如果向量已经归一化,点积就等于余弦相似度
    """
    return np.dot(a, b)

def cosine_from_dot(a, b):
    """
    归一化后的点积等于余弦相似度
    更快,因为不需要计算norm
    """
    return np.dot(a / np.linalg.norm(a), b / np.linalg.norm(b))

# 测试示例
a = np.array([0.1, 0.2, 0.3])
b = np.array([0.11, 0.21, 0.31])
c = np.array([0.9, 0.8, 0.7])

print("相似示例 (a vs b):")
print(f"  余弦相似度: {cosine_similarity(a, b):.4f}")
print(f"  欧氏距离: {euclidean_distance(a, b):.4f}")
print(f"  点积: {dot_product(a, b):.4f}")

print("\n不相似示例 (a vs c):")
print(f"  余弦相似度: {cosine_similarity(a, c):.4f}")
print(f"  欧氏距离: {euclidean_distance(a, c):.4f}")
print(f"  点积: {dot_product(a, c):.4f}")

# 输出
# 相似示例 (a vs b):
#   余弦相似度: 0.9999
#   欧氏距离: 0.0173
#   点积: 0.1460

# 不相似示例 (a vs c):
#   余弦相似度: 0.8827
#   欧氏距离: 1.0770
#   点积: 0.4600

⚠️ 踩坑提醒:我早期做RAG时没注意,用了点积但没归一化向量,结果长文档的得分总是偏高(因为向量模长大),召回质量很差。后来统一换成余弦相似度,问题解决。建议生产环境统一用余弦相似度,省心


五、实战选型:中文Embedding模型怎么选?

5.1 先看榜单:MTEB和C-MTEB

选模型别凭感觉,看榜单。MTEB(Massive Text Embedding Benchmark)是目前最权威的Embedding评测基准,中文对应的是C-MTEB。

榜单地址:

5.2 主流中文Embedding模型对比

模型维度上下文长度特点适用场景
BGE-large-zh-v1.51024512智源出品,C-MTEB霸榜通用RAG,首选
BGE-M310248192多语言+多粒度+多功能长文档、多语言
GTE-large-zh1024512阿里达摩院,效果接近BGE阿里云生态
M3E-base768512开源社区,轻量资源受限场景
BCEmbedding768512网易有道,RAG优化特定RAG场景

5.3 我的选型建议

无脑选:BGE-large-zh-v1.5,效果稳、社区活跃、文档全。

长文档场景(比如整篇论文、合同):上BGE-M3,支持8192 tokens,还能做稀疏检索(关键词匹配+向量检索双管齐下)。

资源紧张(边缘设备、高并发):BGE-small-zh-v1.5或者M3E-base,维度低、推理快。

💡 一个真实案例:我们之前做法律文档检索,合同动辄上万字。用普通512长度的模型,切分后语义断裂严重。换成BGE-M3的8K长上下文,配合它自带的稀疏检索(lexical matching),Recall@10从62%提到了81%。长文档场景,上下文长度和稀疏检索能力真的很关键。


六、进阶:Matryoshka Embedding——一套向量多种用法

这是2022年提出的一种很巧妙的思路,名字取自俄罗斯套娃(Matryoshka)。

6.1 核心思想

训练时让模型学习层次化的向量表示

  • 前64维:粗略语义
  • 前128维:更精细
  • 前256维:更更精细
  • ...直到完整维度

6.2 有什么好处?

想象一下这个场景:

  • 粗排阶段:用64维快速过滤,减少候选集
  • 精排阶段:用256维仔细比对
  • 最终匹配:用完整1024维确保精度

一套向量,按需取用。不用存多份,不用换模型,直接切片就行。

OpenAI的text-embedding-3系列就支持这个特性,BGE的新版本也在跟进。如果你的系统有多级检索需求,Matryoshka值得关注。

# Matryoshka Embedding 模拟示例

def matryoshka_slice(embedding, dimensions):
    """
    从完整向量中按维度截取
    dimensions: 需要的维度列表,如 [64, 128, 256, 1024]
    """
    return {d: embedding[:d] for d in dimensions}

# 模拟一个1024维的完整向量
full_embedding = np.random.randn(1024)

# 按需截取
dimensions = [64, 128, 256, 512, 1024]
sliced = matryoshka_slice(full_embedding, dimensions)

print("Matryoshka Embedding 示例:")
for dim, vec in sliced.items():
    print(f"  {dim:4d}维向量 → 存储大小: {vec.nbytes} bytes")

# 输出:
# Matryoshka Embedding 示例:
#     64维向量 → 存储大小: 512 bytes
#    128维向量 → 存储大小: 1024 bytes
#    256维向量 → 存储大小: 2048 bytes
#    512维向量 → 存储大小: 4096 bytes
#   1024维向量 → 存储大小: 8192 bytes

七、RAG中的Embedding实战要点

7.1 检索链路里的Embedding

一个典型的RAG检索链路长这样:

用户问题 → Embedding模型编码 → 向量数据库ANN检索 → Top-K召回 → Rerank精排 → 送入LLM

Embedding的质量直接决定了召回率(能不能找到相关文档)。

7.2 几个关键调优点

1. 查询指令(Instruction)

BGE系列模型支持查询指令,加不加效果差很多:

# 编码查询时加上指令
query_instruction = "为这个句子生成表示以用于检索相关文章:"
query_embedding = model.encode(query, instruction=query_instruction)

# 文档不需要加指令
doc_embedding = model.encode(documents)

2. 向量归一化

# 编码后做L2归一化
embeddings = model.encode(sentences, normalize_embeddings=True)

归一化后可以直接用点积代替余弦相似度,计算更快。

3. Reranker加持

Embedding检索是"双塔"结构(Query和Doc分别编码),精度有上限。加一层Cross-Encoder Reranker(比如BGE-Reranker),让Query和Doc一起过模型做精细匹配,能再提5-10个点。

from FlagEmbedding import FlagAutoReranker

reranker = FlagAutoReranker.from_finetuned('BAAI/bge-reranker-v2-m3')
scores = reranker.compute_score(query_doc_pairs)

⚠️ 性能注意:Reranker是Cross-Encoder,计算量比Embedding大得多。建议只让它处理Top-20到Top-100的候选,别全量过。

7.3 向量数据库选型速查

数据库特点适合场景
Milvus功能全、分布式、云原生大规模生产环境
Chroma轻量、易用、本地优先原型开发、小项目
QdrantRust写的、高性能、过滤查询强中等规模、需要元数据过滤
pgvectorPostgreSQL插件已有PG基础设施
FaissMeta出品、纯检索库自研系统、极致性能

个人偏好:原型用Chroma,上生产用Milvus或Qdrant


八、代码速查:BGE快速上手

from FlagEmbedding import FlagAutoModel
import numpy as np

# 加载模型(首次会自动下载)
model = FlagAutoModel.from_finetuned(
    'BAAI/bge-large-zh-v1.5',
    use_fp16=True  # 半精度加速
)

# 准备数据
queries = ["如何优化RAG系统的检索效果?"]
documents = [
    "RAG检索优化可以从Embedding模型选择、向量数据库调优、Reranker精排等方面入手...",
    "深度学习模型训练需要关注学习率、批量大小、数据增强等超参数...",
    "Embedding模型将文本映射到语义空间,相似文本距离更近..."
]

# 编码(查询加指令,文档不加)
query_instruction = "为这个句子生成表示以用于检索相关文章:"
query_embeddings = model.encode(queries, instruction=query_instruction)
doc_embeddings = model.encode(documents)

# 计算相似度
similarity = query_embeddings @ doc_embeddings.T
print("相似度:", similarity)
# 输出: [[0.89, 0.12, 0.76]] — 第一个文档最相关

# 获取Top-K
k = 2
top_k_indices = np.argsort(similarity[0])[-k:][::-1]
print(f"Top-{k} 相关文档索引: {top_k_indices}")

九、个人小结

Embedding是RAG系统的"地基",地基不稳,上面盖啥都白搭。几个我觉得最重要的 takeaway:

  1. 别自己造轮子:除非有特殊需求,直接拿BGE-large-zh-v1.5或BGE-M3,效果经过验证
  2. 注意查询指令:BGE的instruction不是摆设,加了和不加可能差10个点
  3. 长文档选M3:普通512长度模型切分长文档会丢语义,M3的8K上下文+稀疏检索是真香
  4. Reranker是性价比之王:加一层精排,召回质量提升明显,计算成本可控
  5. 相似度计算要归一化:避免点积的模长偏差,生产环境统一余弦相似度

Embedding这个领域发展很快,从Word2Vec到BERT再到现在的BGE、GTE、E5,每代都在解决前代的问题。做工程不用追最新,选对模型、调对参数、搭好链路,比用什么SOTA模型更重要。