Skip to content

第9章 Embedding:文本如何变成可检索的向量

"An embedding is a lossy summary that preserves the axis of meaning you care about." — Information Retrieval 课堂总结

本章要点

  • Embedding 模型的核心任务:把文本压缩成低维稠密向量,让语义相近的文本在向量空间中距离相近
  • 三大训练范式:对比学习(SimCSE/BGE 主流)、蒸馏(MiniLM 小模型常用)、指令微调(E5-mistral 新路径)
  • 选型六维度:语言覆盖 / 领域 / 输入长度 / 向量维度 / 推理成本 / License
  • 2024-2026 新趋势:Matryoshka 可截断向量(短向量在线+长向量精排)、late-interaction(ColBERT v2,多向量精度但大)、多向量 per chunk
  • 生产部署的关键是吞吐和稳定性——批处理 + 动态 batch + 熔断 / 降级

9.1 Embedding 要解决的本质问题

RAG 的检索阶段问一个核心问题:"库里哪些 chunk 和这个 query 最相关?" 把这个问题工程化需要一个度量——"相关性"必须是可计算的数字。

最朴素的度量是 BM25(第 12 章)——按词项重叠打分。但"新版企业版 SSO 功能"和"最新产品套餐 单点登录"这两句在词项上几乎不重叠、语义上完全等价。BM25 给低分、语义检索给高分——这是 Embedding 在 RAG 里的不可替代价值。

Embedding 模型把这种"语义距离"学成一个向量空间的几何距离:把一段文本映射到 R^d 空间中的一个点(d 通常 384-1536),让语义相近的文本在空间中靠近。检索时只需要算 query 向量和 chunk 向量的余弦相似度,取 top-k。

这个思路简单,但实现细节决定了效果:

  • 空间怎么训出来?(9.2)
  • 向量维度怎么选?(9.3)
  • 领域不适配怎么办?(9.4-9.5)
  • 推理效率怎么撑起生产流量?(9.6)

9.2 三大训练范式

对比学习:主流中的主流

SimCSE(Gao et al., arXiv:2104.08821)开启的路线。核心思路:让 (anchor, positive) 对的向量靠近、让 (anchor, negative) 对的向量远离

训练数据形如 (query, relevant_doc, irrelevant_doc_1, ..., irrelevant_doc_k)。loss 用 InfoNCE:

text
loss = -log( exp(sim(q, pos)) / (exp(sim(q, pos)) + Σ exp(sim(q, neg_i))) )

BGE(BAAI)、E5(Microsoft)、GTE(Alibaba)、Voyage、Cohere v3 都是这条路线的成熟产品。

关键工程细节:

  • 难负例(hard negatives):纯随机负例学不到细粒度语义。要从 BM25 召回的"表面相似但真正无关"的候选里挑
  • in-batch negatives:同 batch 里其他样本的 positive 互为负例——免费的负例,batch_size 越大效果越好
  • 多阶段训练:先在大规模弱监督数据(网页对、搜索 log)预训练,再在高质量人工标注数据上微调

蒸馏:小模型的出路

大模型(如 BGE-large 335M)效果好但推理慢。蒸馏把大模型的向量空间"教给"小模型:

  • 学生模型 (如 MiniLM 33M) 的输出向量 MSE-loss 对齐教师 (BGE-large)
  • 推理速度 5-10 倍提升,精度损失 2-5%

生产场景的经典权衡:对冷门长尾文档用大模型精细 embedding,对高频查询走小模型快速通道——第 11 章讨论缓存时会回到这个设计。

指令微调:让 Embedding 听懂任务

E5-mistral-7b(Microsoft 2024)走了另一条路:用 LLM 做 embedding、通过 prompt 告诉模型"我要检索什么"

text
Instruct: Given a web search query, retrieve relevant passages that answer the query
Query: how to train a llama

优点:同一个模型可以做检索、分类、聚类等多种任务——靠改 instruction 切换。Cohere embed-v3、Voyage-3 都加了类似的 instruction input。

缺点:模型大(7B+)、推理慢、显存占用高。适合质量优先场景(专业知识库、科研文献);不适合通用高并发场景。

9.3 选型六维度

给定一个 RAG 项目,选 Embedding 模型要看六个维度。

语言覆盖

  • 中文为主:BGE 系列(bge-m3、bge-large-zh-v1.5)、M3E、智源的 PIXIU
  • 英文为主:E5、text-embedding-3、Voyage、Cohere embed-v3
  • 多语言:bge-m3 / paraphrase-multilingual-mpnet / multilingual-e5

MTEB 是基准 leaderboard(huggingface.co/spaces/mteb/leaderboard)——针对语言和任务有独立排名。选前查自己目标语言的子榜。

领域

通用 embedding 在专业领域会水土不服:

  • 医疗:PubMedBERTBioASQ 训练的模型
  • 法律:LegalBERT、阿里的lawformer
  • 代码:voyage-code-2nomic-embed-codejina-embeddings-v2-code
  • 金融:FinBERT 系列

如果业务在垂直领域且有标注数据,微调 比直接换模型效果好(9.5)。

输入长度

Embedding 模型的最大输入 token 数:

模型最大 token备注
bge-small-zh-v1.5512短文本优化
bge-m38192长文本友好
text-embedding-3-small8192OpenAI
text-embedding-3-large8192OpenAI
voyage-316000长文档王者
jina-embeddings-v38192多语言长文本

注意 "最大" ≠ "最佳"。多数模型在 200-500 token 样本上训练最充分,远超训练分布的长 chunk 表现会下降。bge-m3 的 8192 是工程可用、但 4000 token 的 chunk 比 200 token 的 chunk 在检索质量上没有优势。

向量维度

常见维度:

  • 384:bge-small、MiniLM 系列
  • 768:bge-base、multilingual-e5-base
  • 1024:bge-large、bge-m3、jina-v3、voyage-3
  • 1536:text-embedding-3-small、ada-002
  • 3072:text-embedding-3-large

维度影响两件事:

  • 索引内存:1M chunk × 1024 dim × 4 bytes = 4 GB RAM。维度翻倍直接翻倍存储
  • 检索延迟:距离计算 O(d),维度越大越慢

