文章

LLM Embedding 距离度量治理生产实战:用归一化契约、Metric Matrix 与向量回放避免召回失真

向量检索中,模型、归一化方式与距离度量一旦错配,系统通常不会报错,却会悄悄改变排序与阈值。本文给出归一化契约、度量矩阵、黄金向量回放和迁移门禁,帮助团队在模型升级与索引切换时稳定召回质量。

为什么距离度量错配是一类静默故障

Embedding 检索链路通常由四部分组成:Embedding 模型、向量预处理、索引度量和业务阈值。这四部分只要有一个环节发生变化,系统可能依然能够正常写入、正常查询,也不会抛出异常,但 Top-K 排序、召回边界和阈值通过率已经改变。

常见故障包括:

  • 建库时使用 Cosine,查询时按 Dot Product 理解分数;
  • 只对 Query 做 L2 Normalize,库内历史向量没有归一化;
  • 从一个返回”越大越相似”的系统迁移到返回”越小越相似”的系统,却继续使用旧阈值;
  • Embedding 模型升级后,维度没有变化,但向量范数分布和语义空间已经变化;
  • 从精确检索切换到 HNSW、IVF 或量化索引后,把近似误差误判为模型质量退化。

这类问题危险之处在于:接口成功率、延迟和错误率可能全部正常,只有召回结果在悄悄漂移。

三种常见度量到底有什么区别

设查询向量为 q,文档向量为 x

Cosine Similarity:只比较方向

cos(q, x) = (q · x) / (||q|| × ||x||)

Cosine 关注两个向量的方向是否一致,弱化向量长度的影响。它常用于文本语义检索,但前提是模型训练方式和线上实现与该度量匹配。

Dot Product:方向和长度共同参与

dot(q, x) = q · x

Dot Product 不会自动消除范数影响。若向量长度携带置信度、流行度或其他训练信号,Dot Product 可能有意义;若模型和系统原本假设单位向量,未归一化的 Dot Product 则可能让大范数文档获得不合理优势。

L2 Distance:比较空间中的绝对距离

L2(q, x) = ||q - x||

L2 越小表示越接近。部分系统返回 L2,部分返回平方 L2;两者排序相同,但阈值数值不能混用。

qx 都是单位向量时:

||q - x||² = 2 - 2 × (q · x)

因此,在单位归一化前提下,Cosine、Inner Product 和 L2 可以得到等价排序。但 “排序等价”不代表”分数相同”,更不代表旧阈值可以原样迁移。

把 Embedding 当成有版本的检索契约

生产系统不应只记录 model_name。建议为每套向量建立不可变的 Embedding Contract

embedding_contract:
  model_id: text-embedding-model
  model_revision: 2026-07-01
  dimension: 1024
  dtype: float32
  normalization: l2_unit
  metric: cosine
  score_semantics: higher_is_better
  index_type: hnsw
  index_build_version: v17
  preprocessing_version: text-clean-v4

至少需要固化以下字段:

字段说明
模型与修订版本不能只写一个会漂移的别名
向量维度和数据类型用于阻止不同空间误写
归一化策略noneL2 Unit、模型内置归一化需要明确区分
距离度量Cosine、Dot/IP、L2
分数语义Higher Is Better 还是 Lower Is Better
预处理版本大小写、Unicode、切块、前缀模板都会改变向量
索引与量化配置用于解释近似检索误差

写入端、查询端和离线评测端必须引用同一个 Contract ID。发现 Contract 不一致时,应拒绝请求,而不是自动猜测。

建立 Metric Matrix,统一不同引擎的分数语义

不同向量引擎对同一概念的命名和返回值并不一致。例如:

业务语义常见实现排序方向注意事项
Cosine Similaritycosine 或归一化后 IP越大越相似有的系统返回 1 - cosine
Inner ProductIP、Dot越大越相似pgvector 的排序操作符使用负内积
Euclidean DistanceL2、Euclid越小越相似有的实现返回平方 L2

建议在业务层建立统一分数对象,而不是直接透传数据库的 score

from dataclasses import dataclass
from enum import Enum

