Embedding 技术详解
代码阅读约定
文中的数学式与训练循环标为示意;get_embedding 依赖 API 配置,索引、语言检测、缓存与异步适配器需由应用实现。模型下载和外部服务示例未在本文的编辑环境运行,不能视为兼容性测试。
深入理解文本向量化的原理与实践,选择合适的 Embedding 模型
5 分钟核心结论
Embedding 决定“哪些内容有机会成为证据”,不能单凭向量维数或通用榜单决定效果。先固定语料快照、切分、权限和候选 K,用真实问题检验 Recall@K,再比较时延、索引体积和重建成本。
- 查询与文档必须使用兼容的编码器、提示前缀、归一化和版本;同维度不代表同一向量空间。
- 专名、编号和代码符号需要保留关键词基线;混合召回后统一 ID、去重,再重排。
- 单向量适合廉价初筛;late interaction 保留多个 token/patch 表示,用更高索引和打分成本换取细节。视觉页检索仍需独立的回答与引用步骤。
- 上下文增强发生在建索引时:为每个片段补充其在原文中的位置与指代,不是替所有片段复制同一篇摘要。
按任务阅读
| 当前任务 | 先读入口 | 应得到的产物 |
|---|---|---|
| 为新项目选检索表示 | 架构与代价 → 验收 | 固定问题集上的质量、延迟与空间对照 |
| 排查换模型后召回骤降 | 空间失配 | 编码配置清单与重建/回滚方案 |
| 处理代词、表格与扫描页 | 上下文与多模态 | 文本、视觉、混合三条基线 |
| 理解训练为什么有效 | 下文理论、演进与训练折叠章节 | 正负样本与损失的对应关系 |
2026-10-08 核验范围:对照 Contextual Retrieval 官方说明、ColPali 论文与现有检索流程;保留的模型/API 示例未进行下载或外部调用,具体版本需在项目中锁定。
🎯 核心概念
什么是 Embedding?
Embedding(嵌入) 是将离散的文本符号映射为连续的稠密向量表示(Dense Vector Representation) 的技术,是现代 NLP 和 RAG 系统的基础。其核心目标是将人类可读文本转换为机器可处理的数值格式,同时保留并编码语义信息。
# 文本 → 向量的映射过程
"今天天气真好" → [0.12, -0.34, 0.56, ..., 0.78] # 1536维向量
"天气不错" → [0.11, -0.32, 0.58, ..., 0.76] # 语义相似,向量接近
"我喜欢编程" → [-0.45, 0.67, -0.23, ..., 0.12] # 语义不同,向量距离远2
3
4
Embedding的本质特征:
- 语义空间(Semantic Space):向量间距离反映语义相似度(如"国王"与"女王"向量近,与"汽车"向量远)
- 向量运算的语义性:通过向量运算可揭示抽象关系,如经典案例:
vector("国王") - vector("男人") + vector("女人") ≈ vector("女王") - 范式转变:标志着NLP从"人工规则/符号系统"向"数据驱动的统计学习"转变——意义不再是预设实体,而是数据统计结构的涌现属性
为什么需要 Embedding?
核心价值
语义理解:将人类语言转换为机器可理解的数学表示
相似度计算:通过向量距离衡量文本语义相似性
高效检索:在高维向量空间中进行快速相似性搜索
泛化能力:对罕见词可通过上下文推断其Embedding(如"轿车"与"汽车"语义近,向量表示也近)
🧮 Embedding 的理论基础
深入:稀疏/稠密表示、分布式假说与几何解释
从稀疏表示到密集表示
在深入Embedding原理之前,需要理解文本表示技术的演进历程。
早期的稀疏表示
早期文本表示方法聚焦于"稀疏表示",通过"高维零向量"编码文本,虽实现了初步数字化,但存在根本性局限:
| 方法 | 原理 | 缺陷 |
|---|---|---|
| 独热编码(One-Hot) | 为词汇表中每个单词分配唯一索引,向量维度=词汇表大小,仅索引位置为1,其余为0 | 维度灾难、数据稀疏、语义鸿沟(任意两词向量正交) |
| 词袋模型(BOW) | 将文本视为无序单词集合,通过词频构建向量 | 忽略词序与上下文关系 |
| TF-IDF | 在BOW基础上引入"词在文档中的重要性"权重 | 仍无法捕捉深层语义 |
# 独热编码示例:假设词表大小为10000
"猫" → [0, 0, ..., 1, ..., 0] # 第1037位为1,其余为0
"狗" → [0, 0, ..., 1, ..., 0] # 第592位为1,其余为0
# 问题:余弦相似度为0,无法体现"猫"和"狗"的语义相似性2
3
4
密集表示的革命
Embedding将单词/文本片段映射到低维实数密集向量(通常100-4096维),实现"语义编码"的革命性突破:
- 维度降低:压缩高维稀疏向量,减少模型参数量与计算复杂度
- 语义编码:意义相近的文本,其Embedding向量在空间中更接近
- 核心目标:不是简单的"降维",而是**"降维同时编码语义关系"**
分布式假说(Distributional Hypothesis)
所有现代Embedding模型的核心理论基础,源于20世纪50年代语言学家J.R. Firth提出的分布式假说:
核心思想:"You shall know a word by the company it keeps"(观其伴而知其义)
- 基本主张:出现在相似上下文的单词,语义更相近
- 理论价值:将抽象"语义相似度"转化为可量化的"分布相似度"——无需直接定义"意义",只需分析单词在海量语料中的"共现模式"
- 学科桥梁:连接语言学(语义分析)与机器学习(统计建模),为Embedding技术提供理论依据
示例:
上下文1: "国王统治着王国"
上下文2: "女王统治着王国"
→ "国王"和"女王"常与"皇家""宫殿""权力"共现 → 语义关联紧密2
3
模型演进的底层逻辑:从早期Word2Vec到现代BERT,Embedding模型的目标始终未变——更精准地捕捉单词的上下文共现模式。模型演进的本质是"优化上下文捕捉方法"。
向量空间模型(Vector Space Models)
分布式假说提供理论基础,向量空间模型则提供数学框架,将语义关系转化为"几何空间中的位置关系":
语义的几何类比
- 核心思想:将每个单词/文本表示为高维向量空间中的一个"点",向量空间即"意义地图"
- 空间关系与语义关联:语义相近的文本(如"医生"与"医院")在空间中距离更近
相似度度量方法
| 度量方法 | 公式 | 特点 |
|---|---|---|
| 余弦相似度 | cos(θ) = (A·B)/(‖A‖‖B‖) | 值域[-1,1],对向量长度不敏感,聚焦"语义方向" |
| 欧几里得距离 | d = √Σ(aᵢ-bᵢ)² | 衡量向量间的直线距离 |
| 点积(内积) | A·B = Σaᵢbᵢ | 保留模长信息;是否归一化取决于训练目标 |
向量运算即语义关系
向量空间模型的强大之处在于,可通过线性代数运算揭示语义规律:
# 经典类比推理案例
vector("国王") - vector("男人") + vector("女人") ≈ vector("女王")
# 这是特定词向量上的近似类比,不是精确语义代数,也不保证迁移到句子向量。2
3
📈 Embedding 技术演进
深入:从词向量到专用检索编码器
第一代:静态词向量
静态词向量的核心是通过"局部上下文共现"学习词向量,代表方法为 Word2Vec(预测式)和 GloVe(统计式)。
Word2Vec(2013年,Google)
Word2Vec 并非单一模型,而是包含 CBOW 和 Skip-gram 两种架构的框架。核心创新:将Embedding学习转化为"基于局部上下文的自监督预测任务"。
连续词袋模型(CBOW)
- 任务目标:根据上下文词预测中心词
- 示例:句子"the man loves his son",以{"the", "man", "his", "son"}为输入,预测中心词"loves"
- 实现逻辑:将上下文词向量"平均/求和",用聚合向量预测目标词
- 优势:训练速度快,对高频词表示效果好
- 劣势:对低频词、罕见词的语义捕捉能力较弱
跳元模型(Skip-gram)
- 任务目标:根据中心词预测上下文词
- 示例:以中心词"loves"为输入,预测上下文词
- 实现逻辑:为每个"中心词-上下文词"配对独立学习
- 优势:对低频词、罕见词的表示更精细
- 劣势:计算复杂度高于CBOW,训练速度较慢
| 架构 | 预测方向 | 计算复杂度 | 适用场景 |
|---|---|---|---|
| CBOW | 上下文 → 中心词 | 较低 | 高频词、大语料快速训练 |
| Skip-gram | 中心词 → 上下文 | 较高 | 低频词、小语料精细训练 |
GloVe(2014年,Stanford)
GloVe(Global Vectors)核心是融合"全局统计"与"局部预测"的优势:
- 全局共现矩阵:先扫描整个语料库,构建"词-词共现矩阵X",其中X_ij表示"单词j出现在单词i上下文的总次数"
- 训练目标:学习词向量,使任意两个词向量的点积"逼近它们共现概率的对数"
- 关键洞察:共现概率的比率(而非概率本身)更能编码语义
# GloVe的语义推理能力示例
P(solid|ice) / P(solid|steam) # 冰与固体的关联性远高于蒸汽 → 比值大
P(gas|ice) / P(gas|steam) # 蒸汽与气体的关联性远高于冰 → 比值小
# 该比率能过滤噪声,凸显"物态"等核心语义维度2
3
4
静态词向量的根本局限:为每个单词生成唯一固定向量,本质是"该词在所有语境中的语义平均",无法处理一词多义(如"bank"在"河岸"和"银行"中的不同含义)。
第二代:动态上下文向量
基于Transformer的Embedding模型(如BERT)通过"动态上下文感知",开启了语境革命。
自注意力机制(Self-Attention)
解决多义词问题的核心突破,为每个词动态计算"对其他词的关注度",生成与上下文相关的向量:
查询(Q)、键(K)、值(V)机制
向量生成:对每个词的原始嵌入,通过线性变换生成三个功能向量:
- 查询向量(Q):代表当前词,相当于"我需要关注哪些词来理解自己?"
- 键向量(K):代表所有词的"身份标签",用于与Q匹配
- 值向量(V):代表所有词的"语义内容",是最终用于整合的信息
注意力分数计算:计算当前词Q与所有词K的点积,得到"注意力分数"
权重归一化:用Softmax函数转换为"注意力权重"(总和为1)
加权求和:用权重对所有词的V向量加权求和,得到当前词的"上下文感知嵌入"
多头注意力(Multi-Head Attention)
为捕捉多维度语义关系(如句法、语义、搭配),并行执行多个独立的自注意力计算,每个"头"学习关注不同语义子空间,拼接后得到"多维度融合的上下文表示"。
BERT(2018年,Google)
基于Transformer的上下文Embedding里程碑模型,核心是"深度双向性"与"自监督预训练"。
深度双向性
- 突破点:以往模型(如ELMo)仅为"左右单向模型拼接";BERT可一次性处理整个序列,无差别融合左右两侧上下文
- 能力体现:处理"bank"时,会同时参考左侧"river"(河岸语境)或"deposited money"(银行语境),动态生成对应含义的向量
预训练任务
- 掩码语言模型(MLM):随机用
[MASK]遮盖部分词,训练模型预测被遮盖的原始词,迫使模型依赖上下文推断语义 - 下一句预测(NSP):给定句子A和B,判断B是否为A的原文下一句,学习句子间逻辑关系
# BERT的上下文感知能力示例
"银行卡在银行办理"
# 第一个"银行"根据"卡"推断为"银行卡"
# 第二个"银行"根据"办理"推断为"金融机构"
# 两个"银行"有不同的向量表示!2
3
4
5
上下文表示使同一词元随语境变化,但表示能力不等同于可靠推理。未经句向量训练的 BERT 输出或任意池化,不应直接视为优秀检索嵌入。
第三代:专用 Embedding 模型
针对检索任务优化的专门模型,通过对比学习等技术专门优化语义检索能力:
| 模型系列 | 特点 | 适用场景 |
|---|---|---|
| sentence-transformers | 专门用于句子级向量化,基于孪生网络架构 | 通用语义相似度计算 |
| BGE系列 | 中文优化的双编码器模型,智源发布 | 中文RAG系统 |
| E5系列 | 面向检索训练,区分英文与多语言型号 | 按模型卡选择语言与输入前缀 |
| OpenAI text-embedding-3 | 多语言、高维度、商业API | 高精度商业应用 |
| GTE系列 | 阿里达摩院出品,通用文本嵌入 | 多任务文本表示 |
� Tokenizer 与 Embedding 的关系
Tokenizer 是 NLP 流水线的第一道关卡,核心任务是将原始文本字符串转换为模型能处理的数值输入序列,是连接人类语言与机器世界的桥梁。
分词策略演进
| 策略 | 原理 | 优势 | 劣势 |
|---|---|---|---|
| 词级别分词 | 基于空格或标点切分单词 | 直观简单 | 词汇表爆炸、无法处理OOV |
| 字符级别分词 | 切分为单个字符 | 解决OOV问题 | 序列极长、难学长距离依赖 |
| 子词级别分词 | 在词和字符级别间平衡 | 控制词汇表、解决OOV、提升泛化 | 需要训练分词器 |
主流子词算法
Byte-Pair Encoding (BPE)
- 原理:从字符级别开始,迭代合并最频繁出现的字符对
- 特点:GPT系列模型使用,基于频率的贪婪合并
- 应用:GPT-2、GPT-3、GPT-4
WordPiece
- 原理:选择能最大化语言模型似然的合并
- 特点:BERT系列模型使用,基于似然的最优合并
- 应用:BERT、DistilBERT
特殊Token的作用
| Token | 功能 | 重要性 |
|---|---|---|
[CLS] | 序列开头,输出向量用作整个序列聚合表示 | 分类任务的关键 |
[SEP] | 分隔不同文本片段 | 问答、句子对任务 |
[PAD] | 填充不同长度序列到相同长度 | 批处理必需 |
[UNK] | 代表未知token | OOV处理 |
[MASK] | MLM预训练中的遮盖标记 | BERT预训练 |
关键警示
编码契约一致性:每个编码器必须搭配其指定 tokenizer、模板和池化配置;查询与文档两端需属于兼容的检索模型。不能只因两端 tokenizer 相同就认定向量兼容。
🏗️ 检索架构详解:Bi-Encoder 与 Cross-Encoder
现代信息检索的核心是"评估查询与文档的语义相关性",衍生出 Bi-Encoder(双编码器) 与 Cross-Encoder(交叉编码器) 两种主流架构。
Bi-Encoder(双编码器)架构
为大规模、高效率的初步检索设计,核心是"独立编码查询与文档,离线存储文档Embedding,在线快速匹配"。
工作机制
┌─────────────┐ ┌─────────────┐
│ Query │ │ Document │
└──────┬──────┘ └──────┬──────┘
│ │
▼ ▼
┌─────────────┐ ┌─────────────┐
│ Encoder A │ │ Encoder B │ (通常共享权重)
└──────┬──────┘ └──────┬──────┘
│ │
▼ ▼
[Query Vec] [Doc Vec]
│ │
└────────┬─────────┘
│
Similarity(cos/dot)
│
▼
Score2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- 离线索引:文档库中所有文档逐一通过"文档编码器"生成Embedding
- 向量存储:文档Embedding存入向量数据库(优化近似最近邻搜索)
- 在线查询:用户查询通过"查询编码器"生成Embedding
- 相似性搜索:向量数据库执行ANN搜索,快速返回最相似的文档
核心特点
- 优势:效率与可扩展性——文档Embedding离线预计算,实时搜索仅需"查询编码+ANN搜索",毫秒级完成海量数据检索
- 劣势:精度有限——编码阶段无直接交互,难捕捉查询和文档间细粒度词元级依赖
- 在RAG中的角色:检索器(Retriever),第一阶段快速大规模候选集召回
Cross-Encoder(交叉编码器)架构
为更细粒度的相关性判断设计,核心是"拼接查询与文档,同时编码以捕捉细粒度交互"。
工作机制
┌─────────────────────────────┐
│ [CLS] Query [SEP] Document │
└──────────────┬──────────────┘
│
▼
┌─────────────────────────────┐
│ Transformer Model │
│ (Full Cross-Attention) │
└──────────────┬──────────────┘
│
▼
Relevance Score2
3
4
5
6
7
8
9
10
11
12
- 输入:将查询与候选文档拼接(如用
[SEP]分隔),送入Transformer模型 - 交互:模型通过自注意力机制,深度交互查询与文档的所有词元
- 输出:生成单一分数,直接表示"文档与查询的相关性程度"
核心特点
- 优势:高精度——直接建模查询与文档的交互,在"细粒度相关性判断"上可优于 Bi-Encoder,但收益需在目标域验证
- 劣势:可扩展性极差——每个(查询,文档)对需完整模型推理,计算复杂度O(N)
- 在RAG中的角色:重排器(Re-ranker),对Bi-Encoder检索出的候选集进行高精度重排序
"检索与重排"两阶段流水线
Bi-Encoder与Cross-Encoder并非竞争关系,而是互补的两阶段架构:
展开完整流程与推演
┌─────────────────────────────────────────────────────────┐
│ 海量文档库 (百万级) │
└────────────────────────┬────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ 阶段1: Bi-Encoder 检索 │
│ - 快速召回 Top-100 候选文档 │
│ - 评估召回率 (Recall) │
│ - 毫秒级响应 │
└────────────────────────┬────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ 阶段2: Cross-Encoder 重排 │
│ - 对 Top-100 候选精排 │
│ - 提升精确率 (Precision) │
│ - 返回 Top-5/10 最终结果 │
└────────────────────────┬────────────────────────────────┘
│
▼
最终检索结果2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| 对比维度 | Bi-Encoder | Cross-Encoder |
|---|---|---|
| 速度 | 极快(毫秒级) | 较慢(秒级) |
| 精度 | 中等 | 极高 |
| 可扩展性 | 极好(支持亿级) | 差(仅支持候选集) |
| 交互方式 | 独立编码后相似度计算 | 联合编码深度交互 |
| 适用阶段 | 召回(Retrieval) | 重排(Re-ranking) |
先进架构:ColBERT 的延迟交互机制
ColBERT 是突破"Bi-Encoder/Cross-Encoder范式"的先进架构,代表检索模型的演进方向。
核心思想
挑战"将文档压缩为单一向量"的范式,为每个文档存储词元Embedding矩阵——保留文档内部细粒度语义信息。
延迟交互(Late Interaction)机制
Query: "机器学习"
↓
["机", "器", "学", "习"] → [q1, q2, q3, q4] (查询词元向量)
Document: "深度学习是机器学习的子领域"
↓
["深", "度", "学", "习", ...] → [d1, d2, d3, d4, ...] (文档词元向量矩阵)
MaxSim计算:
q1 与所有 d_i 计算相似度,取最大值 → max_sim_1
q2 与所有 d_i 计算相似度,取最大值 → max_sim_2
...
最终得分 = sum(max_sim_1, max_sim_2, max_sim_3, max_sim_4)2
3
4
5
6
7
8
9
10
11
12
13
14
- 独立编码:与Bi-Encoder类似,独立计算查询、文档的词元Embedding(文档Embedding可离线预存)
- 细粒度交互式评分:对"查询词元Embedding"与"候选文档所有词元Embedding"计算相似度,取每个查询词元的最大相似度(MaxSim)
- 聚合得分:将查询所有词元的MaxSim相加,得到最终相关性分数
ColBERT的权衡
| 维度 | 表现 |
|---|---|
| 精度 | 逐词元细粒度匹配,远超标准Bi-Encoder |
| 速度 | 避免Cross-Encoder的完整计算,远快于Cross-Encoder |
| 存储 | 需存词元Embedding矩阵,成本较高 |
| 索引 | 向量数据库索引复杂 |
� 主流 Embedding 模型对比
以下是模型家族举例,不是实时榜单;上线前核对模型卡、授权、输入上限和 API 定价,并保存具体 revision。维度更大不自动等于检索更准确。
商业模型
| 模型 | 维度 | 最大Token | 中文支持 | 成本 | 特点 |
|---|---|---|---|---|---|
| text-embedding-3-large | 3072 | 8191 | ✅ | 高 | 候选模型,需在业务语料评估 |
| text-embedding-3-small | 1536 | 8191 | ✅ | 低 | 比较质量、延迟和成本 |
| text-embedding-ada-002 | 1536 | 8191 | ✅ | 中 | 成熟稳定,广泛使用 |
| Cohere embed-v3 | 1024 | 512 | ✅ | 中 | 多语言优化,压缩表示 |
开源模型
| 模型 | 维度 | 优势 | 适用场景 |
|---|---|---|---|
| bge-large-zh-v1.5 | 1024 | 中文优化、开源免费 | 中文RAG系统 |
| bge-m3 | 1024 | 多语言、稠密+稀疏 | 跨语言检索 |
| m3e-base | 768 | 轻量、快速 | 资源受限环境 |
| text2vec-large-chinese | 1024 | 中文特化 | 中文语义搜索 |
| gte-large-zh | 1024 | 阿里达摩院、多任务优化 | 通用中文嵌入 |
| e5-large-v2 | 1024 | 需使用 query/passage 前缀 | 英文检索;多语言需选 multilingual-e5 系列 |
维度的动态权衡:套娃表示学习
Embedding维度是"表示能力"与"计算成本"的权衡:
- 低维(如128维):存储/计算成本低,但可能丢失细粒度语义
- 高维(如4096维):语义容量大,但存储、检索开销高
套娃表示学习(Matryoshka Representation Learning) 为动态权衡提供方案:训练时让Embedding可截断为不同维度(如推理时根据场景选"低维保性能"或"高维保精度"),使维度从"前置设计"变为"运行时决策"。
选择建议
下列仅是候选模型的历史示例,不是效果排名。先在目标语言、文档长度、成本和部署限制下建立评估,再决定型号;表中的模型维度与长度需以实际模型卡/API 配置为准。
模型选择指南
商业候选:OpenAI text-embedding-3-large 较小维度候选:OpenAI text-embedding-3-small 纯中文场景:bge-large-zh-v1.5
资源受限:m3e-base
多语言需求:bge-m3
🎛️ 池化策略(Pooling Strategies)
Transformer模型输出是与输入token一一对应的上下文相关向量,但下游任务(如文本分类、语义搜索)需单一固定长度向量代表整个输入序列,池化(Pooling) 层解决"多对一"表示转换问题。
为何需要定长向量
将可变长度token序列转换为定长向量是高效向量检索系统的根本要求:
| 需求 | 说明 |
|---|---|
| 向量空间一致性 | 向量数据库和相似性搜索算法在固定维度向量空间运作 |
| ANN算法要求 | HNSW等算法依赖任意两定长向量间的距离计算 |
| 计算效率 | 定长向量支持GPU批量矩阵运算,简化内存分配 |
| 存储效率 | 简化磁盘布局和数据检索 |
常见池化策略
池化是已训练模型契约的一部分,应读取模型配置,不能部署时随意替换。下表解释机制,不表示某种池化对所有模型都更好;padding 必须掩码处理,多向量检索还可以保留多个 token 表示。
| 策略 | 原理 | 优势 | 适用场景 |
|---|---|---|---|
| [CLS] Token池化 | 使用序列开头[CLS] token的嵌入作为整体表示 | 专为聚合全局语义设计 | 分类任务微调后的模型 |
| 平均池化(Mean Pooling) | 计算所有token嵌入的逐维度平均值 | 综合所有信息,简单高效 | 通用语义理解 |
| 最大池化(Max Pooling) | 取每个维度的最大值 | 捕捉最显著特征 | 特征突出的任务 |
| 加权平均池化 | 根据注意力权重加权平均 | 考虑token重要性差异 | 精细语义任务 |
import torch
def mean_pooling(model_output, attention_mask):
"""平均池化实现"""
token_embeddings = model_output[0] # [batch, seq_len, hidden_dim]
# 扩展attention_mask以匹配嵌入维度
input_mask_expanded = attention_mask.unsqueeze(-1).expand(token_embeddings.size()).float()
# 求和并除以有效token数量
sum_embeddings = torch.sum(token_embeddings * input_mask_expanded, dim=1)
sum_mask = torch.clamp(input_mask_expanded.sum(dim=1), min=1e-9)
return sum_embeddings / sum_mask
def cls_pooling(model_output):
"""[CLS] Token池化实现"""
return model_output[0][:, 0, :] # 取第一个token的嵌入2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
池化策略选择
池化策略需与模型训练目标和应用任务匹配:
- 平均池化:假设所有词义平均值代表句意,适用于一般语义理解
- [CLS]池化:假设模型已训练将句子聚合意义集中到该token,适用于分类任务微调后的模型
🎓 Embedding 模型训练范式
深入:孪生网络、训练损失与难负例
孪生网络结构
为训练"用于相似性比较的Embedding",常采用孪生网络(Siamese Network):两个共享权重的编码器,依次处理"数据对/三元组"中的文本,确保所有文本映射到同一语义空间。
文本A 文本B
│ │
▼ ▼
┌─────────────┐ ┌─────────────┐
│ Encoder │ │ Encoder │ ← 共享权重
└──────┬──────┘ └──────┬──────┘
│ │
▼ ▼
Embedding_A Embedding_B
│ │
└────────┬─────────┘
│
相似度计算
│
▼
Loss 计算2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
训练数据格式
模型学习依赖"相似性信号的结构化数据",主流格式:
| 格式 | 结构 | 适用损失函数 |
|---|---|---|
| 带标签的句对 | (句子1, 句子2, 相似度分数) | CosineSimilarityLoss |
| 正例句对 | (句子1, 句子2) 隐含相似 | MultipleNegativesRankingLoss |
| 三元组 | (锚点, 正例, 负例) | TripletLoss |
核心损失函数
对比损失(Contrastive Loss)
- 作用于:句对
- 目标:拉近正例对(相似句子)的Embedding,推开负例对(不相似句子),并惩罚负例距离小于 margin 的情况;优化目标不是对每个样本的硬保证
# 对比损失伪代码
def contrastive_loss(embedding1, embedding2, label, margin=1.0):
distance = euclidean_distance(embedding1, embedding2)
if label == 1: # 正例对
return distance ** 2
else: # 负例对
return max(0, margin - distance) ** 22
3
4
5
6
7
三元组损失(Triplet Loss)
- 作用于:三元组 (Anchor, Positive, Negative)
- 目标:对未满足“正例距离 + margin ≤ 负例距离”的三元组施加损失
- 公式:
Loss = max(0, D(A,P) - D(A,N) + margin)
# 三元组损失伪代码
def triplet_loss(anchor, positive, negative, margin=0.3):
pos_dist = euclidean_distance(anchor, positive)
neg_dist = euclidean_distance(anchor, negative)
return max(0, pos_dist - neg_dist + margin)2
3
4
5
MultipleNegativesRankingLoss
- 特点:利用batch内其他样本作为负例,高效利用训练数据
- 优势:无需显式构造负例,自动从batch中采样
难负例挖掘(Hard Negative Mining)
Embedding模型质量依赖"负例的挑战性"——若负例"过于简单"(与锚点语义完全无关),模型无法学习细粒度语义差异。
挖掘流程
- 初步检索:用基线Bi-Encoder为每个查询召回Top-k候选
- 候选核验:用 Cross-Encoder 辅助打分,再检查错误负例;教师分数不是真值
- 识别难负例:筛选"被Bi-Encoder高评分召回,但被Cross-Encoder低评分判定为不相关"的文档
- 构建训练三元组:用难负例构建
(查询, 正例, 难负例)格式的训练数据
# 难负例挖掘伪代码
def hard_negative_mining(queries, documents, bi_encoder, cross_encoder):
hard_negatives = []
for query in queries:
# 1. Bi-Encoder召回Top-100
candidates = bi_encoder.retrieve(query, top_k=100)
# 2. Cross-Encoder重排
reranked = cross_encoder.rerank(query, candidates)
# 3. 找出"高召回分但低重排分"的文档
for doc in candidates:
if bi_encoder_score(doc) > threshold_high and \
cross_encoder_score(doc) < threshold_low:
hard_negatives.append((query, doc))
return hard_negatives2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
知识蒸馏视角
难负例挖掘与知识蒸馏不是同义词:前者选择有区分难度的负例,后者让学生拟合教师信号。教师可辅助挖掘,但需要防止把未标注的真相关文本当负例;批内负例也需检查重复与多正例。
💻 实战代码示例
OpenAI Embedding
展开长代码示例(接口与运行边界见正文)
from openai import OpenAI
import numpy as np
client = OpenAI() # 从 OPENAI_API_KEY 环境变量读取密钥
def get_embedding(text, model="text-embedding-3-small"):
"""获取文本的embedding向量"""
response = client.embeddings.create(
model=model,
input=text
)
return response.data[0].embedding
# 使用示例
text1 = "什么是检索增强生成?"
text2 = "RAG技术的工作原理"
embedding1 = get_embedding(text1)
embedding2 = get_embedding(text2)
# 显式计算余弦,避免将未经验证的向量范数当作 1
similarity = np.dot(embedding1, embedding2) / (
np.linalg.norm(embedding1) * np.linalg.norm(embedding2)
)
print(f"语义相似度: {similarity:.4f}")2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
开源模型使用
from sentence_transformers import SentenceTransformer
# 加载BGE中文模型
model = SentenceTransformer('BAAI/bge-large-zh-v1.5')
# 批量编码
texts = [
"检索增强生成技术原理",
"RAG系统架构设计",
"向量数据库选型"
]
embeddings = model.encode(texts)
print(f"向量维度: {embeddings.shape}")
# 计算相似度矩阵
from sklearn.metrics.pairwise import cosine_similarity
sim_matrix = cosine_similarity(embeddings)
print("相似度矩阵:", sim_matrix)2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
批量处理优化
必须保持文本 ID 与输出一一对应:失败批次不能跳过后继续拼接向量,否则后续按位置入库会错配。下例采用失败即停止;生产版应做有上限的退避重试、断点恢复,并按请求条数与 token 限额切批,固定 sleep 不能保证不被限流。
展开长代码示例(接口与运行边界见正文)
import numpy as np
from typing import List
import time
class EmbeddingProcessor:
def __init__(self, model_name="text-embedding-3-small"):
self.model_name = model_name
self.client = OpenAI()
def batch_embed(self, texts: List[str], batch_size: int = 100):
"""批量处理embedding,提高效率"""
embeddings = []
for i in range(0, len(texts), batch_size):
batch = texts[i:i + batch_size]
try:
response = self.client.embeddings.create(
model=self.model_name,
input=batch
)
batch_embeddings = [item.embedding for item in sorted(response.data, key=lambda item: item.index)]
embeddings.extend(batch_embeddings)
# 避免API限流
time.sleep(0.1)
except Exception as e:
raise RuntimeError(f"批次 {i//batch_size + 1} 失败,停止以避免文本与向量错位") from e
return embeddings
# 使用示例
processor = EmbeddingProcessor()
large_text_list = ["文本1", "文本2", "文本3"]
embeddings = processor.batch_embed(large_text_list)2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
🎯 RAG 中的 Embedding 应用
Embedding作为"语义桥梁",连接用户查询与外部知识库,解决LLM"知识过时""易幻觉"的核心痛点,是RAG系统的技术基石。
语义搜索 vs 关键词搜索
| 对比维度 | 关键词搜索 | 语义搜索(Embedding驱动) |
|---|---|---|
| 匹配方式 | 精确字面匹配 | 语义相似度匹配 |
| 同义词处理 | 需手动配置同义词表 | 自动理解语义关联 |
| 查询理解 | 字面理解 | 意图理解 |
| 示例 | "电脑蓝屏"只能匹配包含"电脑蓝屏"的文档 | "电脑蓝屏"可匹配"Windows系统崩溃解决方案" |
RAG完整工作流程
┌─────────────────────────────────────────────────────────────────┐
│ 阶段一:索引(离线过程) │
├─────────────────────────────────────────────────────────────────┤
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │
│ │ 文档加载 │ → │ 文档分块 │ → │ 向量化 │ → │ 向量数据库存储│ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ 阶段二:检索与生成(在线过程) │
├─────────────────────────────────────────────────────────────────┤
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │
│ │ 用户查询 │ → │ 查询向量化│ → │ 相似性搜索│ → │ 上下文增强 │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────────┘ │
│ ↓ │
│ ┌──────────────┐ │
│ │ LLM生成回答 │ │
│ └──────────────┘ │
└─────────────────────────────────────────────────────────────────┘2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
阶段一:索引(建立知识库)
目标:将非结构化文档转换为"可快速语义检索"的结构化向量库
文档加载与分块
- 加载原始文档(PDF、HTML、Word等),清洗格式噪声
- 按"语义完整性"原则分割为文本块(chunks)
- 分块大小需平衡"上下文完整性"与"检索精准度"(通常256-1024 token)
向量化
- 将每个文本块输入Embedding模型,生成高维密集向量
存入向量数据库
- 将"向量-原始文本块"对存储到专用向量数据库(如Pinecone、Milvus、FAISS)
- 通过ANN算法优化,可在毫秒级从数百万向量中找到最相似结果
阶段二:检索与生成
- 查询嵌入:使用与索引阶段兼容的查询编码器及对应前缀将用户查询转换为向量
- 相似度搜索:精确搜索或 ANN 在文档索引中查找候选,返回Top-K
- 提示词增强:将检索到的文本块与用户查询按模板整合为"增强提示词"
- 生成回答:LLM基于上下文生成候选答案,再检查证据支持、引用和无答案处理
文档索引流程
接口示意:chunk_document 由文档切分实现;生产元数据使用稳定文档 ID、版本与区间,不能拿全文充当来源 ID。
展开长代码示例(接口与运行边界见正文)
def build_document_index(documents: List[str]):
"""构建文档向量索引"""
embeddings = []
for doc in documents:
# 1. 文档切分(见chunking章节)
chunks = chunk_document(doc)
# 2. 向量化
doc_embeddings = []
for chunk in chunks:
embedding = get_embedding(chunk)
doc_embeddings.append({
'text': chunk,
'vector': embedding,
'metadata': {'source': doc, 'chunk_id': len(embeddings) + len(doc_embeddings)}
})
embeddings.extend(doc_embeddings)
return embeddings2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
检索匹配
def semantic_search(query: str, index: List[dict], top_k: int = 5):
"""语义搜索"""
query_embedding = get_embedding(query)
# 计算相似度
similarities = []
for item in index:
similarity = float(cosine_similarity([query_embedding], [item['vector']])[0, 0])
similarities.append((similarity, item))
# 排序返回top-k
similarities.sort(key=lambda x: x[0], reverse=True)
return [item[1] for item in similarities[:top_k]]2
3
4
5
6
7
8
9
10
11
12
13
🚨 向量空间失配问题深度解析
RAG系统中,索引阶段和检索阶段使用不一致的Embedding模型会导致严重的"向量空间失配"问题,本节从数学和工程角度深入剖析其机制。
同构空间假设及其崩塌
需要满足的是查询编码器与文档编码器的检索兼容契约,并非“同构空间公理”。共享权重是一种实现,独立训练后联合对齐的双编码器也是实现。不同供应商的两个 1024 维模型不能直接互查;同一模型错误的前缀、池化或归一化也会造成退化。DPR 原论文 可帮助区分“双编码器”与“权重完全相同”。
负相似度的数学本质
余弦相似度的几何意义
cos(q, d) = q·d / (||q|| ||d||),非零向量的取值范围是 [-1, 1]。正负号表示夹角,与自然语言中的反义、否定没有一一对应关系。合法模型的无关句对也可以得到负分,相关内容也不一定超过某个固定阈值。
高维空间的随机正交性
独立均匀单位向量在高维中趋于近正交,是带分布假设的概率现象;真实模型表示不一定均匀或独立。不能由此推导模型失配会让约 50% 文档得负分,也不能用 Johnson–Lindenstrauss 引理证明这种诊断。失配应靠编码配置和有标签检索回归确认。
架构层的蝴蝶效应:分词器失配
词汇表ID的语义错乱
将 A 模型 tokenizer 的 ID 直接输入 B 模型权重,会违反输入契约;A 与 B 各用自己的 tokenizer 则不会发生这种 ID 混用,但输出空间仍未必兼容。相同词对应不同 ID 本身是正常现象,不证明输出方向相反。
拓扑学视角:表示退化与锥形效应
表示退化(Representation Degeneration)
此处讨论的是向量几何分布,而不是拓扑学保证。若无关文本也普遍高分,可检查均值方向、范数、重复输入、池化和训练任务。不能把某个模型上的“锥形”观察推广为所有嵌入模型的固定性质。
异构锥体的几何互斥
两个模型的输出没有已知兼容关系,但也不能据此断言中心轴相互排斥或所有点积为负。用相同正负样本做受控对照,比解释分数正负更可靠。
训练目标函数的差异
MLM 预训练提供上下文 token 表示;检索模型还需要匹配目标、池化与监督信号。对比学习提升正例相对负例的得分,不要求每个负例指向相反方向,也不保证训练域外的召回质量。
工程实践中的度量陷阱
| 陷阱 | 如何识别 | 修复 |
|---|---|---|
| 内积当余弦 | 范数变化时排序改变,分数超出 [-1,1] | 按模型训练目标选择度量;单位归一化后内积才等于余弦 |
| 任意截断/补零 | 维数相同但召回明显下降 | 仅使用模型明确支持的降维方式,并重建一致索引 |
| 相似度当距离 | 返回值方向与排序相反 | 对已知相同/无关文本验证数据库的分数定义 |
| 查询前缀遗漏 | query→document 召回差,而文本自检索正常 | 使用模型规定的非对称输入模板 |
度量选择参见 Sentence Transformers 相似度文档。
解决方案:系统一致性重构
1. 严格的版本控制
# 应用维护的索引清单示意;revision 应写真实模型版本或权重摘要。
index_manifest = {
"encoder_revision": "YOUR_PINNED_REVISION",
"query_template": "query: {text}",
"document_template": "passage: {text}",
"pooling": "mean",
"normalize": True,
"dimension": 768,
"metric": "cosine",
"corpus_snapshot": "corpus-v3",
}2
3
4
5
6
7
8
9
10
11
模板、池化和维数仅展示应记录的字段,不是可套用到所有模型的配置。
2. 重建索引(Re-indexing)
编码器或预处理变更时,以原始文本构建新索引,回填期间记录增量与删除事件,用新查询编码器回归后原子切换。旧索引留作回滚;不要把新旧模型向量混入同一检索空间。
3. 跨模型对齐(Procrustes Alignment)
Procrustes 需要两套空间中对应样本的成对向量,并假设线性/正交映射足够表达差异。只有旧向量无法估计这种映射;即使有成对样本,也须比较独立测试集召回与重编码基线,不能把它作为无损升级方案。
⚠️ 实践中的常见问题
问题1:模型不匹配
现象:检索效果差,相似文本匹配度低
原因:索引和查询使用了不同的embedding模型
解决:
# ❌ 错误做法
index_embeddings = get_embedding(texts, model="text-embedding-ada-002")
query_embedding = get_embedding(query, model="text-embedding-3-small")
# ✅ 正确做法
MODEL_NAME = "text-embedding-3-small"
index_embeddings = get_embedding(texts, model=MODEL_NAME)
query_embedding = get_embedding(query, model=MODEL_NAME)2
3
4
5
6
7
8
问题2:文本长度超限
以下截断例子仅演示 token 上限处理,会丢弃后文证据;RAG 入库应优先切分并保留区间映射。函数名 safe_embedding 不代表语义无损,截断需明确记录并复核解码后的长度。
现象:长文本被截断,信息丢失
解决方案:
def safe_embedding(text: str, model: str, max_tokens: int = 8191):
"""安全的embedding处理,避免超长截断"""
import tiktoken
encoding = tiktoken.encoding_for_model(model)
tokens = encoding.encode(text)
if len(tokens) <= max_tokens:
return get_embedding(text, model)
else:
# 截断处理
truncated_tokens = tokens[:max_tokens]
truncated_text = encoding.decode(truncated_tokens)
return get_embedding(truncated_text, model)2
3
4
5
6
7
8
9
10
11
12
13
14
问题3:中英文混合处理
现象:跨语言问题无法找回另一语言中的证据。
优先用同一兼容多语言检索空间对所有语料编码,再按“中文问中文、英文问英文、中文问英文、混合术语”分桶测试。不能对每条文本临时切换不同模型后混入同一个索引。如果确实分语言使用不同模型,就维护独立索引与对应查询编码器,通过排名融合合并结果。
E5-large-v2 模型卡 明确其语言和 query/passage 前缀要求;需要多语言时不能把英文版本与 multilingual 版本混称。训练与池化配置可参考 Sentence Transformers 训练文档。
📊 性能优化建议
1. 缓存机制
以下键仅示意模型名与文本的组合;生产还须加入权重 revision、模板、池化、归一化和维数,避免升级后复用旧向量。文本或摘要可能敏感,缓存访问与生命周期按原文权限治理。
展开长代码示例(接口与运行边界见正文)
import hashlib
import json
from functools import lru_cache
@lru_cache(maxsize=10000)
def cached_embedding(text: str, model: str):
"""带缓存的embedding计算"""
return get_embedding(text, model)
# 或使用Redis缓存
def redis_cached_embedding(text: str, model: str):
import redis
r = redis.Redis(host='localhost', port=6379, db=0)
# 生成缓存key
cache_key = f"emb:{model}:{hashlib.md5(text.encode()).hexdigest()}"
# 尝试从缓存获取
cached = r.get(cache_key)
if cached:
return json.loads(cached)
# 计算并缓存
embedding = get_embedding(text, model)
r.setex(cache_key, 86400, json.dumps(embedding)) # 24小时过期
return embedding2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
2. 异步处理
以下是调度接口示意,async_get_embedding 需实现真实调用、超时、重试与响应校验;当前 pass 不产生向量,不能直接用于入库。生产用有界并发和任务队列,避免一次创建全部任务。
import asyncio
import aiohttp
async def async_get_embedding(text: str, session: aiohttp.ClientSession):
"""异步embedding计算"""
# 实现异步API调用
pass
async def batch_async_embedding(texts: List[str]):
"""异步批量处理"""
async with aiohttp.ClientSession() as session:
tasks = [async_get_embedding(text, session) for text in texts]
return await asyncio.gather(*tasks)2
3
4
5
6
7
8
9
10
11
12
13
� 先进技术与未来趋势
RAG领域持续演进,以下技术成为突破传统流程局限的关键方向:
混合搜索(Hybrid Search)
向量与关键词检索可互补,但返回的候选列表不能直接做数值加权,也不能把 BM25 与余弦按默认权重相加。先统一文档 ID 与数据版本,再用 RRF 建立基线;需要分数融合时对缺失分数和校准作明确处理。完整算法见检索策略。
重排(Re-ranking)
对初步检索的Top-K文本块,用更强大的模型(如Cross-Encoder)二次排序:
from sentence_transformers import CrossEncoder
reranker = CrossEncoder('cross-encoder/ms-marco-MiniLM-L6-v2')
def rerank_results(query: str, candidates: List[str], top_k: int = 5):
"""使用Cross-Encoder重排"""
pairs = [[query, doc] for doc in candidates]
scores = reranker.predict(pairs)
ranked = sorted(zip(candidates, scores), key=lambda x: x[1], reverse=True)
return [doc for doc, _ in ranked[:top_k]]2
3
4
5
6
7
8
9
10
11
上下文检索(Contextual Retrieval)
Contextual Retrieval 官方说明(2024-09-19)为每个 chunk生成结合整篇文档的简短上下文,再把它加在 chunk 前,分别建立向量和 BM25 索引。它与“给每块都加相同的全文摘要”不同。收益数字来自该文的数据与配置,不能直接当成你的项目预期。
# 示意:chunk_context 应由原文生成并校验,不能编入原文没有的结论。
def contextual_embedding(chunk: str, chunk_context: str):
enhanced_text = f"片段背景:{chunk_context}\n\n原始片段:{chunk}"
return get_embedding(enhanced_text)2
3
4
工程上同时保存 raw_chunk、chunk_context、原始偏移及生成版本;生成的背景是检索辅助字段,引用仍回到原文。对“本季度”“该产品”等样本检查指代补全,同时统计背景引入错误的比例。更新原文时重建关联背景与两种索引,并对比无背景基线。
多模态RAG
多模态检索先选择“检索单元”,再选择模型;视觉问答能力不等于可用于近邻检索的嵌入能力。
| 证据形态 | 可比较的路线 | 需要保存的定位 |
|---|---|---|
| 产品图片/场景 | 共享图文空间检索 → 图像核验 | 原图 ID、裁剪区域 |
| 扫描页/图表 | OCR 文本检索、页面多向量检索、两者融合 | 文档版本、页码、证据区域 |
| 语音/视频 | 转写文本或帧/片段检索 → 原媒体核验 | 时间戳、采样帧、原媒体 ID |
ColPali 论文用页面图像的多向量表示与 late interaction 做页级检索,并用 ViDoRe 评测;它不直接证明最终问答正确。部署时比较页召回、索引体积和打分时延,再把选中页面或裁剪送入支持该输入的生成器。详见多模态 RAG。
系统协同优化
构建高效RAG系统需采取"整体观",任一环节短板都会削弱整体性能:
| 环节 | 潜在问题 | 优化方向 |
|---|---|---|
| 分块策略 | 分块过大包含冗余信息,过小丢失上下文 | 语义分块、递归分块 |
| Embedding模型 | 领域适配不足 | 领域微调、选择专用模型 |
| 向量数据库 | 检索延迟过高 | 优化索引参数、选择合适的ANN算法 |
| 提示词模板 | LLM无法有效利用上下文 | 迭代优化模板设计 |
验收:把模型选择变成可复现比较
固定切分、语料快照、过滤和候选 K,只改变编码器;记录领域 Recall@K、难负例排名、批处理吞吐、p95 查询延迟、索引字节数和重建时间。包含专名、否定、数字范围、跨语言与无答案样本。再用精确搜索对照 ANN,避免将索引误差当成模型误差。
模型迁移的通过条件应包括:关键问题无不可接受退化、文本/向量数量及维数一致、删除和权限事件已追平、旧版本可回滚。
�🔗 相关阅读
参考文章:
外部资源:
- MTEB排行榜 - Embedding模型性能对比
- Sentence-Transformers文档 - 开源Embedding框架
- OpenAI Embeddings指南