Matryoshka 向量(Kusupati et al., arXiv:2205.13147)让同一向量可以截断使用——完整 1024 维最精、截到 384 维仍可用。text-embedding-3、nomic-embed 都支持。生产常见用法:ANN 检索用短维度快速召回、top-k 精排用完整维度打分。

推理成本

模型吞吐(单 A100)美元/1M tokens
bge-small3000+ chunk/s自托管 ~$0.02
bge-m3500-800 chunk/s自托管 ~$0.15
text-embedding-3-smallAPI$0.02
text-embedding-3-largeAPI$0.13
voyage-3API$0.06
cohere embed-v3API$0.10

API 便利但并发有限;自托管延迟低但运维成本。混合策略:日常索引走 API 按量付费,大批量全量重建走自托管 GPU pool。

License

开源可商用的 Embedding 模型:bge-* (MIT)、nomic-embed (Apache 2.0)、jina-v3 (non-commercial for v3、v2 Apache)。闭源 API:OpenAI、Voyage、Cohere。

注意"开源可商用"≠"可微调再发布"。部分模型用了有限制的训练数据,商用前查清 license。

9.4 Query 和 Document 非对称编码

BGE、E5 这类模型在训练时区分 query 和 document——查询加 "Represent this sentence for searching relevant passages: " 前缀、文档不加。推理时要严格对应:

python
# 正确
q_vec = embed(query, mode="query")    # 带 prefix
d_vec = embed(chunk, mode="document")  # 不带 prefix

# 错误——都用 document 模式
q_vec = embed(query, mode="document")  # 匹配度下降 5-15%

这个细节在 quickstart 教程里常被省略。生产代码必须严格区分 query/document mode。模型卡里会说明各自的 prompt/prefix 格式。

E5-mistral 走了一步更远——用完整的 instruction prompt 描述任务、query 和 document 分别加不同的 instruction。

9.5 领域微调:什么时候值得做

什么时候不用微调

  • 通用领域(百科、一般产品文档):开箱 bge-m3 / voyage-3 已足够
  • 标注数据不够(< 5000 条相关对):微调数据不够反而伤泛化
  • 团队没有 ML 工程师:维护微调模型的运维成本高

什么时候值得微调

  • 专业领域(法律条文、医学术语、代码)且标注数据充足
  • 开箱模型在本领域 gold set 上 recall@10 < 70%
  • 有长期 ML 团队能维护训练和发布流程

微调的两条路

路径 1:全参数微调。拿开源模型(bge-base / E5-base)当起点、在领域对比学习数据上继续训练。效果好但成本高(几小时 GPU × 几次迭代)、模型完整部署。

路径 2:LoRA / QLoRA。只训练 adapter 参数、base 模型冻结。训练成本降 10 倍、部署时 adapter 动态加载、可以针对不同子领域维护多个 adapter。2024 年后这条路主流化。

训练数据从哪来

  • 业务 log:用户点击、采纳的 (query, chunk) 是天然正样本
  • 人工标注:少量高质量标注校准方向
  • 合成数据:用 LLM 根据 chunk 生成问题,(generated_q, chunk) 作为正样本——Llama-index 和 InPars 都有工具链
  • 难负例挖掘:BM25 或老版 embedding 召回的"高分但实际无关"的 chunk 作为负样本

实操比例:30% 人工 + 50% 合成 + 20% 业务 log。纯合成容易过拟合合成模式、纯人工量不够。

9.6 推理部署:吞吐、延迟、稳定性

生产 embedding 服务的三个工程要点。

动态 batch

单次 embedding 调用固定 batch=1 会让 GPU 利用率只有 10-20%。动态 batch 聚合多个请求:

  • 队列里等 0-20ms 积累请求、最多 64 条、到就 flush
  • batch=32 时 GPU 利用率 >80%、吞吐 10 倍于 batch=1
  • 延迟代价:P50 延迟 +10-20ms、换 10 倍吞吐值

开源方案:Triton Inference Server、vLLM 的 embedding backend、TEI(Text Embeddings Inference,Hugging Face 官方)。

Prefix caching

query/document prefix 相同、每次推理都重算 prefix 浪费。推理引擎支持 prefix cache 时复用 prefix 的 KV/attention——节省 5-10% FLOPs。小收益但免费。

熔断和降级

外部 API embedding 服务偶尔抖动(OpenAI limit、网络、服务端故障)。熔断策略:

  • P99 延迟或错误率超阈值:自动切到 fallback 模型(自托管 bge-small)
  • 业务继续运转、索引延迟略增但不中断
  • 恢复后自动切回主模型

在线查询侧比离线索引侧更需要熔断——离线可以等恢复后补、在线等不了。

多模型混合部署

不同场景选不同模型,一套服务对外:

  • 召回模型(bge-small 快):低延迟、覆盖面
  • 精排模型(bge-m3 准):top-k 精调
  • 领域模型(微调 adapter):专业查询

服务端按 request header 路由到不同 model。这种混合部署让单个 RAG 系统同时享受快和准。

9.7 Embedding 服务的容量规划

一个 RAG 项目在 Embedding 这一环的容量规划要算清楚两条需求:

离线索引吞吐。假设知识库 500 万 chunk、每月增量 50 万 chunk。

  • 全量重建:500 万 / 800 chunk/s = ~1.7 小时(单 A100 + bge-m3)
  • 每日增量:50 万 / 30 天 ≈ 1.7 万/日 → 30 分钟(宽裕)
  • 结论:1 张 A100 完全覆盖离线需求

在线查询吞吐。假设日活 10 万、人均 5 次查询、QPS 峰值约 10。每次查询 1 个 embedding 调用。

  • 峰值 10 QPS、单次 < 50ms,1 张 A100 撑得住
  • 但实际要考虑每次查询可能有多次 embedding 调用(query rewrite 生成 3 个子查询)
  • 3 倍 QPS = 30 → 仍然 1 张 A100 够

双活方案:主备各 1 张 A100 跑 embedding 服务、主挂备顶。2 张 A100 覆盖 500 万 chunk 知识库 + 10 万日活的 RAG 足够。

如果查询 QPS 上 100+、或知识库上亿 chunk,需要多 GPU 水平扩展——走 Triton / TEI 的多实例部署。

9.8 评估 Embedding 的方法

选定候选模型后怎么验证?三层评估。

MTEB 排行榜

