AI 工程基础体系 · 第 87/100 篇。内容覆盖机器学习、深度学习与生成式 AI;模型、数据、评测、权限和成本会作为同一生产系统处理。

Embedding 模型选型:维度、语言、归一化、领域与评测

在 RAG(Retrieval-Augmented Generation,检索增强生成)系统中,Embedding 模型负责把文本、代码、图片或其他对象映射为向量,使“语义相近”的对象在向量空间中距离较近。它解决的是候选信息召回问题,不直接负责事实生成、权限判断或答案正确性。

一个典型的 RAG 请求经过以下路径:

flowchart LR
    A[用户问题] --> B[查询 Embedding]
    B --> C[向量检索]
    D[文档切分] --> E[文档 Embedding]
    E --> F[向量索引]
    F --> C
    C --> G[过滤/重排]
    G --> H[上下文拼接]
    H --> I[生成模型]
    I --> J[答案与引用]

离线阶段把文档切分并生成向量;在线阶段把用户问题生成向量,然后在索引中检索候选片段。Embedding 选型因此不是单独比较“向量维度”或“榜单分数”,而是同时比较:

  • 向量表达了什么语义;
  • 查询和文档是否使用兼容的语言与训练目标;
  • 向量距离和归一化是否匹配;
  • 模型是否适合目标领域和文档形态;
  • 在自己的数据、权限、延迟和成本约束下,能否稳定召回正确证据。

一、Embedding 到底在优化什么

1. 从文本到向量

设文本空间为 X\mathcal{X},Embedding 模型为:

fθ:XRdf_\theta: \mathcal{X} \rightarrow \mathbb{R}^d

其中:

  • xXx \in \mathcal{X} 是一段文本;
  • θ\theta 是模型参数;
  • dd 是向量维度;
  • fθ(x)f_\theta(x) 是文本对应的向量。

例如,模型可能把“如何重置密码”和“忘记密码怎么办”映射为相近向量,把“如何重置密码”和“数据库索引原理”映射为较远向量。

但“相近”不是自然形成的数学事实,而是训练目标塑造出来的结果。常见训练方式包括:

  1. 对比学习:让正样本对靠近,让负样本对远离;
  2. 双编码器训练:分别编码查询和文档,再优化相关性;
  3. 多语言对齐:让不同语言表达的相同含义处于相近区域;
  4. 领域继续训练或微调:让模型适应法律、医学、代码、客服等特定语料。

因此,Embedding 的核心不是“把文字转换成数字”,而是学习一个距离函数:

s(q,d)=sim(fθ(q),fθ(d))s(q, d) = \operatorname{sim}(f_\theta(q), f_\theta(d))

其中 qq 是查询,dd 是文档,sim\operatorname{sim} 是相似度函数。

2. 相似度不是相关性的同义词

向量相似度只能近似“相关性”,不能保证文档真正回答问题。例如:

  • 查询:“退款是否需要人工审核?”
  • 文档 A:“退款申请通常需要人工审核。”
  • 文档 B:“退款申请不需要人工审核,系统会自动处理。”

如果模型主要学习了“退款”“人工审核”等主题词,A 和 B 都可能被召回;但只有理解否定关系或经过重排、规则过滤,系统才可能区分它们。

这说明 Embedding 通常承担的是高召回候选生成,而不是最终事实判定。RAG 原始论文提出的基本思想也是将参数化生成模型与外部非参数知识库结合;在工程实现中,向量检索通常还需要关键词检索、重排模型和生成后校验共同完成。

二、距离函数与归一化:先统一数学定义

选型中最容易被忽略、但最容易造成系统性错误的部分,是 Embedding 输出、归一化方法、索引距离和查询排序之间必须一致。

1. 点积、余弦相似度与欧氏距离

设两个向量为 x,yRdx,y \in \mathbb{R}^d

点积定义为:

xy=i=1dxiyix \cdot y = \sum_{i=1}^{d} x_i y_i

点积同时受方向和长度影响。若某些文档向量的模长较大,它们可能因为长度优势获得较高分数。

向量的 L2L_2 范数为:

x2=i=1dxi2\|x\|_2 = \sqrt{\sum_{i=1}^{d}x_i^2}

余弦相似度为:

cos(x,y)=xyx2y2\cos(x,y)=\frac{x\cdot y}{\|x\|_2\|y\|_2}

