Embedding从入门到落地|词向量、语义空间与RAG实战选型
导读:做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崛起的原因:
- 一词多义搞不定:"苹果"是水果还是公司?Word2Vec只能给一个向量,强行折中
- 没有上下文感知:"我打游戏"和"我打电话"里的"打",向量完全一样
- 只能处理单词:对整个句子做Embedding?没门,得靠后面加池化操作
⚠️ 踩坑提醒:我之前做过一个商品搜索项目,直接用Word2Vec做句子匹配,效果惨不忍睹。后来换了Sentence-BERT,NDCG直接涨了15个点。所以Word2Vec了解原理就行,生产环境做语义匹配请直接上Sentence Embedding。
三、从词向量到句向量:Sentence Embedding的进化
3.1 语义空间是什么?
你可以把语义空间想象成一个高维的"地图"。在这个地图上:
- 距离近 = 语义相似
- 方向一致 = 语义关系一致
经典的例子是Word2Vec里的类比推理:
国王 - 男人 + 女人 ≈ 女王
这在向量空间里是实打实的数学运算,挺神奇的。
3.2 现代Embedding模型怎么工作?
现在的Sentence Embedding(比如BGE、GTE、E5)基本都是基于Transformer的。流程大概是:
- Tokenizer分词:把句子切成Token
- Transformer编码:经过多层注意力机制,每个Token拿到上下文信息
- 池化(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.5 | 1024 | 512 | 智源出品,C-MTEB霸榜 | 通用RAG,首选 |
| BGE-M3 | 1024 | 8192 | 多语言+多粒度+多功能 | 长文档、多语言 |
| GTE-large-zh | 1024 | 512 | 阿里达摩院,效果接近BGE | 阿里云生态 |
| M3E-base | 768 | 512 | 开源社区,轻量 | 资源受限场景 |
| BCEmbedding | 768 | 512 | 网易有道,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 | 轻量、易用、本地优先 | 原型开发、小项目 |
| Qdrant | Rust写的、高性能、过滤查询强 | 中等规模、需要元数据过滤 |
| pgvector | PostgreSQL插件 | 已有PG基础设施 |
| Faiss | Meta出品、纯检索库 | 自研系统、极致性能 |
个人偏好:原型用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:
- 别自己造轮子:除非有特殊需求,直接拿BGE-large-zh-v1.5或BGE-M3,效果经过验证
- 注意查询指令:BGE的instruction不是摆设,加了和不加可能差10个点
- 长文档选M3:普通512长度模型切分长文档会丢语义,M3的8K上下文+稀疏检索是真香
- Reranker是性价比之王:加一层精排,召回质量提升明显,计算成本可控
- 相似度计算要归一化:避免点积的模长偏差,生产环境统一余弦相似度
Embedding这个领域发展很快,从Word2Vec到BERT再到现在的BGE、GTE、E5,每代都在解决前代的问题。做工程不用追最新,选对模型、调对参数、搭好链路,比用什么SOTA模型更重要。