快速初筛。针对目标语言和任务(Retrieval / STS / Classification)看分数。但 MTEB 是公开数据集、不完全代表你的业务。

业务 gold set

200-1000 条业务真实 QA,每条标注 gold chunk。跑每个候选模型测 recall@10 / MRR。这是最重要的评估——排行榜再高,业务 gold set 上低就没用。

在线 A/B

候选模型上线 10% 流量、观察 CTR、refusal rate、转人工率。最准但最慢。候选模型必须先通过前两层才值得做 A/B。

实操顺序:MTEB 选 3-5 个候选 → gold set 选 1-2 个 top → A/B 验证选最终 1 个。

9.9 Embedding 的十个常见坑

生产 Embedding 服务里反复出现的坑,列出来避免踩:

  • 坑 1:tokenizer 不一致。训练和推理用不同 tokenizer(版本、vocab)导致 embedding 漂移。统一锁 tokenizer 版本
  • 坑 2:padding 策略不一致。训练时 right_pad 推理时 left_pad——pooling 层取 last_token 拿到的是 pad token。pooling 策略要和训练对齐(CLS / mean / last)
  • 坑 3:正则化遗忘。训练时对 embedding L2 正则化、推理忘了做——余弦相似度计算错误
  • 坑 4:FP16 精度损失。大向量维度(1024+)用 FP16 存储、距离计算累积误差。保留 FP32 或用 BF16
  • 坑 5:温度系数忘记标定。对比学习的 temperature 在推理时不相关,但若做 softmax 重排、得用标定温度
  • 坑 6:text truncation 静默。输入超限被 tokenizer 静默截断、后半段丢失。生产必须显式检查长度并告警
  • 坑 7:多语言混文。中英混文按哪种语言 tokenize 影响大。多语言模型选用对齐 tokenizer(xlm-roberta 系)
  • 坑 8:embedding 不归一化。向量库有的算 cosine 有的算 inner-product——不归一化结果不一致。入库前统一归一化
  • 坑 9:更新模型不回灌。换了新 Embedding 模型,老 chunk 没重 embed,新老向量空间不兼容。必须全量重 embed
  • 坑 10:数值不稳定。某些模型在 long input + 异常字符上产出 NaN 向量。入库前 validator 检查 all(finite(vec))

这十个坑几乎每个生产 RAG 团队都踩过至少三个。前期把 validator 写好、CI 跑好,能躲掉大部分。

9.10 2024-2026 的三条新路线

Embedding 领域不是静止的。最近三年出现的新路线:

Matryoshka:可截断向量

同一模型同一次推理产生一个 1024 维向量、但前 64/128/256/512 维各自可独立用作检索向量。实现靠训练时 loss 同时优化多个截断点。text-embedding-3、nomic-embed-1.5 都是 Matryoshka。

生产用法:ANN 召回用 128 维快速跑大集合、精排再用 1024 维打分。延迟降低 60% 不损精度。

Late-interaction:ColBERT v2

ColBERT v2 不给 chunk 一个向量,给每个 token 一个向量。检索时 query 的每个 token 和 chunk 的每个 token 求 max-sim 再求和。

优点:精度比 single-vector 显著高(BEIR 平均 +5-10 个点)。缺点:存储膨胀 30-100 倍(per-token 向量数)。

应用:作为精排阶段的打分器,而非召回阶段的索引——精排只对 top-50 做、存储压力可控。

三条路线的选型

生产实战:大部分项目选 Matryoshka——成本低、收益显著、无需大改架构。ColBERT v2 适合"成本不敏感但一定要拿到精度上限"的项目(法律、医疗、科研)。多向量 per chunk 是商业 API 提供的"中间档",无需自托管。

未来趋势预判

三条路线可能融合——一个模型同时支持:

  • Matryoshka 可截断主向量
  • 额外产生若干辅助向量(多向量)
  • 在特殊场景下退化成 late-interaction 做精排

2026 年已有模型(jina-v3)部分实现了这种"多模式"架构。预计 2027-2028 年会成为主流 embedding 模型的标配能力。RAG 工程需要跟进这种演进——但稳定生产系统不要追最新模型,等一个模型有 6+ 月的社区验证再换。

多向量 per chunk

极简版 ColBERT——给每个 chunk 产生多个向量(比如 3-5 个,分别代表 chunk 的不同语义角度)。存储膨胀适中(3-5 倍)、精度介于 single-vector 和 ColBERT 之间。商业 embedding API(Jina v3、Voyage)开始提供。

9.11 Embedding 的量化与压缩:int8 与 binary 向量

2024 年后 embedding 领域另一条实用路线——向量本身的量化压缩。这和第 10 章 PQ 不是一回事:PQ 是ANN 索引内部的压缩技巧,向量进索引后再压;而 int8 / binary 量化发生在embedding 输出阶段——模型直接产出低精度向量、内存和传输都省。两种技术可以叠加。

三档精度的权衡

1024 维向量的存储:

  • float32:4096 bytes/向量。100 万向量 = 4 GB
  • int8:1024 bytes/向量。100 万向量 = 1 GB(4× 压缩、recall 保留 ~99%)
  • binary:128 bytes/向量。100 万向量 = 128 MB(32× 压缩、recall 保留 ~90%)

binary 把每个维度压成 1 bit——用 sign(x) 或阈值切分。距离从 cosine 变成 Hamming 距离(异或后数 1)——XOR + popcount 指令比 float 乘加快 10-100 倍。

为什么 recall 损失可控

int8 量化精度保留 ~99% 的秘密:embedding 分布接近高斯,极值稀有、动态范围不大。把 [-1, 1] 的 float32 映射到 [-128, 127] 的 int8、精度损失在小数点后几位、对 cosine 排序几乎无影响。

binary 更激进但仍能工作的原因:高维空间里方向比具体数值更重要。1024 维 float 向量的"方向"大部分由每个维度的符号决定、幅度是次要信息。binary 保留了最核心的符号信息。

两阶段检索模式

binary 最成功的生产用法是两阶段

  1. 粗召回:用 binary 向量 + Hamming 距离在全库跑 top-500。速度比 float32 快 30-50×
  2. 精排 rescoring:对 top-500 加载对应的 float32 向量、算真实 cosine、选 top-k

粗召回阶段扩大 K(如从 top-50 扩到 top-500)吸收 binary 的精度损失、精排阶段用 float32 把排序修正回来。整体 recall 接近纯 float32、延迟和存储接近纯 binary。

