3978 字
约 13 分钟
13
7.4 混合检索与重排
2026-10-25
无标签

7.4 混合检索与重排

一句话结论:只用向量检索,等于放弃了"精确匹配"这个基本能力——搜错误码、搜人名、搜配置项都会失效。而重排是整条 RAG 链路里性价比最高的一步:加一个模型调用,就能把"能用"变成"好用"。

读这篇之前

需要先读 7.2(四个阶段)、7.3(切片)。这一篇解决"召回不准"的问题。


一、向量检索的三个结构性弱点

向量检索很强大,但它有三件事天生做不好:

弱点 例子 原因
精确匹配差 搜 ERR_4032 这类字符串在训练数据里罕见,向量表达不精确
不理解否定/限定 "不是 Java 的方案" 向量把语义压成一个点,"不是"这个限定容易丢失
泛泛相关 问"怎么配缓存"召回"缓存是什么" 语义相近,但回答不了问题

第一条最致命,因为技术文档里充满了错误码、函数名、配置项——全是"精确匹配"的场景。


二、混合检索:向量 + 关键词

两条路各有所长

向量检索(Dense) 关键词检索(BM25 / Sparse)
强项 同义改写、跨语言、语义相似 精确术语、错误码、人名、编号
弱项 精确匹配差 同义改写的找不到
机制 向量距离 词频统计

做法:两路各召回一批(比如各 20 条),合并去重后统一重排。

问题 ──┬─→ 向量检索 → 20 条 ─┐
       └─→ 关键词检索 → 20 条 ─┴─→ 合并去重 → 重排 → Top 5

这就是生产级 RAG 的标配。 只用向量,会遇到"明明库里就有这个错误码,却搜不出来"的困境。

合并后的一个细节:分数不可直接比

向量相似度(0~1)和 BM25 分数(无上界)量纲不同,不能直接加权相加。两种处理:

做法 说明
归一化后加权 各转成 0~1 再按权重合并
只做去重,交给重排排序 更简单可靠,推荐

第二种更稳——因为重排器会重新打分,不需要你在合并阶段调权重。


三、查询改写:提升召回的上游手段

用户提问往往模糊或口语化,直接检索效果差。先用 LLM 改写查询,能显著提升召回。[原文 723]

改写的三种策略

策略 做法 收益
提取关键词 把口语问题转成检索式 去噪
补充同义词 加入可能的替代表述 提高召回率
生成多角度查询 一次生成 3 个不同角度的查询,分别检索后合并 召回率大幅提升

一个具体例子:[原文 723]

原始问题:"Agent 怎么记住东西的?"
改写后:["Agent 记忆系统架构",
        "Agent 短期记忆 长期记忆",
        "LLM memory management"]

多查询并行检索,结果合并去重 → 召回率大幅提升。 这是最实用的技巧之一。

HyDE:用一个"假答案"去检索

有时候问题太短,向量化后语义信息不足。HyDE 的思路很巧妙:[原文 723]

① 用户问题:"RAG 怎么做?"
② LLM 先生成一个"假回答":
   "RAG 是检索增强生成技术,通过先检索相关文档再生成回答……"
③ 用这个"假回答"去向量检索
   (假回答比短问题语义丰富得多)
④ 用检索到的真材料生成真答案

为什么有效? 因为"假回答"的措辞更接近真实文档的措辞,向量也就更接近。

代价与风险:

风险 说明
多一次模型调用 延迟与成本增加
假回答可能跑偏 如果模型对问题理解错,检索就会偏
不适合精确查询 搜错误码时用 HyDE 反而更差

适用场景:概念性、开放式问题。不适用:精确匹配类查询。


四、重排:性价比最高的一步

为什么召回后还要重排

召回 重排
目标 快,从百万里捞 20 条 准,在 20 条里排好序
机制 向量相似度(廉价近似) 交叉编码器(昂贵精确)
范围 全库 只有候选集

核心逻辑:先用便宜的方法缩小范围,再用贵的方法精确排序。

为什么重排更准

向量检索是分别把问题和文档变成向量,再算距离——两者从未"见面"。

重排器把问题和文档一起送进模型,判断"这段能不能回答这个问题"。

打个比方:

  • 向量检索像凭标签匹配(两人都标了"爱读书",就觉得合得来)
  • 重排像真聊一次(才知道能不能处)

重排的实测收益

在生产环境里,重排往往能把准确率提升一个明显档次。原因很直接:"语义相似"不等于"能回答问题"。

例:问"如何解决连接超时"

文档 向量相似度 能回答吗
《连接超时的原因分析》 高 ✅
《超时参数配置指南》 高 ✅
《为什么网络会慢》 中高 ❌(泛泛而谈)
《连接池原理》 中 部分

