RAG 面试拷打 03 · 混合检索
为什么向量相似度都 0.9 了,RAG 还是找错?
文档解析没问题、切片也没把答案切散,搜索框输入制度编号,向量库第一名相似度 0.91——看起来很漂亮,却是另一份名称相近的制度。"相似度都 0.9 了,为什么还是找错?" 本文用图解拆开这个 RAG 最容易翻车的认知误区。
!先戳破一个误区:相似度 ≠ 正确率
核心一句话:向量分数只表示"在当前模型和索引里语义接近",它不是正确率,更不是证据可信度。拿一个分数当答案判断,是很多 RAG 项目第二个容易翻车的地方。
用户问的是精确编号,向量模型却更擅长理解意思。两份文档讨论相同主题时,错的那份完全可能排在前面——因为它的语义确实"更接近"提问的语义,只是它不是用户真正要的那一份。
图 1:用户要"编号 A-1024",但向量模型按"语义接近"打分——同主题的错误文档语义更接近,相似度反而更高,排到第一。
为什么向量会"认错人":向量检索的本质是语义匹配,不是精确匹配。它回答的是"哪份文档的意思最像这个问题",而不是"哪份文档包含用户说的那个精确 token"。当你要的是编号、型号、错误码、人名这类精确词,语义相似度天然会失灵。
1关键词与向量,不是谁替代谁
老周问:"既然向量会错,是不是直接换回全文检索?"——也不行。两种检索擅长的事完全不同:
🔍 关键词检索(BM25 等)
- 擅长精确词:制度编号、产品型号、错误码、人名
- 字面匹配,命中即命中,不"理解意思"
- 用户问"刚入职能不能休年假",原文写"连续工作不满十二个月"——两边没相同词,关键词找不到
🧠 向量检索(Embedding)
- 擅长自然语言语义:同义改写、意译表达
- "刚入职" ↔ "连续工作不满十二个月" 能命中
- 但对精确编号/型号容易"认错人"(图 1)
生产做法:并行做关键词召回和向量召回两路,再用 RRF(Reciprocal Rank Fusion,倒数排名融合)合并结果。它融合的是两条结果里的名次,不是强行比较两种完全不同的原始分数(余弦 0.91 和 BM25 分数根本不在同一量纲)。
图 2:混合检索——两路各按"名次"召回,RRF 融合排名而非比较原始分数,避免量纲不一致。
# RRF 公式:每个文档得分 = Σ 1/(k + rank_i),k 通常取 60
score(d) = Σ 1 / (60 + rank(d, list_i))
# 只用到"名次 rank",完全不碰 BM25 分 / 余弦分本身的数值
2召回前:先把问题和范围搞清楚
对话里用户经常只说"这个怎么申请""它什么时候生效"。三件事不能混成一个向量相似度:
① 上下文补全 + 查询改写
→
② 元数据过滤(缩小范围)
→
③ 关键词召回
+
④ 向量召回
→
⑤ Rerank 重排
① 查询改写:解决"用户怎么问"
先结合对话上下文把代词补全,再按需生成语义问法和关键词问法。但不会让改写凭空加入用户没说过的条件——否则会检索出用户根本没问的东西。
② 元数据过滤:解决"应该去哪里找"
关键原则:用租户、部门、文档类型、适用地区、有效期、权限缩小候选集合。该在召回前排除的旧版本和越权文档,不能先全量找出来,再寄希望于大模型"自觉别用"。过滤必须在召回前做,而不是召回后靠 LLM 自律。
图 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 问题。"
图 4:完整检索链路——改写 → 过滤 →(关键词 + 向量)→ RRF 融合 → Rerank,逐层兜底。
+延伸:都召回了,新旧制度冲突信谁?
老周没有停。他把两份都排进前五的制度放到面前:一份已经废止,一份正在生效,内容刚好互相冲突。"现在该召回的都召回了。新旧制度一起命中,模型准备信谁?"
这正是第二节"元数据过滤"要解决的:如果过滤阶段已经用有效期 / 版本状态把废止文档排除在候选池外,它根本不会进入 Top K,也就不会和生效文档"打架"。召回后再让 LLM 判断新旧,是把本该在过滤层解决的事推给了生成层——既不可靠也不可解释。
落地建议(冲突场景)
- 过滤层优先:用
status=生效 AND valid_to > now() 在召回前剔除废止/过期文档。
- 保留证据来源:输出时带上文档的生效时间、版本号,让用户可追溯,而非只给一句结论。
- 冲突检测:若同主题多份文档同时进入候选,可在 Rerank 阶段加"时效性加权"或显式提示 LLM"以最新生效版本为准"。
- 评测覆盖:问题集里要专门包含"新旧冲突"样本,验证过滤与排序是否真的把生效版顶到最前。
一句话收尾
相似度 0.9 找错,不是向量"不行",而是你让它一个人扛了三件事:表达改写、范围过滤、精确匹配。把"查询改写 + 元数据过滤 + 关键词/向量混合 + Rerank"各归各位,先确认正确证据进没进 Recall@K,再谈排序和生成——这才是 RAG 检索不翻车的正路。