class ScoreDirection(str, Enum):
    HIGHER_IS_BETTER = "higher_is_better"
    LOWER_IS_BETTER = "lower_is_better"

@dataclass(frozen=True)
class RetrievalScore:
    raw_score: float
    engine: str
    metric: str
    direction: ScoreDirection
    contract_id: str

    def passes(self, threshold: float) -> bool:
        if self.direction is ScoreDirection.HIGHER_IS_BETTER:
            return self.raw_score >= threshold
        return self.raw_score <= threshold

更稳妥的做法是:业务规则始终基于明确的 Contract 和 Score Direction 判断,不把来自不同模型、不同索引或不同引擎的裸分数直接比较。

用黄金向量回放定位到底是哪一层变了

上线前应准备一套 Golden Vector Replay Set。它不是只有”查询—相关文档”标签,还应包含固定向量对和人工计算结果。

每条回放记录至少包括:

  • 原始 Query 和 Document 文本;
  • 固定版本的 Query Vector 和 Document Vector;
  • 两侧向量范数;
  • 手工计算的 Cosine、Dot 和 L2;
  • 精确 Top-K 基线;
  • 业务阈值判断结果;
  • ANN 索引返回的 Top-K 和原始分数。

建议分三层执行回放。

第一层:数学探针

直接对少量固定向量手工计算三种度量,检查数据库返回值是否符合预期:

import numpy as np

def metric_probe(query: np.ndarray, target: np.ndarray) -> dict[str, float]:
    if query.ndim != 1 or target.ndim != 1:
        raise ValueError("vectors must be one-dimensional")
    if query.shape != target.shape:
        raise ValueError("vector dimensions do not match")
    if not np.isfinite(query).all() or not np.isfinite(target).all():
        raise ValueError("vectors contain NaN or Infinity")

    q_norm = np.linalg.norm(query)
    x_norm = np.linalg.norm(target)
    if q_norm == 0 or x_norm == 0:
        raise ValueError("zero vector cannot be used for cosine similarity")

    dot = float(np.dot(query, target))
    cosine = float(dot / (q_norm * x_norm))
    l2 = float(np.linalg.norm(query - target))

    return {
        "query_norm": float(q_norm),
        "target_norm": float(x_norm),
        "dot": dot,
        "cosine": cosine,
        "l2": l2,
    }

这一层可以快速发现 Score 取反、平方距离、只归一化一侧等基础错误。

第二层:精确检索与 ANN 对照

使用 Flat/Brute-Force 索引生成精确 Top-K,再与 HNSW、IVF、PQ 或量化索引对比。重点观察:

指标说明
Recall@KANN 召回精确 Top-K 的比例
Top-K 交集率精确与近似检索的结果重合度
排名相关性排序顺序是否一致
阈值附近翻转率边界样本是否因近似误差而误判
分桶差异不同 Query 长度、语言和领域的差异

这样可以把”度量契约错误”和”ANN 参数不足”分开。

第三层:业务查询回放

对黄金查询集运行完整链路,记录:

  • 首阶段召回文档;
  • 进入 Reranker 的候选集;
  • 最终答案引用文档;
  • 无结果率和阈值拒绝率;
  • 不同业务分桶的 Recall 与延迟。

不要只看总体平均值。距离度量错配往往先影响长尾、短文本、专有名词或阈值边缘样本。

模型升级和索引迁移如何安全发布

1. 新旧空间必须物理隔离

不同模型版本、不同归一化规则或不同度量不应写入同一个 Vector Field。建议使用:

embedding_v1_cosine
embedding_v2_cosine
embedding_v2_dot

或者直接使用独立 Collection/Index。不要依赖一列 model_version 在查询时临时过滤来维持空间兼容性。

2. 双写并进行影子查询

迁移期间同时生成旧向量和新向量,线上请求继续使用旧索引,新索引执行 Shadow Query。比较:

  • Top-K Overlap;
  • 新增与丢失文档;
  • 分数分布和范数分布;
  • 原阈值与新阈值的通过率;
  • 业务黄金集的 Recall 和错误案例。