它只比较方向,不比较长度。

欧氏距离为:

xy2\|x-y\|_2

如果两个向量都经过 L2L_2 归一化,即:

x^=xx2,y^=yy2\hat{x}=\frac{x}{\|x\|_2}, \qquad \hat{y}=\frac{y}{\|y\|_2}

则:

x^y^22=x^22+y^222x^y^=22(x^y^)\|\hat{x}-\hat{y}\|_2^2 = \|\hat{x}\|_2^2+\|\hat{y}\|_2^2-2\hat{x}\cdot\hat{y} =2-2(\hat{x}\cdot\hat{y})

因此,在双方都归一化时:

  • 最大化余弦相似度;
  • 最大化点积;
  • 最小化欧氏距离;

三者会产生相同的排序。

这不是近似,而是由单位向量条件严格推出的等价关系。

2. 归一化的完整算例

考虑两个文档向量和一个查询向量:

q=(1,1)q=(1,1)

d1=(10,0),d2=(1,1)d_1=(10,0), \qquad d_2=(1,1)

未经归一化时:

qd1=10q\cdot d_1=10

qd2=2q\cdot d_2=2

如果使用点积,d1d_1 排名更高。

但余弦相似度为:

cos(q,d1)=102×100.707\cos(q,d_1) =\frac{10}{\sqrt{2}\times 10} \approx 0.707

cos(q,d2)=22×2=1\cos(q,d_2) =\frac{2}{\sqrt{2}\times\sqrt{2}} =1

如果真正关心方向上的语义相似,d2d_2 应当排名更高。

将向量归一化:

q^=(0.707,0.707)\hat{q}=(0.707,0.707)

d1^=(1,0)\hat{d_1}=(1,0)

d2^=(0.707,0.707)\hat{d_2}=(0.707,0.707)

此时点积为:

q^d1^0.707\hat{q}\cdot\hat{d_1}\approx0.707

q^d2^=1\hat{q}\cdot\hat{d_2}=1

排序与余弦相似度一致。

3. 什么时候不能随意归一化

归一化并非永远正确。模型可能把向量模长作为训练信号的一部分,例如:

  • 模长反映置信度;
  • 模长携带文本长度或信息量;
  • 模型训练目标明确使用未归一化点积;
  • 服务端返回的相似度定义已经固定。

此时强行归一化可能丢失有用信息。

相反,如果模型文档明确建议使用余弦相似度,或者模型输出的长度主要是数值尺度而非语义信号,则不归一化会让点积受模长干扰。

生产系统必须同时记录:

embedding_model = model-name
embedding_dimension = d
normalization = none | l2
similarity = cosine | dot_product | euclidean
index_metric = matching metric

只记录模型名称而不记录归一化和距离度量,无法可靠重建索引行为。

4. 不一致会如何失败

常见错误路径如下:

  1. 文档入库时使用未归一化向量;
  2. 查询时使用归一化向量;
  3. 索引使用点积;
  4. 工程师却把返回分数当成余弦相似度解释。

此时查询和文档的向量尺度不同,点积分数不再具有一致意义。表现可能是:

  • 某些长文档长期占据高位;
  • 更换查询长度后排名异常;
  • 阈值在离线测试有效,线上大量误召回;
  • 同一文档在不同批次中的分数不可比较。

正确做法是让文档和查询采用同样的预处理、同样的归一化、同样的度量定义,并用已知向量做单元测试。

import numpy as np

def l2_normalize(x: np.ndarray) -> np.ndarray:
    norm = np.linalg.norm(x)
    if norm == 0:
        raise ValueError("zero vector cannot be normalized")
    return x / norm

q = np.array([1.0, 1.0])
d1 = np.array([10.0, 0.0])
d2 = np.array([1.0, 1.0])

q_n = l2_normalize(q)
d1_n = l2_normalize(d1)
d2_n = l2_normalize(d2)

print("dot without normalization:", q @ d1, q @ d2)
print("dot after normalization:", q_n @ d1_n, q_n @ d2_n)
print("cosine:", q_n @ d1_n, q_n @ d2_n)

预期输出近似为:

dot without normalization: 10.0 2.0
dot after normalization: 0.70710678 1.0
cosine: 0.70710678 1.0

这个例子只验证数学关系,不代表某个具体模型应该使用哪种度量。实际度量应服从模型说明和离线验证结果。

