RAG 面试拷打 03 · 混合检索

为什么向量相似度都 0.9 了,RAG 还是找错?

文档解析没问题、切片也没把答案切散,搜索框输入制度编号,向量库第一名相似度 0.91——看起来很漂亮,却是另一份名称相近的制度。"相似度都 0.9 了,为什么还是找错?" 本文用图解拆开这个 RAG 最容易翻车的认知误区。

目录
  1. 误区:相似度 ≠ 正确率
  2. 一、关键词与向量,不是谁替代谁
  3. 二、召回前:先把问题和范围搞清楚
  4. 三、重排不是万能补救
  5. 四、30 秒面试回答 + 排查顺序
  6. 延伸:都召回了,新旧制度冲突信谁?

!先戳破一个误区:相似度 ≠ 正确率

核心一句话:向量分数只表示"在当前模型和索引里语义接近",它不是正确率,更不是证据可信度。拿一个分数当答案判断,是很多 RAG 项目第二个容易翻车的地方。

用户问的是精确编号,向量模型却更擅长理解意思。两份文档讨论相同主题时,错的那份完全可能排在前面——因为它的语义确实"更接近"提问的语义,只是它不是用户真正要的那一份。

查询:制度编号 A-1024 ✅ 正确文档 标题:A-1024 年假制度 正文含精确编号 A-1024 向量相似度 0.62 ❌ 错误文档 标题:A-1024 相关培训制度 同主题,语义高度接近 向量相似度 0.91 ← 排第一 语义更近→胜出 精确编号未命中
图 1:用户要"编号 A-1024",但向量模型按"语义接近"打分——同主题的错误文档语义更接近,相似度反而更高,排到第一。
为什么向量会"认错人":向量检索的本质是语义匹配,不是精确匹配。它回答的是"哪份文档的意思最像这个问题",而不是"哪份文档包含用户说的那个精确 token"。当你要的是编号、型号、错误码、人名这类精确词,语义相似度天然会失灵。

1关键词与向量,不是谁替代谁

老周问:"既然向量会错,是不是直接换回全文检索?"——也不行。两种检索擅长的事完全不同:

🔍 关键词检索(BM25 等)

  • 擅长精确词:制度编号、产品型号、错误码、人名
  • 字面匹配,命中即命中,不"理解意思"
  • 用户问"刚入职能不能休年假",原文写"连续工作不满十二个月"——两边没相同词,关键词找不到

🧠 向量检索(Embedding)

  • 擅长自然语言语义:同义改写、意译表达
  • "刚入职" ↔ "连续工作不满十二个月" 能命中
  • 但对精确编号/型号容易"认错人"(图 1)
生产做法:并行做关键词召回向量召回两路,再用 RRF(Reciprocal Rank Fusion,倒数排名融合)合并结果。它融合的是两条结果里的名次,不是强行比较两种完全不同的原始分数(余弦 0.91 和 BM25 分数根本不在同一量纲)。
关键词召回 命中编号/型号(名次) 向量召回 命中语义(名次) RRF 融合 按名次合并 融合结果 两路互补
图 2:混合检索——两路各按"名次"召回,RRF 融合排名而非比较原始分数,避免量纲不一致。
# RRF 公式:每个文档得分 = Σ 1/(k + rank_i),k 通常取 60 score(d) = Σ 1 / (60 + rank(d, list_i)) # 只用到"名次 rank",完全不碰 BM25 分 / 余弦分本身的数值

2召回前:先把问题和范围搞清楚

对话里用户经常只说"这个怎么申请""它什么时候生效"。三件事不能混成一个向量相似度:

① 上下文补全 + 查询改写 ② 元数据过滤(缩小范围) ③ 关键词召回 + ④ 向量召回 ⑤ Rerank 重排

① 查询改写:解决"用户怎么问"

先结合对话上下文把代词补全,再按需生成语义问法关键词问法。但不会让改写凭空加入用户没说过的条件——否则会检索出用户根本没问的东西。

② 元数据过滤:解决"应该去哪里找"