支持矩阵

2026 年主流模型的量化支持:

模型int8binary备注
bge-m3✓(社区版)原生支持 int8、binary 需后处理
mxbai-embed-large官方提供 binary 和 int8 权重
Cohere embed-v3API 参数 embedding_types: ["int8", "binary"]
nomic-embed支持 Matryoshka + binary 叠加
text-embedding-3-通过客户端量化
voyage-3-官方 int8

无原生支持的模型也能客户端量化——但会损失一些精度,因为训练时没考虑量化感知。

量化感知训练 vs 事后量化

两种获得量化向量的路径:

  • 事后量化(post-hoc quantization):用 float32 模型跑推理、对输出向量做量化。简单、零改动。精度损失典型 0.5-2%
  • 量化感知训练(quantization-aware training, QAT):训练时就让模型输出 int8 / binary 向量、loss 反向传播包含量化误差。精度损失可低到 < 0.5%、但需要重训

生产场景里事后量化已经够用——0.5-2% 的 recall 损失在 hybrid + rerank 链路里基本看不出。

何时用量化

场景推荐
千万级以上向量 + 内存紧张binary 粗召 + float 精排
百万级 + 延迟敏感int8
百万级以下 + 有 rerank 兜底int8 或 float32(看成本)
高精度硬需求(法律/医疗)float32 + int8 备份路径

常见坑

  • 归一化顺序错:量化前要对 float 向量做 L2 归一化、否则 int8 动态范围失控
  • Hamming 距离和 cosine 不同量纲:不要直接拿 binary 分数当 cosine 分数用——要么都转一致单位、要么用 rank 融合(ch13 RRF)
  • 数据漂移后量化参数失效:int8 的缩放系数(scale/zero_point)按训练分布定、分布变后量化误差爆增。定期校准
  • 多版本混用:int8 向量和 float32 向量不能直接比——要么统一升到 float32 算、要么分别维护索引
  • 粗召回 K 设太小:粗召回 top-50 + 精排 top-5——binary 的精度损失让 top-5 之外的真相关 chunk 漏掉。粗召回 K 通常 10× 最终 k

和 Matryoshka 的叠加

Matryoshka 降维度、量化降每维度精度——两者正交、可叠加:

text
原始: 1024 dim × 4 bytes = 4096 bytes/向量
+ Matryoshka 截到 256 dim: 256 × 4 = 1024 bytes (4× 压缩)
+ int8 量化: 256 × 1 = 256 bytes (16× 压缩)
+ binary: 256 / 8 = 32 bytes (128× 压缩)

128× 总压缩——1 亿向量从 400 GB 降到 3.2 GB 单机可装。recall 用两阶段 rescoring 补回来。2025 年后部分 SOTA RAG 系统就是这种组合。

9.12 多模态 Embedding:文本之外的检索

前 11 节的 Embedding 都假设输入是文本。但 2024-2026 年真实企业文档里、纯文本只占 60-70%——剩下是截图、示意图、产品照、嵌入 PDF 的表格、产品视频里的演示片段、录音会议纪要。这些非文本内容用传统 text embedding 漏召、用 OCR+文本 embedding 又损失结构——多模态 Embedding 是这个空白的答案。

多模态 Embedding 的三条主线

  • 文本 embedding:前面 11 节
  • 图像-文本联合(CLIP 家族):图像和文本映射到同一向量空间——用文本 query 能召回图像、反之亦然
  • 音频-文本联合(CLAP、LAION-CLAP):类似 CLIP、但替换成音频
  • 统一多模态:2024 年后兴起的"一个模型处理所有模态"——Cohere Embed v3 multimodal、Voyage-multimodal-3、Jina-clip-v2

CLIP 的工作原理和局限

CLIP(OpenAI 2021)和它的变体(SigLIP、EVA-CLIP)是多模态 embedding 的基础。核心思路:

  1. 训练时大量 (image, caption) 对、用对比学习拉近同一对的距离、拉远不同对
  2. 推理时图像过 image encoder、文本过 text encoder、各自输出同维度向量
  3. cosine 距离直接跨模态对比

生产里 CLIP 的几个实际局限:

  • 文本端能力弱:CLIP 的 text encoder 比 BGE/E5 短不少、长句理解差。"文本 + 文本"的 RAG 不要用 CLIP 当通用 embedding
  • 领域泛化有限:CLIP 预训练数据偏自然图像、企业文档里的流程图、截图、PDF 版式表现一般
  • 分辨率限制:典型 224×224 或 336×336、小字识别不行

生产补救:

  • 文本 embedding 继续用 BGE/Voyage——只把图像走 CLIP 独立索引
  • 业务领域多的话、fine-tune CLIP 或用领域模型(MedCLIP 医疗、FashionCLIP 时尚)

统一多模态:2024 年后的方向

Cohere Embed v3 multimodal、Voyage-multimodal-3、Jina-clip-v2 的核心卖点:一个 embedding 模型同时支持文本、图像、PDF 版面——不用分别维护 text 和 image 索引。

python
# Voyage 风格 API
text_vec = embed("企业版 SSO 配置界面")
image_vec = embed(png_bytes)     # 同一个 embed 函数
pdf_vec = embed(pdf_bytes)        # PDF 原始字节(内部处理版面)

# 都在同一向量空间、可直接对比
similarity = cosine(text_vec, image_vec)

统一多模态对 RAG 工程的简化明显:

  • 单一向量索引、不需要 chunk 是图还是文
  • 检索跨模态自然、"找和这张产品图类似的文档"一次查询搞定
  • 只维护一个 embedding 服务、成本 / 运维都省

但 2026 年的统一多模态模型在纯文本 retrieval 精度上通常仍不如专用文本 embedding(BGE-m3 等)——权衡是"一处简单 vs 两处最优"。

多模态 RAG 的工程模式

生产多模态 RAG 的典型架构:

  • 解析阶段(第 5 章):PDF 解析出文本 + 图像 + 表格、分别产生独立 chunk
  • 嵌入阶段
    • 文本 chunk → text embedding
    • 图像 chunk → CLIP / 统一模型 embedding
    • 表格 chunk → 转成 markdown 后 text embedding(结构化保留)
    • 音频 chunk → CLAP embedding 或先 ASR 转文本
  • 索引阶段:所有 embedding 进同一向量库(同维度或按类型分 collection)
  • 检索阶段:query 先判断类型(文本 query 还是图像 query)、选对应 embedding 路径