3. 阈值必须重新校准

即使排序基本一致,Score 数值也可能改变。应在标注集上重新选择阈值,并区分:

阈值用途说明
是否进入 Reranker粗筛阶段的召回边界
是否判定”没有相关文档”触发拒答或降级策略
是否触发人工复核置信度不足时引入人工
是否允许直接回答高置信度自动应答边界

阈值应绑定 embedding_contract_id + index_version,不能作为全局常量。

4. 灰度切换必须可回滚

灰度维度可以是租户、业务域、语言或 Query Hash。回滚时必须同时恢复:

  • Embedding 模型;
  • 归一化规则;
  • 索引别名;
  • 分数适配器;
  • 阈值版本。

只回滚模型而保留新阈值,同样会造成召回异常。

适用场景

这套治理方法尤其适合:

  • 从 Faiss 迁移到 Milvus、Qdrant、pgvector 等系统;
  • 更换 Embedding 模型或输出维度;
  • 从 Cosine 切换到 Dot Product;
  • 引入向量量化、HNSW 或 IVF;
  • 多供应商 Embedding 共存;
  • 检索阈值直接影响拒答、自动化操作或合规判断的系统。

常见误区

误区一:Cosine 和 Dot Product 永远等价

只有两侧向量都单位归一化时才等价。模型是否已内置归一化,也必须通过实际向量范数验证,不能靠名称猜测。

误区二:维度一样就能混用

两个模型都输出 1024 维,不代表它们处于同一个语义空间。跨模型直接比较向量通常没有意义。

误区三:迁移后 Top-1 看起来相同就算成功

Top-1 相同可能掩盖候选集变化。Reranker、引用生成和阈值判断依赖整个 Top-K,应检查排序和边界样本。

误区四:Reranker 会修复所有召回问题

Reranker 只能重排已经召回的候选。如果度量错配导致相关文档没有进入候选集,后续模型无法补救。

误区五:把数据库 Score 直接暴露给业务

不同引擎对 Score 的定义可能相反。必须经过 Score Adapter,并附带 Metric、Direction 和 Contract Version。

上线检查清单

  • 模型 ID、Revision、维度、Dtype 已固化
  • Query 与 Document 使用相同的预处理和归一化规则
  • Index Metric 与 Query Metric 完全一致
  • 明确 Score 是 Similarity 还是 Distance
  • 手工向量探针通过
  • 精确检索与 ANN Recall 达标
  • 黄金查询集完成 Top-K 与阈值回放
  • 新旧向量范数分布已比较
  • 新阈值绑定新 Contract 和 Index Version
  • 双写、影子查询、灰度和回滚路径可用
  • 监控包含 Contract Mismatch、Zero Vector、NaN/Inf 和 Dimension Error
  • 禁止不同 Embedding Space 混写同一索引

参考资料

  1. Faiss — MetricType and distances
  2. Sentence Transformers — Semantic Textual Similarity
  3. Qdrant — Collections and Distance Metrics
  4. Qdrant — Diagnosing Migration Discrepancies
  5. Milvus — Metric Types
  6. pgvector — Vector Similarity Search for PostgreSQL

常见问题

Cosine 与 Dot Product 可以直接互换吗?
只有查询向量和库内向量都进行单位长度归一化时,两者的排序才等价。只归一化一侧,或模型输出范数携带业务信息时,结果会发生变化。
更换向量数据库后,原来的相似度阈值还能继续使用吗?
通常不能直接沿用。不同系统可能返回相似度、距离、负内积或平方L2,分数方向和数值范围也可能不同,应通过黄金向量回放重新校准。
只修改距离度量,是否一定需要重新建索引?
取决于索引实现。度量类型常与HNSW、IVF等索引构建绑定;若同时改变归一化规则,应对库内全部向量一致处理,并通常通过新索引双写迁移。
模型升级后维度没变,还需要做度量治理吗?
需要。维度相同不代表语义空间相同。向量范数分布、归一化行为和分数数值都可能变化,仍应执行归一化契约比对、黄金向量回放和阈值重校准。