重排器能更好地区分前三者。


五、完整链路与配置建议

查询
 ├─ 改写(提取关键词 / 多角度)
 ├─ 向量检索 → 20 条
 ├─ 关键词检索 → 20 条
 ├─ 合并去重 → 约 30 条
 ├─ 重排 → Top 5
 ├─ 拼装(加来源标记,防注入)
 └─ 生成(要求引用来源)

参数建议(起点值):

参数 建议 说明
向量召回数 20 太少会漏
关键词召回数 20 与向量对齐
重排后保留 5~8 太多会稀释注意力(2.2)
是否改写 概念性问题开启 精确查询关闭

六、常见误区

误区一:向量检索足够先进,不需要关键词。
精确匹配场景(错误码、函数名)会失效。混合检索是标配。

误区二:两种分数可以直接相加。
量纲不同。归一化或交给重排。

误区三:查询改写总是有益。
它会引入偏差,且增加延迟。精确查询不要改写。

误区四:HyDE 适合所有场景。
搜错误码时会变差。只用于概念性、开放式问题。

误区五:重排太贵,能省就省。
它是性价比最高的一步——只对 30 条候选做,不是全库。

误区六:召回数越多越好。
太多会让重排压力变大、上下文变长。20~30 条足够。

误区七:检索准了,答案就一定对。
还要看模型是否读懂、是否会混用材料(7.5)。


七、动手:token 泄漏诊断表

7.4 讲了三个弱点。这里给出一张"症状 → 原因"的诊断表。

症状 可能原因 验证方法
搜错误码/函数名搜不到 纯向量检索 换成关键词搜同一个词,能搜到就确认
召回的都"大概相关"但没用 缺重排 看 top-5 与问题的实际相关性
否定查询失效("不是 X") 向量不理解否定 改写查询为肯定表述
同义改写搜不到 纯关键词检索 换同义词再搜
中文搜不到英文内容 单语言 embedding 检查模型是否多语言
长尾问题完全召回空 切片太大或太小 看那块的切片结果

一个完整的验证脚本

TEST_CASES = [
    ("getUserById 定义在哪", "精确匹配"),
    ("怎么配置缓存", "语义检索"),
    ("不是用 Redis 的方案", "否定"),
    ("缓存配置", "同义"),
]

def diagnose():
    for q, kind in TEST_CASES:
        vec = retrieve_vector(q, k=5)
        kw  = retrieve_keyword(q, k=5)
        merged = rerank(q, dedupe(vec + kw), top_n=5)
        print(f"{kind:6} | 向量 {len(vec)} | 关键词 {len(kw)} | 合并后 {len(merged)}")
        print(f"        首条: {merged[0]['title'] if merged else '空'}")

四类查询各测一遍,就能看出你的 RAG 缺哪一环。

一个常见的误判

现象:搜不到,以为是 embedding 模型不行
真相:切片时把那个函数名切成了两半
验证:搜切片后的原文,看它是否完整

先查切片,再查模型 —— 顺序反了会白换模型。


八、动手:重排模型的三种选择

7.4 讲了重排的重要性。这里给出怎么选。

三种实现

方案 说明 延迟 成本
交叉编码器(本地) 小模型(如 bge-reranker) 50-200ms(GPU) 一次性部署
LLM 打分 让大模型对每段打分 1-3 秒 每次调用计费
规则重排 关键词命中数、位置权重 <1ms 免费

各自的适用面

场景 建议
追求质量、有 GPU 交叉编码器
候选少(<10)、质量要求极高 LLM 打分
延迟敏感、成本敏感 规则重排
起步阶段 规则重排(先跑通)

一个规则重排的例子

def rule_rerank(q, candidates):
    kw = set(extract_keywords(q))
    for c in candidates:
        score = 0
        # ① 关键词命中
        score += 2.0 * len(kw & set(extract_keywords(c["text"])))
        # ② 标题命中(更强信号)
        if any(k in c["title"] for k in kw):
            score += 3.0
        # ③ 位置靠前(文档开头更可能是概述)
        score += 0.5 * (1.0 - c["position"] / c["total"])
        c["score"] = score
    return sorted(candidates, key=lambda x: -x["score"])

规则重排在很多场景能拿到交叉编码器 70-80% 的效果,而成本几乎为零。

一个建议的演进路径

起步:规则重排(0 成本,先跑通)
       ↓ 效果不够
第二:交叉编码器(加 GPU 或云服务)
       ↓ 还想要更好
第三:LLM 打分(只在候选 <10 时用)

九、动手:重排的收益量化

7.4 讲重排是"性价比最高的一步"。能定量说明吗?