图像 chunk 的 metadata 要保留文档来源 + 所在页码 + 描述文本——这样答案可以显示"见《产品手册》第 5 页的架构图"、而不是只给个图片 URL。

多模态 RAG 的评估挑战

多模态评估比纯文本复杂:

  • 跨模态相关性:"这张图是否回答了这个问题"——需要人工标注、LLM-as-judge 在视觉任务上稳定性差
  • 图像质量是信号:同一内容的清晰图 vs 模糊图在召回时应有差异——单纯 embedding 不直接体现
  • 答案引用的视觉指代:答案说"如图所示"、UI 要能跳转展示——测试 retrieval 不够、还要测 UI 对齐

生产方案:文本 RAG 的 gold set 保持独立测试、图像 RAG 建独立 gold set(问题 + 期望图像)、分别评估、避免相互干扰。统一报告里分 modality 拆指标。

什么时候该上多模态 RAG

不是所有 RAG 都需要:

场景是否值得上多模态
纯文本 FAQ / 文档问答不需要
产品手册含大量示意图值得
代码库 RAG(含 diagrams)中等、取决于 diagram 重要性
电商 / 时尚 / 视觉检索产品必需
医疗影像 / 法律证据图必需、且需要领域专用模型

判断标准:如果 30%+ 的有效信息藏在图像 / 表格 / 音视频里、就值得上多模态。否则 OCR + 文本 RAG 成本效益更高。

多模态的成本

多模态 Embedding 的成本典型比纯文本高 3-10 倍:

  • Image embedding:CLIP 类单图约 50-100ms、每千图几美分(API)或自托管 GPU
  • Audio embedding:CLAP 每分钟音频约 100-200ms
  • 统一多模态:Voyage/Cohere 按 token + image token 混合计费

生产考虑离线索引时批量计算(GPU 利用率高、成本降 50%+)——在线检索只做 query 侧 embedding、就和纯文本 RAG 成本相当。

9.13 Embedding 模型升级的迁移工程

§9.9 的坑 9 提到"换新 embedding 模型、老 chunk 没重 embed、新老向量空间不兼容"——这是 embedding 升级最危险的陷阱。实际生产每 6-12 月就可能遇到一次 embedding 升级(模型厂商出新版、自托管模型迭代、业务换垂直模型)——这一整节讲清楚升级的工程路径、避免踩第二次坑。

升级的三种触发

每种触发性质不同:

  • 厂商换版:被动、必须跟(老版本可能下架)
  • 自托管更新:主动、按需跟进
  • 垂直换模型:主动、ROI 驱动

核心挑战:向量空间不兼容

Embedding 升级最严重的问题:新老模型的向量不在同一空间。直觉检验:

python
# 新老模型对同一文本的 embedding
v_old = old_model("企业版 SSO")  # 例 [0.12, -0.34, ...]
v_new = new_model("企业版 SSO")  # 例 [0.21, 0.05, ...]

# 两个向量的 cosine 相似度可能只有 0.3-0.5
# 就算内容完全相同、模型视角不同

这意味着:

  • 新 query 用新模型 embed、拿去老索引检索 → 距离完全错位、recall 崩溃
  • 老 chunk 用老模型 embed、新 query 用新模型 → 同样不行
  • 必须全量重 embed所有 chunk——不能混用

三种迁移策略

策略 1:停机重建。停服务、全量重 embed、重建索引、切换、启服务。

  • 优点:代码路径不变、简单
  • 缺点:百万 chunk 重 embed 几小时、用户感受明显
  • 适合:小项目、非关键业务、低活跃时段

策略 2:蓝绿双索引。老索引(蓝)继续服务、新模型并行建新索引(绿)。建完后切换路由到绿索引、蓝索引保留 N 天作回滚。

  • 优点:零停机、回滚快
  • 缺点:存储翻倍一段时间、应用层需要支持 index 切换
  • 适合:中大型项目、推荐方案

策略 3:渐进双写。新增 chunk 同时用新老模型 embed 写两份、历史 chunk 后台逐步重 embed。全部完成后切换。

  • 优点:迁移周期最长但对系统冲击最小
  • 缺点:工程复杂、期间可能部分不一致
  • 适合:超大规模项目、不能有任何停机窗口

多数项目选策略 2(蓝绿双索引)——平衡简单和零停机。

迁移的验证 checklist

切换前必须验证的项:

  • [ ] 向量格式兼容:新模型维度、数值范围和向量库 schema 匹配
  • [ ] gold set recall 不退化:新索引在历史 gold set 上 recall 至少不低于老版
  • [ ] 新能力测试:新模型的优势(如长 context)在对应 gold set 上体现
  • [ ] 延迟测试:新模型推理延迟在 SLA 内
  • [ ] 成本估算:新模型 + 新索引的月成本和预算对比
  • [ ] badcase 回归:历史 badcase 在新索引上仍然修复(不回退)
  • [ ] 权限和 metadata 完整:全量重 embed 不能丢任何 chunk 的元信息

任一不过关 → 不切换。带着问题上线后恢复代价 10× 预防。

Shadow 验证阶段

切换前、让新索引先跑 shadow 流量

python
# 线上请求 shadow 到新索引
async def retrieve(query):
    # 主路径:老索引
    primary_result = await old_index.search(query)
    
    # shadow 路径:新索引(异步、不阻塞用户)
    asyncio.create_task(shadow_check(query, primary_result))
    
    return primary_result

async def shadow_check(query, primary_result):
    new_result = await new_index.search(query)
    # 对比 recall 交集、记录 delta
    log_delta(query, primary_result, new_result)

Shadow 一周、积累几万条真实 query 的对比数据——看新索引在真实流量下是否达标。用 gold set 不够、用真实流量才稳。

回滚策略

切换后发现问题、能分钟级回滚是必备:

  • 应用配置里 active_index = "green" 改回 "blue" 即可
  • 不涉及数据库变更、纯流量切换
  • 演练过的团队、回滚 < 5 分钟

蓝索引保留期建议 至少 2 周——某些问题只在长尾流量里暴露、短期内没发现。2 周后确认稳定再清理蓝索引释放存储。

