Embedding 质量的差距,最终会放大成检索与 RAG 效果的差距。向量化不是「调个 API」那么简单——对比学习怎么训、负样本怎么挖、距离怎么算,每一个决定都影响召回。本文把 Embedding 从原理讲到工程。
Embedding 为什么有效
Embedding 把离散文本(或图像、多模态内容)映射成连续向量,让「语义相近 → 向量相近」。从词向量(Word2Vec/Glove)到句子向量(Sentence-Transformer)再到多模态向量(CLIP),共同内核是在向量空间中用距离度量语义相似度。
一个常被误解的点:Embedding 本身无所谓好坏,好坏在于「相似度是否与语义一致」。同一个向量空间里,余弦相似度高的一对样本语义应该相近;如果距离关系和人类判断对不上,这个 Embedding 就没用。训练的目标就是让「向量空间的拓扑 ≈ 语义空间的拓扑」。
对比学习:Embedding 训练的核心范式
现代 Embedding 训练几乎都用对比学习(Contrastive Learning):让模型学会「正样本对靠近、负样本对远离」。核心损失是 InfoNCE(温度缩放交叉熵):
# InfoNCE 损失示意(批次内所有样本互为负样本)
# L = -log[ exp(sim(q, pos)/τ) / Σ_j exp(sim(q, neg_j)/τ) ]
# sim: 余弦相似度 | τ: 温度系数 | pos: 正样本 | neg_j: 批次内其他样本
import torch, torch.nn.functional as F
def contrastive_loss(q, pos, temperature=0.05):
logits = q @ pos.T / temperature # 批次内相似度矩阵
labels = torch.arange(len(q), device=q.device) # 对角线为正样本
return F.cross_entropy(logits, labels)
三个核心超参:
- 温度系数 τ:τ 越小,损失对「难分样本」越敏感,区分度越高,但训练越不稳定。常用 0.02~0.1。
- 正样本来源:同义词、相似问题、相似文档、数据增强扰动。正样本质量决定 Embedding 上限。
- 负样本来源:批次内其他样本、随机采样、难负样本挖掘。负样本是训练稳定的关键。
双塔模型与 Sentence-Transformer
双塔(Two-Tower)架构是 Embedding 的主流形态:Query 塔和 Document 塔(或共享权重的单塔)各自把输入编码成向量,训练时用对比损失拉近正对、推远负对。推理时两条塔独立编码,向量预先索引,查询只需计算一次向量再做 ANN 检索——这是向量检索低延迟的架构基础。
Sentence-Transformer 是双塔思想的开源实现,用预训练 Transformer(BERT 系)加池化层输出句子向量。工程要点:
# Sentence-Transformer 加载与编码
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("BAAI/bge-m3") # 支持 dense + sparse + multi-vector
emb = model.encode("如何优化 RAG 召回率", normalize_embeddings=True)
微调 Embedding 模型的常用套路是在已有 Sentence-Transformer 上做领域适配:构造领域内的相似/不相似对,用对比损失继续训练几轮,比从头训省太多。数据量小(几千对)也能有明显提升。
训练数据构造:正样本、负样本与难负样本
Embedding 模型吃数据的方式很挑剔,负样本设计是质量的分水岭:
- 随机负样本:随便抽的不相关样本。太简单,模型学不到判别力。
- 批次内负样本(In-batch Negatives):同 batch 其他样本当负样本,几乎零成本,是默认配置。batch 越大负样本越多样。
- 难负样本(Hard Negatives):检索结果里相似但不相关的样本。这是最重要的负样本——它逼模型区分「看似相关实则无关」的边界,直接决定检索精度的上限。
# 难负样本挖掘的简化流程
# 1) 用当前模型对语料建索引
# 2) 对每个正样本检索 top-K 相似但不匹配的文档
# 3) 把这些"假阳性"作为难负样本加入下一轮训练
# 4) 可迭代多轮(hard negative mining),每轮精度提升
**知识蒸馏(Distillation)**是数据不足时的高效补量:用强模型(如 GPT 打分、LLM 生成)为弱模型制造相似/不相似对,让弱模型学强模型的相似度分布。蒸馏数据往往比人工标注性价比高得多。
相似度计算与归一化
向量建好索引后,检索就是算距离。三种常用度量各有用场:
- 余弦相似度(Cosine):对向量长度不敏感,适合文本语义(只看方向)。默认首选。
- 点积(Dot Product):长度也参与计分,常用于评分模型、多模态匹配。
- 欧氏距离(L2):几何距离,物理/坐标类场景更直观。
# 归一化是检索正确性的关键一步
# 归一化后: 余弦相似度 == 点积 | 欧氏距离与余弦单调对应
# 不归一化直接做点积检索,长向量会系统性偏高分,召回被污染
# 生产实践:编码后一律 normalize_embeddings=True
工程铁律:除非明确需要长度信息,一律归一化后统一用点积。这样既保留余弦语义,又能利用向量库对点积/内积的硬件优化(比余弦计算快)。
向量索引与量化:召回与精度的平衡
Embedding 建索引后要用 ANN(近似最近邻) 检索才能撑住百万级规模,常用 HNSW / IVF-PQ:
- HNSW:图式索引,召回率高、查询快,但内存占用大。中小规模(<1000 万)首选。
- IVF-PQ:先聚类再量化,省内存、可扩展,但参数多、召回略降。大规模海量数据场景适用。
- 标量量化(SQ8):把 float 向量压成 int8,内存减 4 倍,召回损失很小,是「免费午餐」。
# Qdrant 建 HNSW + 量化索引的示例
from qdrant_client import QdrantClient, models
client.create_collection(
"documents",
vectors_config={"size": 1024, "distance": models.Distance.DOT},
quantization_config=models.ScalarQuantization(
scalar=models.ScalarQuantizationConfig(
type=models.ScalarType.INT8, always_ram=True
)
),
hnsw_config=models.HnswConfigDiff(m=32, ef_construct=200),
)
召回与精度的平衡点要靠离线评测标定:建一个带 ground truth 的评测集(每个 query 标注正确答案),对比不同索引参数下的 Recall@K 与延迟,而不是拍脑袋选参数。
维度、长度与多模态对齐
工程上几个容易踩的坑:
- 向量维度:1024 维以上内存爆炸(1000 万 × 1024 × 4B ≈ 40GB)。bge-m3 的 1024 维可用,但超大语料建议用 256~768 维的轻量模型,或做 PCA 降维。
- 输入长度:长文档直接编码会丢信息。标准做法是切块(Chunking)后对块编码,检索到块再映射回原文,兼顾召回与上下文完整性。
- 多模态对齐:CLIP 类模型把图文映射到同一空间,支持「文本搜图、图搜图、图文联合检索」。对齐训练靠大规模图文对(image-text pairs),负样本同样用 in-batch + 难负样本。
# 长文档切块的简化示意
# 128 token 一句的滑动窗口 + 重叠 → 块 Embedding → 检索块 → 用块定位原文段落
# 块太大语义混叠、块太小上下文断裂;512~1024 token 是常见折中
生产调优清单
把 Embedding 调好,按下面清单逐项核对:
- 归一化:编码后一律 normalize,检索统一用点积。
- 温度与批次:训练 τ 用 0.05 起步,batch 尽量大(≥64)以扩充 in-batch 负样本。
- 难负样本:每轮加 hard negatives,两三轮挖掘后召回与 NDCG 提升最明显。
- 检索参数:HNSW 的
ef_search调高召回、m调高图质量(均牺牲内存)。 - 评测闭环:建 ground truth 集,用 Recall@K / MRR / NDCG 回归,任何索引或模型改动都要过评测。
- 混合检索兜底:纯向量召回不到精确关键词,配 BM25 或稀疏向量混合,再 Reranker 精排——RAG 生产标配。
总结
Embedding 的工程本质是**「空间拓扑 = 语义拓扑」的闭环**:对比学习定范式,双塔架构定形态,难负样本与蒸馏定质量,归一化与索引定检索效率,评测定迭代方向。落地顺序建议:先按「归一化 + 点积 + HNSW」把基线跑起来,再挖难负样本迭代精度,最后按评测指标调索引参数。Embedding 是检索系统的地基,值得花时间打磨。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。