一个可做的对比实验

def compare(k=20, top_n=5):
    stats = {"with_rerank": 0, "without_rerank": 0}
    for q, expect in TEST_CASES:
        cands = retrieve_hybrid(q, k=k)
        # 不重排:取前 5
        if hits_expect(cands[:top_n], expect):
            stats["without_rerank"] += 1
        # 重排后取前 5
        if hits_expect(rerank(q, cands, top_n=top_n), expect):
            stats["with_rerank"] += 1
    return stats

一个典型结果(50 个问题)

方案 命中数 命中率
不重排(向量 top-5) 31 62%
不重排(混合 top-5) 36 72%
重排后 top-5 44 88%

两个收益叠加:混合 +12%,重排再 +16%。

成本对照

项 每次额外成本
混合检索 +1 次 BM25(<5ms,免费)
重排 20 条 50-200ms(GPU)/ 或 API 计费

用 200ms 换 16 个百分点的命中率 —— 这是 RAG 里最划算的一笔交易。

一个反例

错:只做向量检索,直接取 top-5
后果:62% 命中率,用户觉得"经常答不到点子上"
心态:以为模型不行,去换模型

换模型救不了检索问题 —— 这是 13.1 讲的"评的是什么"在 RAG 层的重复。


一个补充:重排失败时的兜底

重排模型可能不可用(服务挂了、超时)。要有兜底。

def safe_rerank(q, cands, top_n=5):
    try:
        return rerank(q, cands, top_n=top_n)
    except Exception as e:
        log.warn(f"重排失败,降级为原文顺序: {e}")
        return cands[:top_n]        # 降级:用召回顺序

三种降级策略

降级 效果
用召回顺序(推荐) 还能用,质量降一档
用规则重排 更好,但需实现
直接报错 不可取(用户看到失败)

"降级但不失败"是生产系统的必备能力。

一个真实场景

事故:重排服务 OOM,整个 RAG 全部报错
用户体验:完全不可用(比"稍微不准"差得多)
修复:加重排的 try/except + 降级

这件事的教训:任何"增强"环节,都要能优雅退化。


十、动手:HyDE 与多查询改写的实际效果

7.4 提到 HyDE(先造假答案再检索)和查询改写。这里给出边界。

HyDE 的三个前提

前提 说明
问题模糊或口语化 "这东西老卡怎么办"
文档是正式书面语 手册、规范
允许一次额外生成 多一轮模型调用

三条都满足才有收益。

HyDE 的实现与陷阱

def hyde_retrieve(question):
    # ① 让模型先"编"一个假答案
    fake = llm.chat(f"写一段能回答这个问题的文档片段:{question}",
                    temperature=0.3, max_tokens=300)
    # ② 用假答案去检索
    return store.search(embed(fake), top_k=20)

陷阱:

陷阱 后果
假答案里混入错误事实 检索到错误方向的文档
精确查询用 HyDE 错误码被"改写"成别的词,搜不到
多一轮模型调用 延迟 +300ms~1s

最后一条是关键边界:HyDE 不适合精确查询(错误码、函数名、版本号)。

一个判断表

查询类型 HyDE
口语化问题 ✅ 有效
概念解释 ✅ 有效
精确匹配(ID/错误码) ❌ 禁用
短关键词 ❌ 无意义

多查询改写(比 HyDE 更稳)

def multi_query_retrieve(question, n=3):
    variants = llm.chat(f"把这个问题的{n}种不同问法列出来:{question}",
                        temperature=0.5).split("\n")
    hits = []
    for v in [question] + variants:
        hits.extend(store.search(embed(v), top_k=10))
    return dedupe(hits)

它的好处是"不做假答案" —— 只改问法,不引入编造内容。

所以在"怕污染"的场景,多查询改写比 HyDE 更安全。


十一、下一篇

到这里,RAG 的工程主体讲完了:切片、嵌入、召回、重排、拼装。

但真实系统里,RAG 出错的方式远比"检索不准"多。下一篇专门讲失败模式与诊断——包括那个最危险的:它不会说不知道,而是会把错误的材料编成正确答案。

一个补充:top_k 怎么定

top_k 效果 风险
3 精准,上下文短 可能漏掉答案
5-8 平衡 ——
20+ 召回全 上下文长,注意力稀释

推荐做法:召回 20-30 条,重排后取 5-8 条。

召回要多(20-30)→ 保证不漏
重排要准 → 把对的排前面
送入要少(5-8)→ 保证上下文干净

这个"漏斗"结构是 RAG 的核心范式 —— 每一层解决不同问题。

7.4 混合检索与重排
https://clxhxhhr.top/posts/4900/
作者
clxstart
发布于
2026-10-25
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。