这是 RAG 学习笔记的第 2 篇,把在线链路走一遍:
问题清洗 → 查询改写 → 混合检索 → Rerank → 组装上下文 → LLM 生成 → 引用后处理我原本是硬背这串流程,背不下来;后来把它想成"帮朋友查资料回答问题的完整过程",就顺了。
本系列导航
- 00 开篇:用「开卷考试」理解 RAG 与我的学习方法
- 01 Chunking 切块与 Embedding 选型
- 02 在线链路七步走与混合检索 RRF(本篇)
- 03 Rerank 与查询改写
- 04 工程化:延迟、缓存、成本与权限
- 05 评测调优与三个进阶架构
一、七步流程:每一步为什么存在
| 步骤 | 做什么 | 类比 |
|---|---|---|
| 问题清洗 | 去掉错别字、多余空格、敏感词 | 把朋友含糊的话听清楚 |
| 查询改写 | 把口语化、有指代的问题改成适合检索的句子 | 把"它退款怎么算"改成"会员年卡的退款条件是什么" |
| 混合检索 | 同时用关键词(BM25)和语义(向量)找文档 | 同时用"关键字搜索"和"意思相近搜索" |
| Rerank | 把找回来的几十篇用更精细的模型重新排序 | 海选后面试,精挑 5 个 |
| 组装上下文 | 把挑出来的片段拼成一段话给 LLM | 把资料整理好放在桌上 |
| LLM 生成 | 大模型根据资料写出答案 | 你根据资料组织语言回答朋友 |
| 引用后处理 | 标来源、检查不安全内容 | 告诉朋友"这是从 XX 文件第 3 页看到的" |
记忆口诀:清 → 改 → 找 → 排 → 拼 → 答 → 标。
面试口述版:“在线阶段先清洗和改写用户问题,然后混合检索召回候选,再用 Rerank 精排,组装上下文给 LLM 生成,最后做引用和安全后处理。”——这就够了,不需要一字不差背步骤名。
二、混合检索:BM25 管"字面",向量管"意思"
BM25 是什么?
BM25 是一种基于关键词的检索算法,传统搜索引擎(比如 Elasticsearch)默认就用它。
它给每篇文档打分,依据三个因素:
- 词频:查询词在文档里出现几次,越多分越高(但有上限);
- 逆文档频率(IDF):一个词在所有文档里越罕见,权重越高——比如"Pod"比"怎么"稀有,所以匹配上"Pod"得分更高;
- 文档长度:长文档做归一化,避免长文档占便宜。
一句话:BM25 就是"字面匹配打分器"。
向量检索在做什么?
很多人(包括我)以为"向量检索"就是把文字编码成向量——其实它包含两步:
- 编码:用 Embedding 模型把用户问题变成向量,约 10–30ms;
- 搜索:拿这个向量去向量数据库(Milvus / FAISS)里找最相似的文档向量,约 20–50ms。
类比:你要在图书馆找一本内容相似的书——先把需求翻译成管理员能懂的"索引号"(编码),管理员再去书架按索引号找(搜索)。两步加起来才是向量检索时间。
为什么必须混合?
- BM25 擅长精确匹配:产品名、错误码、版本号、专有名词;
- 向量检索擅长语义泛化:同义改写、口语表达。
举例:用户问"K8s Pod 启动失败怎么排查"
| 方式 | 结果 |
|---|---|
| 纯向量 | 召回"Kubernetes 容器无法运行"(意思相近),但可能漏掉精确包含"Pod"的文档 |
| 纯 BM25 | 精确命中"Pod"“启动失败”,但漏掉"容器无法运行"这类语义相关文档 |
| 混合 + RRF | 两者互补,Recall@10 从 0.78 提升到 0.91 |
一句话:BM25 擅长"字面一样",向量擅长"意思一样",合起来才最全。
三、RRF:为什么只看排名、不看分数
RRF = Reciprocal Rank Fusion(倒数排名融合)。
公式很朴素:
score = Σ 1 / (k + rank_i)意思是:对每一路检索结果,按其排名贡献分数(第 1 名贡献最大,越靠后越小),再把多路分数相加。
为什么不用"加权分数"而是用 RRF?
这是我最没懂、后来最服气的一点:
BM25 的分数范围可能是 0–20,向量相似度是 0–1,量纲完全不一致。直接加权归一化会引入偏差(归一到哪个区间?权重怎么定?)。
而RRF 只看排名,对分数量纲不敏感,所以更稳健。k 值常用默认k=60,对排名差异不敏感。
面试被问到"怎么融合多路检索结果"时,我的答法是:“我用 RRF 而不是加权求和,因为两路分数量纲不同,加权归一化会引入偏差;RRF 只依赖排名,更稳健。”
四、顺带搞懂:什么叫"双塔粗排"
面试文档里写"召回阶段是双塔粗排:Query 和 Doc 独立编码,交互不充分",我一开始完全不懂。
双塔:有两个"塔"(神经网络)——一个塔编码 Query,另一个塔编码 Doc。它们各自独立把文字变成向量,然后算两个向量的相似度。
为什么叫"粗排":因为 Query 和 Doc没有放在一起看,模型不知道它们之间具体的词级交互。
举例:Query 是"苹果价格",Doc 是"苹果手机售价"——双塔只能分别编码,可能忽略"价格"和"售价"的细微对应关系。
精排(Cross-Encoder):把 Query 和 Doc 拼在一起,如[CLS] 苹果价格 [SEP] 苹果手机售价 [SEP],让模型同时看到两边做深度交互——精度更高,但计算慢。
所以标准流程是:双塔粗排召回 Top-50 → Cross-Encoder 精排 Top-5。粗排快但不够准,精排准但慢,所以只对少量候选做。
五、检索为空怎么办:三层兜底
举例:保险理赔 RAG,用户问"2025 年新增的宠物医疗险报销比例"
- 第一层——放宽重试:降阈值、去过滤、启用 BM25 兜底;
- 第二层——明确拒答:Prompt 约束"上下文无相关信息时直接说未找到";
- 第三层——追问澄清:追问"您是指哪一款宠物险?"
核心原则:宁可拒答,不可幻觉。
这一条我后来在评估部分又见到一次(无答案样本、受监管行业设 Faithfulness 门槛 0.98)。"会拒答"是 RAG 系统成熟的标志,只会答不会拒的系统在真实业务里很危险。
六、这一篇的记忆卡
| 概念 | 一句话 |
|---|---|
| 在线七步 | 清 → 改 → 找 → 排 → 拼 → 答 → 标 |
| BM25 | 基于关键词的字面匹配打分器(词频 + IDF + 长度归一) |
| 向量检索 | 编码(文字→向量)+ 搜索(向量库找相似) |
| 混合检索 | BM25 管字面、向量管意思,Recall@10 可从 0.78 → 0.91 |
| RRF | 只看排名不看分数,避免量纲不一致 |
| 双塔粗排 | Query/Doc 独立编码,快但不准 |
| 检索为空 | 放宽重试 → 明确拒答 → 追问澄清 |
七、我的复盘
这一篇最大的收获是搞清了**"召回"和"排序"是两个不同的目标**:
召回阶段要的是"别漏"(高 Recall),排序阶段要的是"把对的放前面"(高精度)。
一句话想通后,很多设计就顺了:
- 为什么召回想多要几路?——为了 Recall;
- 为什么召回了还要 Rerank?——为了把对的排前面;
- 为什么 RRF 只看排名?——因为在"排序"这个目标上,排名比原始分数更可比。
下一篇讲 Rerank 和查询改写:为什么 Cross-Encoder 更准却更慢、HyDE 什么情况下会把检索带偏、以及为什么多轮对话里每一轮的 Query 都必须"自包含"。