语义检索为什么会找错?因为「语义相近」和「对回答问题有用」是两件事。前者是向量空间的几何关系,后者是任务相关性的判断。理解这中间的落差,才能明白向量检索的边界在哪里、什么时候必须叠加别的检索手段。
编辑部注 · 本章的核心是「能力边界」而不是数学。重点是理解为什么纯向量检索在一些场景必然失效。
先把向量检索的物理本质讲清楚:它把文本映射到一个高维空间里的点,让「语义相近的文本」在空间里距离更近。检索就是找最近的邻居。这个机制非常有效,也有非常明确的边界——而多数误用都源于不清楚边界在哪。
一、嵌入做的事:从符号到坐标
文本在计算机里本来是离散符号序列,无法比较「相似」。嵌入(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 分钟,能省下后面大量的排查时间。
三、能力边界:三类必然失效的场景
四、动手:三种度量的对比与边界探测
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- 跑一遍
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 倍。把「维度翻倍」当成优化目标,最常见的结局是成本涨了、指标没动。
六、自测
七、小结
| 议题 | 结论 |
|---|---|
| 嵌入的本质 | 把语义相近映射为距离相近;编码的是「语义场」而非精确信息 |
| 度量选择 | 依据嵌入模型的训练方式(查模型卡),并用已知样本对比验证 |
| 三类失效 | 精确匹配类、极性反转类、条件约束类——都是机制性失效 |
| 应对方向 | 精确串用关键词检索;极性用查询改写;条件用元数据过滤下推 |
| 一条纪律 | 不要指望换嵌入模型解决边界问题;要靠混合手段 |
向量检索不是「更好的搜索」,它是「另一种搜索」。它的价值在于补上了关键词检索找不到的语义相近,代价是丢掉了关键词检索最擅长的精确。本刊编辑部
最后一章把关键词检索与向量检索拼在一起,解决「召回要宽、排序要准、引用要能被验证」这三件事。
八、参考与延伸
这一章的重点是「边界」而不是数学,所以下面三份材料按「先读文档、再看中文代码、最后读论文」排好了。全站不做原文转载,这里只登记链接与「为什么值得读」。
先读文档(把度量和归一化的关系钉死)
- Sentence Transformers · Semantic Textual Similarity —— 官方文档里
similarity_fn_name的取值就是 cosine / dot / euclidean 三种,并且明确写着「模型末尾带归一化层时应该选 dot,它比 cosine 更快且等价」。本章第二节那张表的判据,在这页文档里就是一行配置——想确认「用哪个度量」到底由谁决定,看这一段。
再看中文代码(把相似度算一遍)
- 动手学深度学习(中文,zh.d2l.ai) —— 第 14 章的词嵌入部分把「向量、相似度、近似训练」讲成了可跑的代码。把余弦相似度那一节敲一遍,再回过头看本章的边界探测,会发现「支持 / 不支持分数很高」是可复现的,而不是玄学。
最后读论文(对齐一手定义)
- Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks(arXiv:1908.10084) —— 双塔句向量的一手来源,也是「为什么向量检索可以预计算」的起点。重点看它为什么要用 Siamese 结构把 10000 句的配对从 6500 万次推理降到 10000 次——这正是向量检索能存在的前提。
◇ 面试官会怎么问
共 6 条 · 其中 2 条高频 · 先自己答一遍,再展开对照
语义检索为什么会找错? 高频
四类具体错法:
① 语义相似但答案无关——查询"如何申请年假"召回《请假制度总则》的相似段落,但真正有用的是《年假实施细则》,两者语义高度接近。
② 精确标识失效——向量对数字、工号、型号、专有名词不敏感,"型号 A123" 和 "型号 A132" 在向量空间里几乎没区别,但答案完全不同。
③ 否定与条件丢失——"不含"、"除……之外"这类逻辑关系在稠密向量里表达很弱。
④ 频次与时效被稀释——出现 20 次的关键条款,可能比只出现 1 次的临时通知排在更前,但后者才是当前有效的版本。
所以纯向量检索在真实系统里几乎总是配合关键词检索(补精确匹配)+ 元数据过滤(补时效与权限)+ Rerank(补真实相关性)使用。
余弦相似度、点积、欧氏距离该怎么选?
向量已归一化(模长为 1)——余弦相似度与点积完全等价,此时用点积更快(省掉除法)。大多数现代 embedding 模型输出的向量本身就是归一化的,所以实践中常见"用点积近似余弦"。
向量未归一化——点积会把模长也计入相似度,如果向量模长本身有含义(例如代表置信度或频次),这可能是特性;如果没有含义,那就是噪声,应该先归一化。
欧氏距离——在归一化向量上,欧氏距离与余弦相似度是单调等价的(
d² = 2 − 2·cos),所以排序结果相同。它的好处是直觉上更好解释,坏处是对模长变化更敏感。工程上的建议是:跟你的向量库与索引实现保持一致——很多 ANN 库针对特定度量做了优化(比如内积),混用不同度量的预计算会导致结果与暴力检索不一致,这种 bug 很难发现。
向量检索的能力边界在哪?什么场景不该用它?
① 需要精确匹配——编号、单号、金额、日期范围、代码符号。这些必须走结构化查询或关键词精确匹配。
② 需要聚合与统计——"上个月有多少人入职"。向量检索只能找相似文本,不能做聚合,这本质上是数据库问题。
③ 需要严格权限与合规——向量检索没有天然的权限语义,必须在检索前做元数据过滤(pre-filter),而不是检索后再筛(否则会泄漏被过滤内容的存在性,也浪费召回额度)。
④ 需要多跳推理——"和 A 合作过项目的人,他们的共同上级是谁"。这是图查询擅长的事,向量检索只能拿到与 A 相关的文本片段。
一句话总结:向量检索擅长"找相似的说法",不擅长"精确匹配、聚合、关系推理、权限控制"——所以真实系统里它只是检索层的一个组件,而不是全部。
用户问「哪些功能不支持批量操作」,检索回来的却全是「支持批量操作」的文档。你会怎么修,为什么不建议先换嵌入模型? 高频
这是机制性局限,换任何通用嵌入模型都会遇到,所以要修的地方在向量之外:
- 查询理解阶段把否定改写成正向表述(不支持 → 缺少、未提供、不兼容);
- 再叠一路关键词检索,让「不支持」这个精确串能被直接匹配上。
经验上,改写之后这类查询会从「基本不命中」变成「命中一半以上」,是量级差别。
追问链
- 「同比增长 12%」与「同比下降 12%」属于哪一类失效?
- 精确标识符(订单号、错误码、函数名)应该怎么检索?
同一批向量、同一批候选,换成点积打分之后,长文档的名次普遍上升。原因是什么,怎么处理?
所以未归一化时用点积(或欧氏距离)会系统性偏好长文本。
处理分两步:确认嵌入模型训练时用的是哪个度量(查模型卡)并保持一致;如果模型走余弦,就把向量做 L2 归一化之后改用点积——两者等价,而且更快。
判据:一旦出现「长文档总是排得更高」或者「同一段话复制两遍分数变高」,先查归一化与训练度量这两项。
追问链
- L2 归一化除了省算力,真正的语义作用是什么?
- 欧氏距离在向量归一化之后,与余弦是什么关系?
团队想把嵌入模型从 384 维换成 1536 维,期望检索指标明显提升。作为评审你会问哪几个问题?
- 现在的失败样本里,有多少属于「语义该匹配但没匹配上」?如果失败集中在精确串、否定词、条件约束这三类,换维度完全无用——它们是机制性失效。
- 换模型意味着索引要全量重建,存储与延迟的预算给了吗?很多 384 或 768 维模型在同义匹配上并不输 1536 维,而索引体积与延迟差 2 到 4 倍。
- 相似度阈值有没有跟着重标?分数分布会随模型改变,旧阈值会失效。
追问链
- 换嵌入模型之后,哪些东西必须一起重建或重标?
- 如果失败样本集中在精确匹配类,你会先改哪一环?
结论先行(一句话给出取舍)→ 机制(为什么是这样,涉及哪条链路)→ 代价或边界(这么做放弃了什么)→ 你的实践或数字(真实场景里怎么落地)。 只讲机制不讲代价,是背题;只讲取舍没有数字,是空谈。