三、维度:不是越大越好,也不是越小越省钱

1. 维度的含义

如果模型输出 dd 维向量,每条向量通常需要:

  • float324d4d 字节;
  • float162d2d 字节;
  • int8:约 dd 字节,具体还依赖量化方案和缩放参数。

原始存储估算为:

memoryN×d×b\text{memory} \approx N \times d \times b

其中:

  • NN 是向量数量;
  • dd 是维度;
  • bb 是每个元素占用的字节数。

例如,100 万条、1536 维、float32 向量,仅原始向量数据约为:

1,000,000×1536×4=6,144,000,0001{,}000{,}000 \times 1536 \times 4 =6{,}144{,}000{,}000

约 6.14 GB,尚未包含索引结构、元数据、副本和数据库开销。

2. 高维度可能带来的收益

更高维度通常提供更大的表示容量,使模型可以编码更多差异,例如:

  • 术语和实体;
  • 语义关系;
  • 句法和局部上下文;
  • 多语言表达;
  • 代码结构或领域概念。

但“维度更高”只说明输出空间更大,不保证模型真正学会了更多有用信息。一个训练不足的高维模型,可能只是增加存储和检索成本。

3. 高维度的代价

维度增大通常会增加:

  • 向量存储;
  • 写入和读取带宽;
  • 相似度计算成本;
  • ANN 索引构建和查询开销;
  • 缓存压力;
  • 量化误差传播的复杂度。

在高维空间中还可能出现“距离集中”现象:不同样本之间的距离相对差异变小,最近邻与普通邻居更难区分。实际程度取决于数据分布和模型训练方式,不能简单地说“维度越高,检索越差”。

4. 降维不是免费的压缩

设原始向量为 xRdx \in \mathbb{R}^{d},通过线性投影降到 kk 维:

z=Wx,WRk×d,k<dz = Wx,\qquad W\in\mathbb{R}^{k\times d},\quad k<d

降维会丢失部分信息。若 WW 是 PCA 得到的投影,它主要保留整体数据方差最大的方向,但检索任务需要保留的可能是低方差的关键区分,例如:

  • “允许”与“不允许”;
  • “2023 年”与“2024 年”;
  • 两个只差一个型号的产品;
  • 代码中的两个相似 API。

因此,不能只用重构误差判断降维是否可用。必须直接测量 Recall@k、MRR 或 nDCG。

5. 截断维度与重新训练维度不是一回事

有些模型支持 Matryoshka Representation Learning 一类的训练方式,使前 kk 个维度本身就具备较强语义能力。此时截断向量可能是模型设计的一部分。

普通模型则不能假设:

short_vector = full_vector[:256]

仍然保留了可靠语义。除非模型文档明确支持,或实验已经证明,否则这只是未经验证的压缩。

6. 维度选型应比较“效果—成本曲线”

合理的实验不是只比较一个分数,而是建立:

(维度, Recall@k, 延迟, 存储成本)(\text{维度},\ \text{Recall@k},\ \text{延迟},\ \text{存储成本})

例如比较 384、768、1536 维时,记录:

  • 同一查询集合上的 Recall@5、Recall@20;
  • P95 查询延迟;
  • 索引构建时间;
  • 每百万条向量的存储成本;
  • 过滤条件下的有效召回率。

如果从 768 增加到 1536 只带来极小的召回提升,却使成本和延迟接近翻倍,那么较低维度可能更适合生产。这个结论必须来自目标数据,而不是维度本身。

四、语言:多语言不是“支持更多字符”

1. 单语言模型与多语言模型

单语言模型通常在某一种语言或相近语言上拥有更强的训练密度。多语言模型则试图将多种语言映射到共享语义空间。

多语言模型至少需要分别验证三种场景:

  1. 中文查询检索中文文档;
  2. 英文查询检索英文文档;
  3. 中文查询检索英文文档,或反过来进行跨语言检索。

第三种场景是跨语言检索,不应由前两种结果推断。

2. 词法切分和语义对齐的影响

中文没有以空格分隔的词边界,日文、韩文、德文复合词、阿拉伯文形态变化也各有特点。模型的分词器可能把同一概念拆成不同数量的 token。

这会影响:

  • 输入长度;
  • 截断风险;
  • 文档切分粒度;
  • 领域术语的完整性;
  • 推理成本。