关键原则:租户、部门、文档类型、适用地区、有效期、权限缩小候选集合。该在召回前排除的旧版本越权文档,不能先全量找出来,再寄希望于大模型"自觉别用"。过滤必须在召回前做,而不是召回后靠 LLM 自律。
全库文档 100 万条 元数据过滤后 同租户+生效+有权限 5万 混合检索 + Rerank 在 5 万里精排 Top K ↑ 先把"不该找的"挡在门外,相似度才不会被噪声干扰
图 3:检索链路顺序——元数据过滤(缩小范围)必须在召回前,而不是召回后靠 LLM 自律排除。
三件事各管一段:
查询改写 → 用户怎么问(表达层)
元数据过滤 → 应该去哪里找(范围层)
混合检索 → 在这个范围里找哪些证据(召回层)
混成一团只丢给向量相似度,等于让"语义分"同时背了三口锅。

3重排不是万能补救

老周继续问:"那我召回 Top 50,再交给 Rerank,不就稳了吗?"

重排的边界:Rerank 能更精细地比较问题和候选块,把真正相关的证据往前提。但它只能重新排列已经进入候选池的内容。正确条款如果一开始根本没被关键词或向量召回,Rerank 不可能从全库把它变出来。

Top K 太小

  • 容易漏掉正确证据(根本没进候选池)
  • Recall@K 低

Top K 太大

  • 增加重排成本
  • 相似但无关的内容混进上下文,干扰 LLM

参数要靠真实问题集评测,不是拍脑袋。Top K 不是越大越好。

排查顺序:别一出问题就盲调 Prompt

1

先看 Recall@K:正确证据进候选池了吗?

没进 → 问题在召回(改查询改写 / 元数据过滤 / 混合权重 / 扩 Top K)。

2

进了却没排到前面 → 看融合权重和重排

调整 RRF 权重、换更强 Reranker、调重排阈值。

3

证据顺序正确但答案仍错 → 才查生成层

此时才是 Prompt / 模型理解的问题。

经验法则:召回缺失 ≠ Prompt 问题。按"召回 → 排序 → 生成"逐层定位,才不会一出问题就乱改提示词,而真正的病根(比如正确文档根本没被召回)一直没人管。

4如果只给我 30 秒,我会这样答

30 秒面试回答

"向量相似度高只代表语义接近,不代表证据正确。我的检索链路会先做上下文补全和查询改写,再按权限、版本、范围做元数据过滤;关键词检索负责编号、专有名词和精确词,向量检索负责自然语言语义,两路并行后用 RRF 融合,再对候选集做 Rerank。评测时先看正确证据是否进入 Recall@K,再看排序和生成,避免把召回缺失误判成 Prompt 问题。"

改写 补全+双问法 过滤 权限/版本/范围 关键词 精确词 向量 语义 RRF 融合 Rerank 精排
图 4:完整检索链路——改写 → 过滤 →(关键词 + 向量)→ RRF 融合 → Rerank,逐层兜底。

+延伸:都召回了,新旧制度冲突信谁?

老周没有停。他把两份都排进前五的制度放到面前:一份已经废止,一份正在生效,内容刚好互相冲突。"现在该召回的都召回了。新旧制度一起命中,模型准备信谁?"

这正是第二节"元数据过滤"要解决的:如果过滤阶段已经用有效期 / 版本状态把废止文档排除在候选池外,它根本不会进入 Top K,也就不会和生效文档"打架"。召回后再让 LLM 判断新旧,是把本该在过滤层解决的事推给了生成层——既不可靠也不可解释。
落地建议(冲突场景)
  • 过滤层优先:status=生效 AND valid_to > now() 在召回前剔除废止/过期文档。
  • 保留证据来源:输出时带上文档的生效时间、版本号,让用户可追溯,而非只给一句结论。
  • 冲突检测:若同主题多份文档同时进入候选,可在 Rerank 阶段加"时效性加权"或显式提示 LLM"以最新生效版本为准"。
  • 评测覆盖:问题集里要专门包含"新旧冲突"样本,验证过滤与排序是否真的把生效版顶到最前。

一句话收尾

相似度 0.9 找错,不是向量"不行",而是你让它一个人扛了三件事:表达改写、范围过滤、精确匹配。把"查询改写 + 元数据过滤 + 关键词/向量混合 + Rerank"各归各位,先确认正确证据进没进 Recall@K,再谈排序和生成——这才是 RAG 检索不翻车的正路。