升级的成本

100 万 chunk 规模的升级典型成本:

  • 重 embed:100 万 × 单价(如 $0.00001/chunk)≈ $10-50
  • 新索引构建:几 GB 内存、几十分钟
  • 存储翻倍(蓝绿期):2 周 × 双倍存储费 ≈ $100-200
  • 人工:验证 + shadow + 切换 → 2-3 人周

总计千元美金级——相对 RAG 基础设施成本是小数目。但不做可能花百倍修复 recall 漂移事故

渐进式升级:Matryoshka 和快照

两种可以减少升级风险的新趋势:

  • Matryoshka embedding(§9.10):新老模型如果都支持 Matryoshka、可以从高维截到相同低维、兼容性可能保留一部分(虽然不完美)
  • Embedding 空间对齐(学术):训一个映射模型 f: V_old → V_new、近似把老向量迁到新空间——避免重 embed。2025-2026 年在研究、生产还不稳

两者都是减负、但不替代完整重建——追求正确性还是要全量重 embed。

常见迁移反模式

  • 增量 embed 新 chunk、不动老 chunk:新老混用、新 query embed 后检索结果漂移、recall 崩
  • 没做 shadow 验证:直接切换、出问题时已经线上用户遭殃
  • 不保留老索引:切换后立刻删老、回滚不可能
  • Gold set 和训练数据混:新模型在 gold 上分高、上线垮——gold 污染了
  • 升级时机错:业务高峰、版本迭代高频期间升级——错上加错

升级频率建议

  • 商用 API:跟厂商主版本、一年 1-2 次
  • 自托管开源:半年评估一次、看是否有明显更优选择
  • 业务模型:有明确 badcase 时才升、不盲目追新

过频升级是工程浪费——每次重建索引、验证、可能事故——ROI 通常只在有明确问题驱动时才好。

9.14 Embedding 的调试与可解释性

前 13 节讨论了 embedding 的训练、选型、部署、升级——但遇到问题怎么定位是单独一门工程手艺。embedding 是 1024 维黑盒、"这两条为什么检索不到"、"这个 chunk 的向量为什么异常"——没有专门的调试工具、工程师只能靠猜。这节把 embedding 调试的常用方法整理出来、让黑盒变灰盒。

调试的典型场景

每种场景都有不同的调试切入点。

场景 1:召回不到已知相关 chunk

用户问 "企业版 SSO 配置"、系统里明明有一份讲这个的文档、但没被召回到 top-10。排查:

python
def debug_not_found(query, expected_chunk_id):
    # 1. 查 chunk 是否在索引里
    chunk = vector_db.get(expected_chunk_id)
    if not chunk:
        return "chunk not indexed"
    
    # 2. 算 query 和 chunk 的 cosine 相似度
    q_vec = embed(query)
    similarity = cosine(q_vec, chunk.vec)
    print(f"Similarity: {similarity}")  # 如果 < 0.5 说明 embedding 把两者分开了
    
    # 3. chunk 在 top-K 的排名
    results = vector_db.search(q_vec, top_k=100)
    ranks = [r.id for r in results].index(expected_chunk_id) if expected_chunk_id in [r.id for r in results] else None
    print(f"Rank in top-100: {ranks}")
    
    # 4. 找比 chunk 更相似的"抢位"chunk
    for r in results[:20]:
        if r.id != expected_chunk_id:
            print(f"Rank {r.rank}: {r.text[:100]} (sim={r.score})")

这个脚本能区分:

  • 根本没索引:chunk 不在 DB 里
  • 相似度过低:embedding 把 query 和 chunk 分开了
  • 被别的 chunk 抢位:有更相似但不相关的干扰项

不同原因对应不同修复——瞎猜浪费时间。

相似度可视化:UMAP / t-SNE

想看 chunk 的 embedding 空间整体分布——用降维可视化:

python
from umap import UMAP
import matplotlib.pyplot as plt

def visualize_embeddings(chunks, sample_n=1000):
    # 随机采样避免图太密
    sample = random.sample(chunks, sample_n)
    vectors = np.array([c.vec for c in sample])
    labels = [c.metadata["category"] for c in sample]
    
    # UMAP 降到 2D
    reducer = UMAP(n_components=2, random_state=42)
    coords = reducer.fit_transform(vectors)
    
    # 按 category 着色
    for cat in set(labels):
        mask = [l == cat for l in labels]
        plt.scatter(coords[mask, 0], coords[mask, 1], label=cat, alpha=0.5)
    plt.legend()
    plt.savefig("embedding_viz.png")

这种图能肉眼看出:

  • 聚类分布:同类 chunk 是否在空间里聚成团?
  • 异常点:孤立的 chunk 可能是异常(embedding 坏了 / 内容异常)
  • 类别重叠:两个"不同"类别的 chunk 混在一起、说明 embedding 分不出

2D 图不严格、但能发现大问题。做重大 embedding 迁移前(§9.13)必做一次可视化对比。

异常 embedding 的识别

有些 chunk 的 embedding 本身就有问题——识别方法:

python
def detect_anomalous_embeddings(chunks):
    vectors = np.array([c.vec for c in chunks])
    norms = np.linalg.norm(vectors, axis=1)
    
    # 1. norm 异常(应该都是 1 如果归一化了)
    wrong_norm = (norms < 0.95) | (norms > 1.05)
    
    # 2. 全 0 或全非常小
    dead = norms < 0.01
    
    # 3. NaN / Inf
    invalid = ~np.isfinite(vectors).all(axis=1)
    
    # 4. 离群(和均值距离极远)
    mean_vec = vectors.mean(axis=0)
    dist_to_mean = np.linalg.norm(vectors - mean_vec, axis=1)
    outlier = dist_to_mean > dist_to_mean.mean() + 3 * dist_to_mean.std()
    
    return {
        "wrong_norm": wrong_norm.sum(),
        "dead": dead.sum(),
        "invalid": invalid.sum(),
        "outlier": outlier.sum(),
    }

每周跑一次、发现异常及时处理。

相似度分布的健康检查

健康的 embedding 系统、cosine 相似度应该满足:

  • 跨 chunk 平均相似度:0.3-0.5(太高说明 embedding 塌缩、所有 chunk 都相似)
  • 同 doc 邻近 chunk 相似度:0.6-0.8(高于跨 doc)
  • top-10 检索结果的分数分布:递减、不是平坦

