上一章 RAG 的链路与失效模式 RAG 不是"检索 + 生成"两句话,它是一条有六个环节的链路,每个环节都能独立地把结果做错。这一章把失效模式逐个拆开,并给出定位手段。 IV · 知识引擎 · 第 02 章 · 进阶 · 约 32 分钟
Agent 工程学习站 做棵大树 出品 主站 beatree.cn 免费 · 无需登录 · 进度存本机
IV 版 · 知识引擎 ◆ 第 03 章 ◆ Vectors and Similarity
IV · 知识引擎 03 / 22 进阶

Embedding 与向量检索

Vectors and Similarity
预计阅读 28 分钟
难度 进阶
关键词 Embedding · 余弦相似度
本机状态 未读

语义检索为什么会找错?因为「语义相近」和「对回答问题有用」是两件事。前者是向量空间的几何关系,后者是任务相关性的判断。理解这中间的落差,才能明白向量检索的边界在哪里、什么时候必须叠加别的检索手段。

编辑部注 · 本章的核心是「能力边界」而不是数学。重点是理解为什么纯向量检索在一些场景必然失效。

先把向量检索的物理本质讲清楚:它把文本映射到一个高维空间里的点,让「语义相近的文本」在空间里距离更近。检索就是找最近的邻居。这个机制非常有效,也有非常明确的边界——而多数误用都源于不清楚边界在哪。

一、嵌入做的事:从符号到坐标

文本在计算机里本来是离散符号序列,无法比较「相似」。嵌入(Embedding)模型把它变成连续向量,于是「相似」变成了「距离」这个可计算的东西。

关键在于「相似」是通过什么学到的。模型在大量文本对(相似的、不相似的)上训练,学会了把「意思接近」的文本映射到相近的位置——这就是语义场(Semantic Field)。这带来两个直接结论:像精确标识符(Exact Identifier)(订单号、错误码)差一个字符就是另一个东西,而否定词(Negation Problem)带来的极性反转(Polarity Reversal)在向量空间几乎不产生位移。

嵌入擅长的嵌入不擅长的
同义表达:「转化率低」与「成交比例不高」精确标识符:订单号、错误码、函数名、型号(差一个字符就是另一个东西)
跨语言:「客户满意度」与「customer satisfaction」逻辑关系:「A 大于 B」与「B 大于 A」在向量空间里可能很近
语义概括:「怎么提升留存」与「提高用户复购的方法」否定与极性:「有效」与「无效」在向量空间里往往距离很近
主题归类:一段文字属于哪个业务领域时效与版本:「2024 口径」与「2025 口径」的向量几乎相同
否定词问题是向量检索最经典的失效模式

「这个方案有效」和「这个方案无效」在向量空间里通常非常接近,因为模型学到的主要是「方案 + 有效性评估」这个主题,而不是那个「否」字带来的极性反转。类似的还有「同比增长」与「同比下降」、「支持」与「不支持」。

后果很严重:用户明确问「哪些功能不支持 X」,检索回来的全是「支持 X」的文档。解法不是换嵌入模型(这是共性局限),而是在查询理解阶段把「不支持」改写为「缺少 / 未提供 / 不兼容」等正向表述,或者辅以关键词检索把「不支持」这个精确串匹配上。

二、相似度度量:怎么选

选哪个度量不看直觉,看训练:相似度度量选择(Metric Selection)取决于嵌入模型怎么训的。余弦相似度(Cosine Similarity)是默认;若模型用点积则必须用点积(未归一化时长向量会被高估),欧氏距离(Euclidean Distance)在归一化(L2 Normalization)后与余弦等价。

度量公式直觉何时用注意
余弦相似度只看两个向量的夹角,忽略长度默认选择,绝大多数文本嵌入归一化之后与点积等价
点积夹角 + 长度都算模型训练时就用的点积(如部分 OpenAI 模型)必须与训练时一致,否则排序会系统性偏移
欧氏距离空间中的直线距离向量已归一化时与余弦等价(单调对应)未归一化时对长度敏感,容易偏向短文本
一条实用纪律

使用哪个度量,取决于嵌入模型是怎么训练的,而不是取决于你的直觉。模型卡(model card)通常会写明推荐度量。用错了不会报错,只会让排序质量悄悄变差——这类问题极难发现,因为结果「看起来还算相关」。