“支持中文”通常只表示模型可以处理中文输入,不表示它对中文专业术语、数字、表格和混合语言都具有同等质量。

3. 语言错配的典型表现

语言选择不匹配时,可能出现:

  • 中文查询召回大量主题相近但答案语言错误的文档;
  • 中英术语混合时,关键词检索有效而向量检索失效;
  • 专有名词、产品型号、错误码被错误拆分;
  • 跨语言结果只保留主题相似性,丢失具体条件;
  • 同义表达能召回,但否定、时间和数值关系不稳定。

例如查询:

“S3 存储桶在 eu-west-1 是否支持某权限策略?”

模型可能能理解“存储桶”和“权限”,但若它没有充分学习 AWS 服务名、区域名和策略语法,召回结果仍可能停留在一般云存储文档。

4. 语言评测必须分层

一个多语言评测集至少应包含:

测试类型 查询语言 文档语言 检查内容
同语言 中文 中文 中文语义检索
同语言 英文 英文 英文语义检索
跨语言 中文 英文 跨语言对齐
混合术语 中文+英文 中文+英文 产品名、API、错误码
数字约束 中文 中文 日期、金额、版本号
否定条件 中文 中文 “不允许”“除外”等逻辑

如果业务只需要中文,完全可以优先选择中文表现更强的模型;如果需要跨语言知识库,则必须接受跨语言能力可能成为主要成本和误差来源。

5. 语言与混合检索的边界

Embedding 对语义改写有优势,关键词检索对以下内容通常更可靠:

  • 精确产品型号;
  • 错误码;
  • API 名称;
  • 文件路径;
  • 版本号;
  • 法条编号;
  • 人名、地名和专有名词。

因此,在技术文档、代码和企业知识库中,常见组合是:

候选集=向量检索结果BM25/关键词检索结果\text{候选集} = \text{向量检索结果} \cup \text{BM25/关键词检索结果}

然后由重排模型在联合候选集上排序。混合检索不是对 Embedding 失败的简单补丁,而是因为语义匹配和精确匹配本来就是不同的相关性信号。

五、领域:通用语义不等于领域可用

1. 什么是领域适配

领域适配是让模型更适合特定业务语料和检索目标,例如:

  • 医疗指南;
  • 法律法规;
  • 金融研报;
  • 工业维修手册;
  • 企业内部制度;
  • 源代码和 API 文档。

领域适配可能改变以下能力:

  • 术语之间的关系;
  • 缩写与全称的对应;
  • 查询表达与文档表达的匹配;
  • 专业概念的粒度;
  • 领域内正负样本的边界。

2. 先区分“语料领域”和“检索任务”

领域相同,不代表检索任务相同。例如金融文本中可能同时存在:

  • 新闻主题检索;
  • 公司实体检索;
  • 财务指标问答;
  • 条款相似度匹配;
  • 事件时间线检索。

如果训练数据只优化“主题相似”,不一定能解决“查询是否能由文档直接回答”。

更适合 RAG 的正样本通常是:

(q,d+)(q, d^+)

其中 qq 是真实用户问题,d+d^+ 是能提供答案证据的文档片段,而不是仅仅包含相同主题词的文档。

负样本也应有层次:

  • 随机负样本:主题完全不同;
  • 难负样本:主题相同但不能回答问题;
  • 反事实负样本:结论相反;
  • 时间负样本:旧版本与新版本;
  • 权限负样本:内容相关但用户无权访问。

3. 领域微调的基本目标

对比学习常见形式是让正样本相似度高于负样本。以温度参数 τ>0\tau>0 为例:

L=logexp(s(q,d+)/τ)exp(s(q,d+)/τ)+j=1mexp(s(q,dj)/τ)\mathcal{L} = -\log \frac{\exp(s(q,d^+)/\tau)} {\exp(s(q,d^+)/\tau)+ \sum_{j=1}^{m}\exp(s(q,d_j^-)/\tau)}

其中:

  • s(q,d)s(q,d) 是查询和文档向量的相似度;
  • d+d^+ 是正样本;
  • djd_j^- 是第 jj 个负样本;
  • mm 是负样本数量;
  • τ\tau 控制分布的尖锐程度。

训练会推动 s(q,d+)s(q,d^+) 增大,推动负样本相对变小。