监控:

python
def cosine_stats(chunks, sample=10000):
    sample_chunks = random.sample(chunks, min(sample, len(chunks)))
    vectors = np.array([c.vec for c in sample_chunks])
    
    # 随机对的相似度
    pairs = random.sample(range(len(vectors)), 1000)
    sims = []
    for i in range(0, len(pairs), 2):
        sim = cosine(vectors[pairs[i]], vectors[pairs[i+1]])
        sims.append(sim)
    
    return {"mean": np.mean(sims), "std": np.std(sims)}

分布漂移(均值从 0.4 升到 0.6)——embedding 开始塌缩、质量下降。

跨模型对比

评估一个新 embedding 候选时、不只跑 benchmark 分数——还要和现有模型对比同一份数据的相似度结构

python
def compare_embeddings(chunks, model_a, model_b):
    for chunk in chunks[:100]:
        v_a = model_a.embed(chunk.text)
        v_b = model_b.embed(chunk.text)
        
        # 两模型眼中的"最相似 chunk" 是否一致?
        top_a = find_nearest(v_a, all_vectors_a, top_k=5)
        top_b = find_nearest(v_b, all_vectors_b, top_k=5)
        
        overlap = len(set(top_a) & set(top_b)) / 5
        print(f"Overlap: {overlap:.2f}")

Overlap < 30%——两模型的"相关概念"差距大、升级要慎重。

具体 chunk 的语义解释

想知道 "为什么模型觉得 chunk A 和 chunk B 相似"——技术上很难解释黑盒、但几个 proxy 方法:

  • attention 可视化(自托管模型可用):看两个 chunk 的 encoder attention 重点对应哪些 token
  • 删词实验:逐个删除 chunk 里的关键词、看相似度变化——下降最多的词是"关键"
  • 换词实验:把关键词替换成同义词、看相似度是否保留——保留说明 embedding 捕捉了语义

这些 proxy 不严格、但比完全黑盒好。

Embedding 测试的 gold set

除了检索评估 gold set、embedding 本身也要 gold set:

text
测试项 1:同义句应该接近
- "企业版支持 SSO"
- "Enterprise 版本包含单点登录"
→ 期望 cosine > 0.85

测试项 2:反义句应该远
- "企业版支持 SSO"
- "基础版不支持 SSO"
→ 期望 cosine < 0.6

测试项 3:不相关应该远
- "企业版支持 SSO"
- "天气预报今天晴天"
→ 期望 cosine < 0.3

200-500 条这样的三元组作为 embedding unit gold set、每次升级跑。

生产 debug 工具链

生产环境的 debug 工具配置:

  • cosine similarity explorer:UI 里输入两段文本、立刻看相似度
  • neighbor lookup:任一 chunk → 看其 top-20 邻居(调试检索异常的直观方式)
  • embedding health dashboard:norm / 分布 / 异常数
  • A/B compare UI:同 query 同时跑两个模型、看差异

这些工具不需要花哨——简单 Streamlit / Gradio 应用即可。每周几次省几天人工排查。

Debug 的思维方式

Embedding debug 的核心是:假设 → 验证 → 迭代

  • 不要"瞎试参数"——每次改都要有假设
  • 不要"只看一个案例"——每个改动至少测 50 条、看 aggregate 效果
  • 不要"跳过基础"——先验证 norm / 编码 / 版本正确、再深入调优

这套思维方式不只 embedding——所有 ML 系统 debug 通用。

Debug 工具的长期投资

团队应该长期投入维护这套工具:

  • 初版:2-3 人周
  • 每季度扩展:0.5 人天
  • Badcase 库持续积累

Debug 工具是团队生产力的乘数——有比没有效率差 3-5 倍。尤其 embedding 升级 / 向量库迁移这种大事、工具准备好的团队能几天搞定、没准备的拖几周。

9.15 Embedding 的隐私风险:从向量能反推文本吗

Embedding 看起来是 "1024 维数字、看不懂"——所以很多工程师认为存 embedding 比存原文更安全。这是严重误解——学术界已证明 embedding 可以反推出原文(至少部分)。这对 RAG 的隐私含义重大、尤其是合规场景。这节把 embedding inversion 的研究现状、对 RAG 的实际威胁、防御措施讲清楚。

Embedding inversion 攻击

攻击者拿到 embedding 向量(没有原文)——能部分或完全恢复原文。2023-2025 年多篇论文证明可行:

  • Text Embeddings Reveal (Almost) As Much As Text(2023):从 OpenAI ada-002 embedding 恢复出 92% 的原文内容
  • Vec2Text(2024):多步迭代攻击、恢复精度进一步提升
  • TextObfuscator / Inversion defense(2025):研究防御

结论:embedding 不是"脱敏"形式的原文——是另一种表达、可以还原

对 RAG 的实际威胁

什么时候这个威胁真实存在:

  • 向量库被入侵:攻击者拿到 embedding 文件、反推得到所有 chunk 的内容。等同于原文泄漏
  • Embedding 被导出 / 共享:给第三方分析、以为匿名、实际不匿名
  • Train 数据泄漏:用户数据用来训 embedding 模型、模型被他人使用——可能反推训练数据

威胁场景在 RAG 里具体化:

  • 企业 RAG 索引被黑客拖走:内部机密信息可恢复
  • 和外部合作方共享 "embedding 数据":觉得不是原文所以安全——错
  • 用公有 embedding API(OpenAI):API 能看到你的文本(被 embed 时)

威胁的严重程度

不同场景的实际威胁:

场景威胁等级原因
公开知识(维基百科 embed)本来就是公开的
企业内部知识 RAGEmbedding 文件泄露 = 数据泄露
医疗 / 金融合规极高法规明确、赔偿罚款大
用户个人 memory embed隐私泄露严重

不能假设"embedding 泄漏"就安全——要按原文泄漏同等级别对待。

攻击的实际难度

不是说"随便拿到向量就能恢复"——需要条件:

  • 知道用的是哪个 embedding 模型:不同模型的 inversion 方法不同
  • 有模型本身的访问:攻击用相同模型的知识做迭代
  • 计算资源:反推每条 embedding 可能要几秒到几分钟 GPU 时间