验证方法:取 20 组已知的相关/不相关样本对,分别用两种度量算排序,看哪个更符合预期。这个测试大概花 20 分钟,能省下后面大量的排查时间。

三、能力边界:三类必然失效的场景

向量检索的能力边界 这三类场景不是「调参能解决」的,而是机制上的必然失效。识别出来,才能选对补救手段。 ① 精确匹配类 需要一字不差 查询:「ERR_45004 是什么原因」 期望:定位到报错码对应的文档条目 向量会把它匹配到讨论「错误处理」「异常码规范」的泛泛文档,而 ERR_45004 那条可能排在 50 名开外。 补救:关键词/BM25 检索精确串匹配,再与向量结果融合(见下一章)。 同类:订单号、函数名、配置项名、产品型号、人名、法律条文编号。 ② 极性 / 逻辑反转类 词几乎相同但含义相反 查询:「哪些接口不支持批量操作」 期望:只返回不支持的那些 「支持批量操作」与「不支持批量操作」的向量距离极近,检索会把两者混在一起。 补救:查询改写为正向表述(「缺少批量能力」),或用否定词规则在召回后做二次判别。 同类:有效/无效、同意/不同意、同比增长/同比下降、包含/不包含。 ③ 条件 / 数值约束类 信息在结构化字段里,不在语义里 查询:「2025 年 9 月之后更新的华东区口径文档」 期望:三个条件同时满足 向量只能表达「华东区口径文档」这个语义主题,无法表达「时间 > 2025-09」这个约束。 补救:条件抽出来做元数据过滤(结构化过滤下推到召回层),向量只负责语义部分。 同类:时间范围、版本号、状态、地区、金额区间、部门归属。
图 1 三类必然失效的场景与补救手段。共同规律是:失效都发生在「需要精确或结构化判断」的地方,而向量只在「语义模糊匹配」上强。所以正确的架构不是选一种,而是让每种手段处理它擅长的部分。
把句子放进向量空间:该分开的没分开 圆点之间的距离代表语义距离(示意,非真实尺度);右侧框内是这一对句子的余弦相似度 ① 极性对 · 词几乎相同,含义相反 接口支持批量操作 接口不支持批量操作 cos ≈ 0.92 几乎重合 两句共享「批量操作 × 支持性」这个主题,那个「不」字几乎不产生位移。 ② 极性对 · 数值方向相反 同比增长 12% 同比下降 12% cos ≈ 0.90 几乎重合 句子里唯一的不同是方向词,而方向词造成的位移最小。 ③ 精确串 · 差一个字符就是另一个东西 ERR_45004 参数超长 常见的错误处理规范 cos ≈ 0.31 相距很远 精确串与主题文档离得远,在向量里查它必然排不上来,只能靠关键词兜住。 所以「语义相近」和「能回答我的问题」是两件事:前者是几何关系,后者是任务判断。 对策都不在换模型:极性靠查询改写(不支持 → 缺少 / 未提供),精确串靠关键词检索。
图 2 三组对照放在一起看:越该被区分的句子,在向量空间里往往挨得越近;越需要一字不差的串,反而离得越远。极性反转与精确标识符是机制性的失效,换任何通用嵌入模型都躲不掉。

四、动手:三种度量的对比与边界探测

embedding_bounds.pypython
import numpy as np
def l2_normalize(m: np.ndarray) -> np.ndarray:
    n = np.linalg.norm(m, axis=-1, keepdims=True)
    return m / np.where(n == 0, 1, n)
def cosine(a: np.ndarray, b: np.ndarray) -> np.ndarray:
    return l2_normalize(a) @ l2_normalize(b).T
def dot(a: np.ndarray, b: np.ndarray) -> np.ndarray:
    return a @ b.T
def euclidean(a: np.ndarray, b: np.ndarray) -> np.ndarray:
    # 返回负距离,便于与相似度统一为「越大越好」
    diff = a[:, None, :] - b[None, :, :]
    return -np.linalg.norm(diff, axis=-1)