但如果负样本错误,训练目标也会错误。例如把“退款政策旧版本”当作负样本,而它实际上对历史查询是正确答案,模型就可能学会错误地压低有价值内容。

4. 领域模型的风险

领域微调可能导致:

  • 通用语言能力下降;
  • 只适应训练部门的术语;
  • 对新产品、新法规和新版本泛化较差;
  • 把高频模板误认为相关证据;
  • 训练数据中的权限边界被错误学习;
  • 数据泄漏,使离线分数虚高。

因此,是否自训模型不能只看领域词汇覆盖,而要看真实检索任务上的增益,以及维护训练集、模型、索引和回滚机制的成本。

六、文档切分是 Embedding 选型的前置变量

Embedding 模型接收的是切分后的片段,而不是抽象的“整篇文档”。同一个模型在不同 chunk 策略下,效果可能差异很大。

1. 片段太短

片段太短会丢失上下文:

“该功能仅对管理员开放。”

如果“该功能”在前一个片段中定义,单独嵌入后,向量无法表达它指代的对象。

2. 片段太长

片段太长会混合多个主题:

安装步骤、权限说明、故障排查、版本变更

查询“如何升级版本”时,向量可能被其他内容稀释;即使命中,送入生成模型的上下文也更嘈杂。

3. 结构化切分优于盲目按字符切分

应尽量保留:

  • 标题层级;
  • 段落;
  • 列表;
  • 表格行列关系;
  • 代码块;
  • 问题与答案的配对;
  • 文档版本和生效时间。

可将父级标题拼入子片段:

标题路径:运维手册 > 数据库 > 备份恢复
正文:恢复前必须停止写入任务……

这样可以增加语义上下文,但会增加 token 数和成本,需要评测其收益。

4. 重叠不是越多越好

相邻片段重叠可以降低边界切分造成的信息丢失,但会增加:

  • 嵌入调用量;
  • 索引规模;
  • 重复召回;
  • 上下文重复;
  • 结果去重难度。

重叠比例应通过“答案证据是否跨边界”来决定,而不是固定套用某个数字。

七、评测:从向量相似度转向检索任务

1. 评测对象至少有三层

Embedding 层:向量是否能区分相关和不相关内容。

Retriever 层:给定查询,正确证据是否出现在 Top-kk 中。

RAG 层:生成模型是否基于证据给出正确、完整、可引用的答案。

不能用生成答案的质量替代检索评测,也不能用一个公开 Embedding 榜单分数替代自己的业务评测。

2. Recall@k

对每个查询 qq,设相关文档集合为 RqR_q,Top-kk 返回集合为 Tk(q)T_k(q)。Recall@k 为:

Recall@k(q)=RqTk(q)Rq\operatorname{Recall@k}(q) = \frac{|R_q\cap T_k(q)|}{|R_q|}

数据集上的平均值为:

Recall@k=1QqQRecall@k(q)\operatorname{Recall@k} = \frac{1}{|Q|} \sum_{q\in Q} \operatorname{Recall@k}(q)

如果每个问题只有一个标注正确片段,Recall@k 就退化为“正确片段是否出现在前 kk 个结果中”。

Recall@k 适合衡量候选召回能力,但不关心正确片段排在第几位,也不关心结果中混入了多少无关片段。

3. MRR

若第一个相关结果出现在排名 rqr_q,则:

RR(q)={1/rq,存在相关结果0,否则\operatorname{RR}(q)= \begin{cases} 1/r_q,& \text{存在相关结果}\\ 0,& \text{否则} \end{cases}

平均倒数排名为:

MRR=1QqQRR(q)\operatorname{MRR} = \frac{1}{|Q|} \sum_{q\in Q}\operatorname{RR}(q)

MRR 强调第一个正确结果,适合单一最佳答案或 FAQ 场景。

4. nDCG

当相关性有等级时,例如:

  • 3:直接、完整回答;
  • 2:部分回答;
  • 1:相关背景;
  • 0:无关;

可以使用 nDCG。对排名位置 ii 的相关性得分 relirel_i,常见 DCG 定义为:

DCG@k=i=1k2reli1log2(i+1)\operatorname{DCG@k} = \sum_{i=1}^{k} \frac{2^{rel_i}-1}{\log_2(i+1)}

再除以理想排序的 DCG,得到:

nDCG@k=DCG@kIDCG@k\operatorname{nDCG@k} = \frac{\operatorname{DCG@k}} {\operatorname{IDCG@k}}

nDCG 能区分“相关但不够回答”和“直接提供证据”,比简单命中更适合复杂知识库。

5. 一个完整的评测算例

假设查询有一个正确片段 D_correct,系统返回:

Top-1: D_related
Top-2: D_correct
Top-3: D_noise

则:

  • Recall@1 = 0;
  • Recall@2 = 1;
  • MRR = 1/2=0.51/2=0.5

如果另一个系统返回:

Top-1: D_correct
Top-2: D_noise
Top-3: D_noise

则:

  • Recall@1 = 1;
  • Recall@2 = 1;
  • MRR = 1。

两个系统的 Recall@2 一样,但第二个系统更适合低延迟 RAG,因为正确证据更早出现。

6. 可执行的离线评估骨架

下面代码不依赖具体 Embedding 厂商,只要求已有查询向量、文档向量和标注结果。它使用点积计算排序,因此调用方必须保证向量处理方式与索引一致。

import numpy as np

def top_k(query_vector, document_vectors, k):
    """
    query_vector: shape [d]
    document_vectors: shape [n, d]
    return: 按点积从高到低排列的文档下标
    """
    scores = document_vectors @ query_vector
    order = np.argsort(-scores)
    return order[:k], scores[order[:k]]

def recall_at_k(ranked_ids, relevant_ids, k):
    return int(bool(set(ranked_ids[:k]) & set(relevant_ids)))

def reciprocal_rank(ranked_ids, relevant_ids):
    relevant_ids = set(relevant_ids)
    for rank, doc_id in enumerate(ranked_ids, start=1):
        if doc_id in relevant_ids:
            return 1.0 / rank
    return 0.0

# 3 个文档,每个向量 2 维
documents = np.array([
    [1.0, 0.0],  # 文档 0:正确证据
    [0.8, 0.2],  # 文档 1:主题相关但不完整
    [0.0, 1.0],  # 文档 2:无关
])

query = np.array([1.0, 0.0])
ranked, scores = top_k(query, documents, k=3)

print("ranked:", ranked.tolist())
print("scores:", scores.tolist())
print("Recall@1:", recall_at_k(ranked, {0}, 1))
print("Recall@2:", recall_at_k(ranked, {0}, 2))
print("MRR:", reciprocal_rank(ranked, {0}))

预期排序为:

ranked: [0, 1, 2]
Recall@1: 1
Recall@2: 1
MRR: 1.0

这段代码只演示指标计算。真实系统还需要处理:

  • 多个正确片段;
  • 同一文档的多个 chunk;
  • 文档版本;
  • 无答案查询;
  • 权限过滤;
  • 混合检索分数融合;
  • ANN 近似搜索与精确搜索的差异。

7. 评测集如何避免虚高

评测集应覆盖真实失败模式,而不是只抽取容易的问题:

  • 术语缩写;
  • 拼写错误;
  • 模糊问题;
  • 多轮上下文;
  • 否定条件;
  • 日期和版本;
  • 表格和代码;
  • 跨语言查询;
  • 文档更新前后;
  • 没有答案的问题;
  • 用户无权访问但语义相关的文档。

应按时间或文档版本切分训练集和测试集,避免同一原文的轻微改写同时出现在两边。若测试问题由文档作者生成,也应检查是否把原句直接复制进问题,从而让模型只需词面匹配。

八、从离线评测到生产检索

1. ANN 只改变搜索近似方式

当向量数量很大时,逐一计算所有文档的距离成本为:

O(Nd)O(Nd)

ANN(Approximate Nearest Neighbor,近似最近邻)索引通过图、倒排分区或量化等结构降低查询成本,但可能牺牲少量召回率。

因此应区分:

  • Embedding 效果:精确搜索下的结果;
  • 索引效果:ANN 相对精确搜索损失多少;
  • 系统效果:过滤、重排、截断和权限逻辑后的结果。

如果模型更换后 Recall 下降,可能是模型问题,也可能是索引参数或过滤执行顺序问题。

2. 权限必须是检索约束,不是 Embedding 能力

向量本身不理解访问控制。一个向量可以表达“这段内容与查询相关”,但不能表达“当前用户是否允许阅读这段内容”。

正确的数据流应至少包含:

用户身份
  -> 权限范围解析
  -> 元数据过滤条件
  -> 向量候选检索
  -> 权限再次校验
  -> 重排与上下文构造

常见安全错误是先取全库 Top-10,再在应用层过滤权限。如果相关的私有文档排在前面,系统可能在过滤后只剩很少结果;更严重时,错误的缓存或日志可能泄漏标题、分数或片段内容。

权限字段应与向量一起版本化管理,例如:

{
  "chunk_id": "doc-123#chunk-04",
  "embedding_model": "model-v2",
  "embedding_version": "2025-01",
  "tenant_id": "tenant-a",
  "acl": ["group-finance"],
  "document_version": "7",
  "valid_from": "2025-01-01"
}

3. 分数阈值不能跨模型直接复用

不同模型、不同归一化方式和不同距离函数产生的分数分布不同。即使都叫“相似度”,也不能直接比较:

模型 A 的 0.78
模型 B 的 0.78

更换模型后,应重新建立:

  • Top-kk 召回基线;
  • 无答案查询的分数分布;
  • 有答案与无答案的分数分布;
  • 阈值与精确率、召回率之间的曲线。

对于拒答型系统,尤其需要验证“没有可靠证据时是否能低分或进入无答案路径”,而不是只优化有答案问题。

4. 文档更新需要增量和回滚状态

Embedding 迁移通常不是覆盖写入这么简单。至少要处理:

  1. 读取原文;
  2. 按新规则重新切分;
  3. 生成新模型向量;
  4. 写入新索引或新命名空间;
  5. 校验向量数量、维度和元数据;
  6. 离线评测;
  7. 灰度切换;
  8. 保留旧索引以便回滚。

在新旧模型同时存在时,禁止把不同模型的向量放入同一个相似度空间中直接检索。即使维度碰巧相同,它们的坐标语义也不同。

九、成本:不仅是每次 API 调用价格

Embedding 成本可拆成四部分。

1. 生成成本

文档首次入库成本近似为:

Cingest=Ttokens×PembeddingC_{\text{ingest}} = T_{\text{tokens}} \times P_{\text{embedding}}

查询成本近似为:

Cquery=Qtokens×PembeddingC_{\text{query}} = Q_{\text{tokens}} \times P_{\text{embedding}}

其中 PembeddingP_{\text{embedding}} 是服务商按 token 或其他计费单位给出的价格。具体价格会变化,应以当前服务文档为准。

2. 存储和索引成本

向量存储成本随 NNdd、数据类型和索引结构增长。不能只计算原始数组,还要包括:

  • ANN 图或分区结构;
  • 元数据;
  • 副本;
  • 快照;
  • 热缓存;
  • 多租户隔离;
  • 新旧索引并存期间的双份空间。

3. 延迟成本

维度、批大小、网络往返、过滤条件和重排候选数都会影响延迟。平均延迟不足以描述生产表现,应至少观察:

  • P50;
  • P95;
  • P99;
  • 大查询和长文本的尾延迟;
  • 索引构建期间的资源争用。

4. 迁移成本

如果模型效果提升很小,却要求全库重嵌入、重建索引、重新校准阈值、重新验证权限和回滚流程,实际总成本可能超过模型调用费用。

因此模型选择应采用总拥有成本:

Ctotal=Cembedding+Cstorage+Cindex+Clatency+Cmigration+CevaluationC_{\text{total}} = C_{\text{embedding}} + C_{\text{storage}} + C_{\text{index}} + C_{\text{latency}} + C_{\text{migration}} + C_{\text{evaluation}}

这不是要求精确预测每一项,而是避免只比较单次向量生成价格。

十、常见误解与诊断方法

误解一:维度越高,语义一定越好

失败表现:存储和延迟显著增加,但 Recall@k 没有提升。

诊断方法:在相同 chunk、相同查询集、相同精确搜索条件下比较不同维度,并绘制效果—成本曲线。

误解二:余弦和点积可以随便替换

失败表现:离线排名与线上排名不同,或长文本被系统性偏好。

诊断方法:抽取同一批查询和文档,分别计算原始点积、归一化点积和余弦,确认索引实际使用的度量。

误解三:多语言模型自动解决跨语言检索

失败表现:中文检索中文正常,中文检索英文很差。

诊断方法:把同一组语义问题分别翻译成多种语言,构造同语言和跨语言矩阵,而不是只测单语言平均分。

误解四:通用模型不需要领域评测

失败表现:通用数据集分数很高,但企业内部术语、版本和权限问题召回失败。

诊断方法:用真实匿名化查询、真实文档结构和领域难负样本做评测,并按问题类型拆分指标。

误解五:检索到文档就等于 RAG 能回答

失败表现:Top-k 中包含正确文档,但生成答案遗漏条件、混淆版本或引用了错误片段。

诊断方法:分别检查:

  1. 正确证据是否进入候选集;
  2. 重排是否把证据置顶;
  3. 上下文是否因截断丢失;
  4. 生成模型是否遵守证据;
  5. 引用是否对应实际片段。

误解六:Embedding 可以替代权限系统

失败表现:用户看到不属于自己的文档标题、摘要或答案片段。

诊断方法:使用无权限但语义高度相关的文档构造安全测试,验证过滤发生在候选暴露之前,并检查缓存键是否包含租户和权限范围。

十一、一个可执行的选型流程

第一步:固定检索任务

明确系统要优化的是:

  • 语义问答;
  • FAQ 匹配;
  • 文档相似度;
  • 代码搜索;
  • 跨语言检索;
  • 多跳证据召回;
  • 精确实体和版本检索。

没有任务定义,模型之间的分数没有可比性。

第二步:固定数据处理

固定并记录:

  • 文档解析器;
  • chunk 规则;
  • 标题拼接规则;
  • 是否重叠;
  • 最大输入长度;
  • 去重策略;
  • 元数据和权限过滤。

否则模型比较结果会混入数据处理差异。

第三步:选择候选模型

候选模型至少按以下维度分组:

  • 单语言与多语言;
  • 通用与领域模型;
  • 不同向量维度;
  • 本地部署与托管 API;
  • 是否支持指定的归一化和距离度量;
  • 是否有稳定版本和迁移方案。

第四步:建立分层评测

先用精确搜索测 Embedding 本身,再单独加入 ANN、过滤、混合检索、重排和生成模型。至少报告:

Recall@1 / Recall@5 / Recall@20
MRR
nDCG@k
无答案误召回率
P50 / P95 延迟
每百万条向量的存储
每次查询和每次入库成本

第五步:进行故障集评测

单独维护失败样本集,包含:

  • 术语和缩写;
  • 否定;
  • 日期和版本;
  • 跨语言;
  • 代码和错误码;
  • 表格;
  • 长文档;
  • 文档边界;
  • 权限冲突;
  • 无答案问题。

新模型必须同时比较总体指标和故障集指标,不能只看平均值。

第六步:灰度与回滚

新模型使用独立索引和独立版本标识。灰度期间记录:

  • 查询向量生成是否成功;
  • Top-k 结果变化;
  • 过滤后剩余候选数;
  • 重排耗时;
  • 用户反馈;
  • 无答案率;
  • 引用正确率。

确认新模型满足效果、成本和安全条件后再切换默认版本;否则通过索引别名或路由配置回滚,而不是重新现场生成旧向量。

十二、最终判断标准

Embedding 模型选型可以归纳为以下因果链:

任务定义数据与切分语言和领域匹配向量维度与距离索引与过滤检索评测RAG 端到端效果\text{任务定义} \rightarrow \text{数据与切分} \rightarrow \text{语言和领域匹配} \rightarrow \text{向量维度与距离} \rightarrow \text{索引与过滤} \rightarrow \text{检索评测} \rightarrow \text{RAG 端到端效果}

其中任何一环不一致,都可能让模型效果被错误解释:

  • 维度决定表示容量,也决定存储和计算成本;
  • 语言决定语义空间是否覆盖目标语言及跨语言关系;
  • 归一化决定点积、余弦和欧氏距离是否等价;
  • 领域决定模型是否理解业务术语和真实检索意图;
  • 评测决定我们看到的是模型能力、索引能力,还是数据泄漏和流程偏差。

一个可用的生产模型,不是公开榜单上分数最高的模型,而是在固定数据处理、固定权限约束和固定索引配置下,能够以可接受的成本稳定把正确证据送进生成模型上下文的模型。


系列导航与关联阅读

官方资料

本文依据研究论文、标准组织与主流框架官方文档重新梳理;正文、示例与工程清单由 WR BLOG 编写。