所以不是"黑客拿到向量文件立即看到明文"——是"经过几天的工作恢复大部分"。但对高价值数据来说、这个门槛不高。

防御措施

防御 1:加密存储

向量库文件 at-rest 加密(磁盘级或应用级)——和原文一样保护。常识但很多团队忽略。

防御 2:访问控制

向量库 API 权限严控:

  • 只有 RAG 服务自身能查询
  • 定期审计访问日志
  • 任何大规模 export 触发告警(防撞库式下载)

防御 3:不必要时不存明文

如果某 chunk 的 metadata 里存了原文 text——和 embedding 一起存会双重暴露。考虑:

  • Metadata 里只存 text_id、原文存独立加密存储
  • 或 metadata 里只存 text 的哈希、检索后回查原文

防御 4:差分隐私 embedding

研究方向:在 embedding 时加噪声、让 inversion 恢复的内容"近似但不准"。代价是检索精度降——工程上还不成熟、但值得关注。

防御 5:不发出 embedding

API 给外部只返回 "chunk_id 列表"、不返回 embedding 向量。即使 API 被滥用、也得不到向量、无法反推。

外部 API 的隐私风险

用 OpenAI / Voyage / Cohere 的 embedding API:

  • 你的文本离开你的环境、到厂商服务器
  • 厂商是否存储、存多久——看合同和 ToS
  • 厂商被入侵的话、你的数据也受影响

合规场景(医疗 / 金融)——尽量自托管、不发敏感文本给第三方 API。

一个妥协:敏感部分脱敏后才 embed。比如把 "张三的薪资 5 万" 脱敏为 "<人名> 的薪资 <金额>"、embed 脱敏后的文本——语义保留但没隐私。

隐私和检索能力的权衡

完全保护隐私 vs 保留检索能力的取舍:

  • 完全加密 检索时无法用 embedding——失去 RAG
  • 差分隐私 加噪、降精度 5-10 点——成本高
  • 脱敏 + 正常 embed—— 丢失部分细节信息
  • 自托管 + 加密存储—— 相对均衡

多数企业 RAG 选 自托管 + 加密存储 + 访问控制 组合——既保留检索、又不让数据出境。

合规的具体要求

GDPR 第 4 条:pseudonymization(匿名化)的要求——embedding 是否算匿名?法律尚无明确定义、但研究证明可反推——趋势是按"伪匿名"对待、和原文同级保护。

HIPAA(美国医疗):要求 PHI(protected health information)不泄漏。Embedding 含可反推的 PHI——也在保护范围内。

中国 PIPL:个人信息保护法、embedding 若可反推识别个人——同受保护。

各地法规仍在演化——保守处理:按原文同级保护、不要赌"embedding 不是原文"。

向量库的 at-rest 加密实现

主流向量库的加密支持:

  • Qdrant:支持 disk 加密、Qdrant Cloud 强制启用
  • Milvus:支持 storage-layer 加密
  • pgvector:继承 Postgres 的 TDE(Transparent Data Encryption)
  • Pinecone:自带加密、不可选

自托管时 at-rest 加密用 LUKS / dm-crypt 等系统层加密——和普通敏感 DB 一样。

传输层加密

向量库和应用之间的网络也要加密:

  • TLS 是必需——不是可选
  • 内部网络也不要明文(vs "内部可信"的错误假设)
  • 敏感场景用 mTLS、双向认证

审计和合规证明

合规审查时要能证明已采取保护措施:

  • 证书:加密算法 / 密钥管理
  • 访问日志:谁查了什么向量
  • 审计报告:定期第三方审计
  • 事故响应 plan:embedding 泄漏时的通知机制

这些是合规的 baseline——不是 "有就好"、是"必须要"。

新兴威胁:member inference

除了 inversion、还有 member inference——攻击者不恢复原文、只判断"某条数据是否在你的训练集 / 索引里"。对隐私敏感场景同样威胁:

  • "某患者是否在这家医院" → 泄漏就业信息
  • "某交易是否在数据库" → 泄漏商业信息

防御类似——限制模型输出的信息

  • 检索返回不带具体分数(只给 yes/no)
  • 限制查询频率(防拖库)

这是新兴领域

Embedding 隐私是2023+ 的新研究领域——实践标准仍在形成。建议:

  • 关注学术进展(arXiv 上的 embedding inversion 研究)
  • 跟合规团队同步、不闭门造车
  • 按"比当前法规严一点"的标准做——等法规落定时已经合规

对 RAG 工程的启示

别把 embedding 当"看不见的数据"——把它当原文等级的敏感数据对待

  • 存储加密
  • 传输加密
  • 访问控制严格
  • 不必要时不共享
  • 审计完整

这些加起来、增加的工程复杂度不大、但避免未来的隐私事故——小投资大回报

9.16 跨书关联:Embedding 和预训练模型

Embedding 模型通常是预训练 encoder + 对比学习 finetune 的产物。encoder 骨架常用 BERT、RoBERTa、XLM-R、或最近的 Mistral/Gemma 裁剪。训练机制和原 LLM 预训练有重叠——了解 Transformer encoder 的 attention 和 pooling 层有助于理解 embedding 怎么产生。

《Rust 编译器与运行时揭秘》讨论 async 状态机时用的"把任意函数编译成状态机"的思路,在 Embedding 里有平行——把任意文本编码成固定维向量。两者都是"把多样输入压缩到固定形式"的工程抽象,只是一个在编译期、一个在推理期。

9.17 本章小结

  1. Embedding 把文本编码成向量让语义检索可计算——这是 RAG 检索的核心工具
  2. 三大训练范式:对比学习(主流)、蒸馏(小模型)、指令微调(LLM 级)
  3. 选型六维度:语言 / 领域 / 长度 / 维度 / 成本 / license
  4. Query / Document 非对称编码是必须注意的细节——生产代码严格区分 mode
  5. 微调 在专业领域值得——LoRA/QLoRA 成本低、效果显著
  6. 部署靠 动态 batch + prefix cache + 熔断
  7. 新趋势:Matryoshka / ColBERT v2 / 多向量——分层检索和精排

下一章讲向量索引——拿到几百万向量如何支持毫秒级 ANN 搜索——Flat、HNSW、IVF、PQ 的工程直觉。

基于 VitePress 构建