METRICS = {"cosine": cosine, "dot": dot, "euclidean": euclidean}
def compare_metrics(query: str, docs: list[str], embed) -> None:
    """
    用一组已知样本对比三种度量的排序差异。
    20 分钟的成本,能避免后面几个月的排序质量问题。
    """
    qv = embed([query])
    dv = embed(docs)
    print(f"查询:{query}\n")
    for name, fn in METRICS.items():
        scores = fn(qv, dv)[0]
        order = np.argsort(-scores)[:5]
        print(f"--- {name} top5 ---")
        for i in order:
            print(f"  {scores[i]:+.4f}  {docs[i][:44]}")
    print()
def probe_polarity_and_exactness(embed) -> dict:
    """
    边界探测:亲手验证两类必然失效。
    这一步的目的是建立「什么时候不能只靠向量」的判断力。
    """
    pairs = [
        # (A, B, 期望:两者是否应该被区分开)
        ("接口支持批量操作", "接口不支持批量操作"),
        ("ERR_45004 参数超长", "常见的错误处理规范"),
        ("同比增长 12%", "同比下降 12%"),
        ("该字段可以为空", "该字段不可以为空"),
        ("华东区转化率 1.1%", "华东区转化率偏低,建议关注"),
    ]
    out = {}
    for a, b in pairs:
        va, vb = embed([a, b])
        out[f"{a[:14]}… vs {b[:14]}…"] = float(cosine(va, vb)[0, 0])
    return out
# --- 输出示例(数值会随模型不同而变,但规律稳定) ---
# 「支持批量操作」vs「不支持批量操作」            → 约 0.92  ← 距离极近,无法区分极性
# 「ERR_45004 参数超长」vs「常见的错误处理规范」   → 约 0.31  ← 精确串反而更远
# 结论:极性反转与精确标识符,必须靠向量之外的手段处理。
def hybrid_score(vec_score: np.ndarray, kw_score: np.ndarray,
                  alpha: float = 0.6) -> np.ndarray:
    """
    一个极简的混合打分示意。
    真实系统用 RRF 或带权重归一化的加权和(见下一章),
    这里只为说明「两路信号可以互补」。
    """
    v = (vec_score - vec_score.min()) / (vec_score.ptp() + 1e-9)
    k = (kw_score - kw_score.min()) / (kw_score.ptp() + 1e-9)
    return alpha * v + (1 - alpha) * k
同一组向量,三种度量给出三种答案 三行用的是同一组向量:b 与 a 成夹角 θ,c 与 a 同方向、长度为 a 的两倍 a c = 2a b ① 余弦相似度 只看夹角,不看长度 cos(a,b) 与 cos(c,b) 完全相同:方向一样,长度翻倍也不影响分数。 默认选择;向量归一化之后,它与点积等价。 a c = 2a b ② 点积 夹角 × 长度 c·b = 2 × a·b:同样的夹角,长向量拿到更高的分数。 必须与嵌入模型训练时用的度量一致,否则排序会系统性偏移。 a c = 2a b ③ 欧氏距离 直线距离,长度也算进去 |c−b| 明显大于 |a−b|:方向一样但更长,距离反而更远。 归一化后与余弦单调对应;未归一化时容易偏向短文本。 L2 归一化的意义就在这里:把「长度」这一维从打分里去掉,三种度量收敛成同一套排序。 所以「归一化 + 点积」是最省算力的等价写法;用错度量不会报错,只会让排序悄悄偏掉。
图 3 三个面板用的是同一组向量,只换度量。余弦对长度免疫、点积放大长度、欧氏距离把长度算进距离——这就是「用哪个度量要跟嵌入模型训练时保持一致」的全部原因。
实操任务
  • 跑一遍 probe_polarity_and_exactness(),亲眼看「支持/不支持」的余弦相似度有多高——这个数字会改变你对向量检索的信任度;
  • 用你自己的语料构造 10 组「应该被区分开但向量分不清」的样本,统计需要用到关键词检索的比例;
  • 回答一个问题:如果你的检索系统里 30% 的查询属于这三类失效场景,你应该优先改哪一环?

五、常见误区与追问

5.1 误区:向量检索比关键词检索「更高级」,应该全面替换

