语义检索为什么会找错?因为「语义相近」和「对回答问题有用」是两件事。前者是向量空间的几何关系,后者是任务相关性的判断。理解这中间的落差,才能明白向量检索的边界在哪里、什么时候必须叠加别的检索手段。
编辑部注 · 本章的核心是「能力边界」而不是数学。重点是理解为什么纯向量检索在一些场景必然失效。
先把向量检索的物理本质讲清楚:它把文本映射到一个高维空间里的点,让「语义相近的文本」在空间里距离更近。检索就是找最近的邻居。这个机制非常有效,也有非常明确的边界——而多数误用都源于不清楚边界在哪。
一、嵌入做的事:从符号到坐标
文本在计算机里本来是离散符号序列,无法比较「相似」。嵌入模型把它变成连续向量,于是「相似」变成了「距离」这个可计算的东西。
关键在于「相似」是通过什么学到的。模型在大量文本对(相似的、不相似的)上训练,学会了把「意思接近」的文本映射到相近的位置。这带来两个直接结论:
| 嵌入擅长的 | 嵌入不擅长的 |
|---|---|
| 同义表达:「转化率低」与「成交比例不高」 | 精确标识符:订单号、错误码、函数名、型号(差一个字符就是另一个东西) |
| 跨语言:「客户满意度」与「customer satisfaction」 | 逻辑关系:「A 大于 B」与「B 大于 A」在向量空间里可能很近 |
| 语义概括:「怎么提升留存」与「提高用户复购的方法」 | 否定与极性:「有效」与「无效」在向量空间里往往距离很近 |
| 主题归类:一段文字属于哪个业务领域 | 时效与版本:「2024 口径」与「2025 口径」的向量几乎相同 |
「这个方案有效」和「这个方案无效」在向量空间里通常非常接近,因为模型学到的主要是「方案 + 有效性评估」这个主题,而不是那个「否」字带来的极性反转。类似的还有「同比增长」与「同比下降」、「支持」与「不支持」。
后果很严重:用户明确问「哪些功能不支持 X」,检索回来的全是「支持 X」的文档。解法不是换嵌入模型(这是共性局限),而是在查询理解阶段把「不支持」改写为「缺少 / 未提供 / 不兼容」等正向表述,或者辅以关键词检索把「不支持」这个精确串匹配上。
二、相似度度量:怎么选
| 度量 | 公式直觉 | 何时用 | 注意 |
|---|---|---|---|
| 余弦相似度 | 只看两个向量的夹角,忽略长度 | 默认选择,绝大多数文本嵌入 | 归一化之后与点积等价 |
| 点积 | 夹角 + 长度都算 | 模型训练时就用的点积(如部分 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% 的查询属于这三类失效场景,你应该优先改哪一环?
五、自测
六、小结
| 议题 | 结论 |
|---|---|
| 嵌入的本质 | 把语义相近映射为距离相近;编码的是「语义场」而非精确信息 |
| 度量选择 | 依据嵌入模型的训练方式(查模型卡),并用已知样本对比验证 |
| 三类失效 | 精确匹配类、极性反转类、条件约束类——都是机制性失效 |
| 应对方向 | 精确串用关键词检索;极性用查询改写;条件用元数据过滤下推 |
| 一条纪律 | 不要指望换嵌入模型解决边界问题;要靠混合手段 |
向量检索不是「更好的搜索」,它是「另一种搜索」。它的价值在于补上了关键词检索找不到的语义相近,代价是丢掉了关键词检索最擅长的精确。本刊编辑部
最后一章把关键词检索与向量检索拼在一起,解决「召回要宽、排序要准、引用要能被验证」这三件事。
◇ 面试官会怎么问
共 3 条 · 其中 1 条高频 · 先自己答一遍,再展开对照
语义检索为什么会找错? 高频
四类具体错法:
① 语义相似但答案无关——查询"如何申请年假"召回《请假制度总则》的相似段落,但真正有用的是《年假实施细则》,两者语义高度接近。
② 精确标识失效——向量对数字、工号、型号、专有名词不敏感,"型号 A123" 和 "型号 A132" 在向量空间里几乎没区别,但答案完全不同。
③ 否定与条件丢失——"不含"、"除……之外"这类逻辑关系在稠密向量里表达很弱。
④ 频次与时效被稀释——出现 20 次的关键条款,可能比只出现 1 次的临时通知排在更前,但后者才是当前有效的版本。
所以纯向量检索在真实系统里几乎总是配合关键词检索(补精确匹配)+ 元数据过滤(补时效与权限)+ Rerank(补真实相关性)使用。
余弦相似度、点积、欧氏距离该怎么选?
向量已归一化(模长为 1)——余弦相似度与点积完全等价,此时用点积更快(省掉除法)。大多数现代 embedding 模型输出的向量本身就是归一化的,所以实践中常见"用点积近似余弦"。
向量未归一化——点积会把模长也计入相似度,如果向量模长本身有含义(例如代表置信度或频次),这可能是特性;如果没有含义,那就是噪声,应该先归一化。
欧氏距离——在归一化向量上,欧氏距离与余弦相似度是单调等价的(
d² = 2 − 2·cos),所以排序结果相同。它的好处是直觉上更好解释,坏处是对模长变化更敏感。工程上的建议是:跟你的向量库与索引实现保持一致——很多 ANN 库针对特定度量做了优化(比如内积),混用不同度量的预计算会导致结果与暴力检索不一致,这种 bug 很难发现。
向量检索的能力边界在哪?什么场景不该用它?
① 需要精确匹配——编号、单号、金额、日期范围、代码符号。这些必须走结构化查询或关键词精确匹配。
② 需要聚合与统计——"上个月有多少人入职"。向量检索只能找相似文本,不能做聚合,这本质上是数据库问题。
③ 需要严格权限与合规——向量检索没有天然的权限语义,必须在检索前做元数据过滤(pre-filter),而不是检索后再筛(否则会泄漏被过滤内容的存在性,也浪费召回额度)。
④ 需要多跳推理——"和 A 合作过项目的人,他们的共同上级是谁"。这是图查询擅长的事,向量检索只能拿到与 A 相关的文本片段。
一句话总结:向量检索擅长"找相似的说法",不擅长"精确匹配、聚合、关系推理、权限控制"——所以真实系统里它只是检索层的一个组件,而不是全部。
结论先行(一句话给出取舍)→ 机制(为什么是这样,涉及哪条链路)→ 代价或边界(这么做放弃了什么)→ 你的实践或数字(真实场景里怎么落地)。 只讲机制不讲代价,是背题;只讲取舍没有数字,是空谈。