错在哪:把两种检索当成新旧替代关系。为什么自然:语义匹配更接近人理解语言的方式,听起来就是升级版。但它们擅长的是不同的东西:向量赢在同义与跨语言,关键词赢在精确串、编号与否定词。 判据(可操作):统计自己系统里「必须一字不差才能命中」的查询占比——订单号、错误码、型号、函数名、条文编号这类。只要这个比例超过 10%,纯向量就会留下一批必错的查询,必须上混合检索。反过来说,如果 90% 的查询都是口语化长问题,纯粹为了「更全」而加一路关键词,收益也有限。

5.2 误区:相似度分数是绝对可信度,0.8 就是相关

错在哪:把余弦值当成跨模型通用的阈值。为什么自然:分数落在 −1 到 1 之间,看起来是标准化的。但分数的分布会随嵌入模型、语料领域、甚至文本长度而变:同一个 0.78,在这个模型里是「高度相关」,在另一个模型里可能只是「同一主题」。 判据:取 20 组已知相关、20 组已知不相关的样本,分别看两批分数的分布边界,用它们之间那条空隙来定阈值;更换嵌入模型后必须重标一次。这也是 边界探测(Boundary Probe) 值得花 20 分钟的原因——它把「感觉还行」换成一条能复现的线。

5.3 误区:归一化只是为了算得快

错在哪:把 L2 归一化当成性能优化。为什么自然:归一化之后点积可以替代余弦,确实省一次除法。但它真正的作用是让「长度」这一维退出打分,从而让余弦、点积、欧氏距离三种度量收敛到同一套排序(见 图 3)。 判据:如果你发现「长文档总是排得更高」或者「同一段话复制两遍分数变高」,先检查两件事——向量有没有归一化、嵌入模型训练时用的是点积还是余弦。这两项对不上时,排序会系统性偏好长文本,而且不会报任何错。

5.4 误区:否定词问题只能靠换更强的嵌入模型解决

错在哪:把它当成模型能力不足。为什么自然:模型越强似乎越该懂「不」字的含义。但这是嵌入在编码「主题」而不是「极性」的机制性结果,换通用模型同样存在。 判据:跑一次边界探测,若「支持批量操作 / 不支持批量操作」的余弦仍然在 0.85 以上,就不要再指望换模型。正确做法有两条:查询理解阶段把否定改写成正向表述(不支持 → 缺少 / 未提供 / 不兼容),或者叠加一路关键词分做 混合打分(Hybrid Score)。经验上,改写后这类查询会从「基本不命中」变成「命中一半以上」——量级差别,不是几个百分点。

5.5 误区:向量维度越高越好

错在哪:把维度当成容量指标,似乎越高能装的信息越多。为什么自然:维度确实意味着表达能力,而升级到更高维是「只改一个配置」的动作。但维度是嵌入模型出厂固定的,它由训练方式决定,不由你的语料决定。 判据:换模型时同时记录三项——检索指标、索引体积、单次查询延迟。很多 384 或 768 维的模型在同义匹配上并不输 1536 维,而索引体积与延迟差 2 到 4 倍。把「维度翻倍」当成优化目标,最常见的结局是成本涨了、指标没动。

六、自测

本章自测第 2 题是经典考点
Q1用哪种相似度度量,主要取决于什么?
A。用错度量不会报错,只会让排序质量悄悄变差——这类问题极难发现,因为结果「看起来还算相关」。验证方式:取 20 组已知相关性的样本对,对比不同度量的排序是否符合预期。
Q2为什么向量检索难以区分「支持批量操作」与「不支持批量操作」?
C。这是机制性的局限,不是模型不够好——换任何通用嵌入模型都会遇到。所以对策必须来自向量之外:查询改写(把否定转成「缺少/未提供」的正向表述)或叠加关键词检索对否定词做精确匹配。
Q3查询「2025 年 9 月之后更新的华东区口径文档」,向量检索的正确分工是什么?
B。结构化的条件(时间、版本、地区、状态)应该交给元数据过滤,语义部分才交给向量——让每种机制处理它擅长的东西。A 的问题是把数值约束交给了一个无法表达它的机制;D 就是「后过滤」,会因 top-k 额度被占满而导致可用结果过少。

七、小结

议题结论
嵌入的本质把语义相近映射为距离相近;编码的是「语义场」而非精确信息
度量选择依据嵌入模型的训练方式(查模型卡),并用已知样本对比验证
三类失效精确匹配类、极性反转类、条件约束类——都是机制性失效
应对方向精确串用关键词检索;极性用查询改写;条件用元数据过滤下推
一条纪律不要指望换嵌入模型解决边界问题;要靠混合手段

向量检索不是「更好的搜索」,它是「另一种搜索」。它的价值在于补上了关键词检索找不到的语义相近,代价是丢掉了关键词检索最擅长的精确。本刊编辑部

最后一章把关键词检索与向量检索拼在一起,解决「召回要宽、排序要准、引用要能被验证」这三件事。

八、参考与延伸

这一章的重点是「边界」而不是数学,所以下面三份材料按「先读文档、再看中文代码、最后读论文」排好了。全站不做原文转载,这里只登记链接与「为什么值得读」。

先读文档(把度量和归一化的关系钉死)

  • Sentence Transformers · Semantic Textual Similarity —— 官方文档里 similarity_fn_name 的取值就是 cosine / dot / euclidean 三种,并且明确写着「模型末尾带归一化层时应该选 dot,它比 cosine 更快且等价」。本章第二节那张表的判据,在这页文档里就是一行配置——想确认「用哪个度量」到底由谁决定,看这一段。

再看中文代码(把相似度算一遍)

  • 动手学深度学习(中文,zh.d2l.ai) —— 第 14 章的词嵌入部分把「向量、相似度、近似训练」讲成了可跑的代码。把余弦相似度那一节敲一遍,再回过头看本章的边界探测,会发现「支持 / 不支持分数很高」是可复现的,而不是玄学。

最后读论文(对齐一手定义)

◇ 面试官会怎么问

共 6 条 · 其中 2 条高频 · 先自己答一遍,再展开对照

语义检索为什么会找错? 高频
根本原因是:"语义相近"和"对回答问题有用"是两件不同的事。
四类具体错法:
① 语义相似但答案无关——查询"如何申请年假"召回《请假制度总则》的相似段落,但真正有用的是《年假实施细则》,两者语义高度接近。
② 精确标识失效——向量对数字、工号、型号、专有名词不敏感,"型号 A123" 和 "型号 A132" 在向量空间里几乎没区别,但答案完全不同。
③ 否定与条件丢失——"不含"、"除……之外"这类逻辑关系在稠密向量里表达很弱。
④ 频次与时效被稀释——出现 20 次的关键条款,可能比只出现 1 次的临时通知排在更前,但后者才是当前有效的版本。
所以纯向量检索在真实系统里几乎总是配合关键词检索(补精确匹配)+ 元数据过滤(补时效与权限)+ Rerank(补真实相关性)使用。
余弦相似度、点积、欧氏距离该怎么选?
取决于向量是否归一化:
向量已归一化(模长为 1)——余弦相似度与点积完全等价,此时用点积更快(省掉除法)。大多数现代 embedding 模型输出的向量本身就是归一化的,所以实践中常见"用点积近似余弦"。
向量未归一化——点积会把模长也计入相似度,如果向量模长本身有含义(例如代表置信度或频次),这可能是特性;如果没有含义,那就是噪声,应该先归一化。
欧氏距离——在归一化向量上,欧氏距离与余弦相似度是单调等价的(d² = 2 − 2·cos),所以排序结果相同。它的好处是直觉上更好解释,坏处是对模长变化更敏感。
工程上的建议是:跟你的向量库与索引实现保持一致——很多 ANN 库针对特定度量做了优化(比如内积),混用不同度量的预计算会导致结果与暴力检索不一致,这种 bug 很难发现。
向量检索的能力边界在哪?什么场景不该用它?
不该用(或必须配合其他手段)的场景有四类:
① 需要精确匹配——编号、单号、金额、日期范围、代码符号。这些必须走结构化查询或关键词精确匹配。
② 需要聚合与统计——"上个月有多少人入职"。向量检索只能找相似文本,不能做聚合,这本质上是数据库问题。
③ 需要严格权限与合规——向量检索没有天然的权限语义,必须在检索前做元数据过滤(pre-filter),而不是检索后再筛(否则会泄漏被过滤内容的存在性,也浪费召回额度)。
④ 需要多跳推理——"和 A 合作过项目的人,他们的共同上级是谁"。这是图查询擅长的事,向量检索只能拿到与 A 相关的文本片段。
一句话总结:向量检索擅长"找相似的说法",不擅长"精确匹配、聚合、关系推理、权限控制"——所以真实系统里它只是检索层的一个组件,而不是全部。
加分点:把"pre-filter 而非 post-filter"这一点讲出来,会显得你真做过带权限的检索系统。
用户问「哪些功能不支持批量操作」,检索回来的却全是「支持批量操作」的文档。你会怎么修,为什么不建议先换嵌入模型? 高频
根因是极性反转在向量空间里几乎不产生位移:嵌入主要编码主题(批量操作 × 支持性),那个「不」字带来的差异很小,实测余弦相似度常在 0.9 上下。
这是机制性局限,换任何通用嵌入模型都会遇到,所以要修的地方在向量之外:
  • 查询理解阶段把否定改写成正向表述(不支持 → 缺少、未提供、不兼容);
  • 再叠一路关键词检索,让「不支持」这个精确串能被直接匹配上。
判据:先跑一次边界探测,若「支持 / 不支持」的余弦仍在 0.85 以上,就不要指望换模型解决。
经验上,改写之后这类查询会从「基本不命中」变成「命中一半以上」,是量级差别。

追问链

  1. 「同比增长 12%」与「同比下降 12%」属于哪一类失效?
  2. 精确标识符(订单号、错误码、函数名)应该怎么检索?
加分点:加分点:能说出这是「模型编码主题、不编码极性」的机制结果,并给出查询改写与混合检索两条具体对策。
同一批向量、同一批候选,换成点积打分之后,长文档的名次普遍上升。原因是什么,怎么处理?
点积同时算夹角与向量长度,长度翻倍会让分数翻倍:c 与 a 同方向、长度为两倍时,cos(c,b) 与 cos(a,b) 完全相同,但 c·b 是 a·b 的两倍。
所以未归一化时用点积(或欧氏距离)会系统性偏好长文本。
处理分两步:确认嵌入模型训练时用的是哪个度量(查模型卡)并保持一致;如果模型走余弦,就把向量做 L2 归一化之后改用点积——两者等价,而且更快。
判据:一旦出现「长文档总是排得更高」或者「同一段话复制两遍分数变高」,先查归一化与训练度量这两项。

追问链

  1. L2 归一化除了省算力,真正的语义作用是什么?
  2. 欧氏距离在向量归一化之后,与余弦是什么关系?
加分点:加分点:能说清「归一化让三种度量收敛成同一套排序」,而不是只答「归一化更快」。
团队想把嵌入模型从 384 维换成 1536 维,期望检索指标明显提升。作为评审你会问哪几个问题?
先问三件事:
  • 现在的失败样本里,有多少属于「语义该匹配但没匹配上」?如果失败集中在精确串、否定词、条件约束这三类,换维度完全无用——它们是机制性失效。
  • 换模型意味着索引要全量重建,存储与延迟的预算给了吗?很多 384 或 768 维模型在同义匹配上并不输 1536 维,而索引体积与延迟差 2 到 4 倍。
  • 相似度阈值有没有跟着重标?分数分布会随模型改变,旧阈值会失效。
结论:维度是嵌入模型出厂固定的属性,不是一个可以拧的旋钮;把「维度翻倍」当优化目标,最常见的结果是成本涨了、指标没动。

追问链

  1. 换嵌入模型之后,哪些东西必须一起重建或重标?
  2. 如果失败样本集中在精确匹配类,你会先改哪一环?
加分点:加分点:能把「换模型」拆成指标预期、成本预算、阈值重标三件事来评估,而不是凭直觉反对。
答这类题的通用结构

结论先行(一句话给出取舍)→ 机制(为什么是这样,涉及哪条链路)→ 代价或边界(这么做放弃了什么)→ 你的实践或数字(真实场景里怎么落地)。 只讲机制不讲代价,是背题;只讲取舍没有数字,是空谈。

本章收尾 · Wrap up
掌握度自评
点一下给自己打分;低于 3 分建议加入复习队列
个人笔记 · Notes
    下一章 混合检索、Rerank 与知识可信 召回要宽、排序要准、引用要能被点开验证。这一章把关键词检索与向量检索拼成一条混合链路,用 Rerank 做精排,最后解决"凭什么相信这条知识"的问题。 IV · 知识引擎 · 第 04 章 · 深入 · 约 34 分钟