第 17 章 检索增强生成(RAG)
第 17 章 检索增强生成(RAG)
大模型的参数里存着训练截止日期之前的世界,但没有存着你公司的内部文档、昨天的工单、刚刚发布的接口规范。RAG(检索增强生成)解决的就是"如何在不重训模型的前提下,把外部知识接进生成过程",本章给出它的三阶段架构、每一步的数学与工程权衡,以及可量化的评估方法。
学习目标
读完本章,你应当能够:
- 判断一个场景该用 RAG、微调还是长上下文,并给出成本量级对比;
- 推导带重叠切分的块数公式,并完成字符与 token 之间的换算算例;
- 说明 BM25、双塔稠密检索、交叉编码器重排三者的定位与差异;
- 解释 IVF、HNSW、PQ 三种 ANN 索引的核心思想与精度-速度权衡;
- 用余弦相似度完成检索打分,并判断匹配等级;
- 说明上下文拼接顺序、查询改写与引用归因的工程影响;
- 为 RAG 系统选择检索侧与生成侧的评估指标。
前置知识检查
| 你需要知道 | 在哪一节补齐 |
|---|---|
| 内积、L2 范数、余弦相似度、欧氏距离 | 见第 2 章 2.2、2.3 节 |
| 归一化向量下内积与余弦等价、与欧氏距离的单调关系 | 见第 2 章 2.3 节;本章 17.6 节复述 |
| 词嵌入、Tokenization(BPE) | 见第 14 章 |
| 注意力复杂度、上下文窗口 | 见第 15 章;本章 17.2、17.7 节 |
| 召回率、MRR 类排序指标 | 见第 7 章评价指标;本章 17.8 节给出定义 |
| Softmax 用于打分归一化 | 见第 5 章、第 9 章 |
17.1 为什么需要 RAG
17.1.1 四类问题
大模型的原始形态有四个与知识相关的短板:
- 时效性:模型的知识冻结在训练数据截止时间。今天发布的政策、昨天上线的接口,模型一无所知。
- 幻觉(hallucination):当模型不知道答案时,它不会说"不知道",而是生成一个看起来合理的答案。因为语言模型的训练目标是"生成流畅的下文",而不是"在有把握时才断言"。
- 私有数据:企业内部文档、客服工单、代码库不可能进入公开预训练语料。
- 可归因性:即便模型答对了,也无法指出依据来自哪里;在合规场景中,“没有出处的答案"约等于"没有答案”。
RAG 的思路是把这四件事一次性绕过去:不去改模型参数,而是在每次提问时把相关的外部文本检索出来,拼进提示词(prompt),让模型"看着材料答题"。模型从"知识库"退回成"阅读理解 + 语言组织器"。
17.1.2 微调 vs RAG vs 长上下文
| 方案 | 做法 | 知识更新 | 单次成本 | 擅长 | 不擅长 |
|---|---|---|---|---|---|
| 提示词工程 | 全靠指令 | 无 | 极低 | 格式与风格控制 | 引入新知识 |
| 微调(SFT/LoRA) | 用领域语料改参数 | 需重训 | 训练高、推理低 | 风格、术语、固定任务模式 | 频繁变更的事实;易幻觉出旧值 |
| RAG | 检索后拼接 | 更新索引即可 | 每次推理多一点输入 token | 事实问答、可溯源、私有数据 | 需要跨文档推理的综合任务 |
| 长上下文 | 把全部材料塞进窗口 | 无需索引 | 随长度二次增长 | 小规模、一次性材料 | 大规模语料;成本与"中间遗忘" |
| 微调 + RAG | 两者组合 | 索引即时 + 参数稳固 | 中 | 工业界最常见形态 | 系统复杂度最高 |
成本量级直觉:RAG 的增量成本是"输入 token 变多",随每次调用线性变化;微调的增量成本是"训练一次",与调用量无关但一次性很高。因此:
- 知识变更频繁(每天有新工单)→ RAG,因为重建索引远比重训模型便宜;
- 知识稳定且需要内化(行业术语、输出风格)→ 微调;
- 调用量极大且材料量小 → 考虑把材料直接放进提示词,省掉检索链路的复杂度;
- 两者都需要 → 微调教"怎么说",RAG 提供"依据什么说"。
注意
长上下文不能替代 RAG,原因有两个:其一,注意力复杂度是 O(N²)(第 15 章),把 100 万 token 全塞进去的单次推理成本远高于检索出 4 千 token;其二,模型对长上下文中间部分的利用率明显低于首尾(lost-in-the-middle,见 17.7 节),“塞进去"不等于"用得上”。
17.2 三阶段架构
| |
三个阶段的分工与失败模式:
| 阶段 | 关键决策 | 典型失败模式 |
|---|---|---|
| 索引 | 切分粒度、重叠、嵌入模型、元数据字段 | 块太大导致噪声稀释;块太小导致语义断裂;嵌入模型与领域不匹配 |
| 检索 | 召回方式、k 的大小、是否重排、是否混合 | 召回了"字面相似但语义无关"的块;正确块排名靠后没进提示词 |
| 生成 | 顺序、模板、引用要求、是否允许说"不知道" | 忽略证据凭记忆作答;被中间位置的材料干扰 |
重要
RAG 的失败大多不在生成阶段,而在检索阶段:“答案不在提示词里,模型不可能答对”。排错时应先检查召回率(正确块是否被检索到),再检查生成质量。这是 RAG 工程师最重要的一条纪律。
17.3 文本切分与重叠
17.3.1 三种切分策略
固定长度切分:按 token 或字符数硬切。实现最简单、块长度整齐(利于批处理),但会在句子、表格、代码块中间切断,产生语义不完整的块。
按语义/结构切分:按段落、标题层级、Markdown 分节、代码函数边界切分。块内语义完整,但块长度分布极不均匀——有的段落 20 token,有的 5000 token,超出嵌入模型的最大长度时必须二次切分。
递归切分(recursive splitting):也是工程上最常用的折中。按"分隔符优先级列表"逐级尝试,先试最粗的边界,若块仍超长就退到更细的边界:
| |
这样能尽量保住大结构,同时保证每个块不超过长度上限。
为什么必须重叠? 因为切分点是人为的。若某个关键结论横跨切分点(例如"因此,故障的根因是……"),两边的块各自都不完整,检索时两边都匹配度不高。重叠让边界信息在两块中都出现一次,代价是存储与嵌入成本上升。
17.3.2 块数公式推导
设总长度 T(token),块大小 C,相邻块重叠 O(0 ≤ O < C)。
因为相邻块共享 O 个 token,所以滑窗每前进一步,新覆盖的长度是 C − O:
| |
第 1 块覆盖 C 个 token,此后每块新增 C − O 个。要让总覆盖达到 T,需要
| |
解出 k:
| |
警告
两个必须避免的错误:① 用 T/C 或 ⌈T/C⌉——那是无重叠的块数,忽略了重叠带来的额外块;② 忘记"首块单独算"这个 1 + 的结构。注意 (C − O) 越小(重叠越大),块数增长越快:O → C 时块数趋于无穷,这是重叠比例不能设太大的原因。
17.3.3 完整算例:字符 → token → 块数
题目(【09-04 选择 Q6】):某文档共 4800 个中文字符,Tokenizer 编码比例约为 1 中文字符 ≈ 0.7 token。设置 chunk 大小 1024 token、重叠 128 token,至少需要切分为多少个 chunk?
| |
答案:4 块。
反向验算重叠总量:4 块总 token 数 4 × 1024 = 4096,其中新增 3360,故重复内容 4096 − 3360 = 736 = 3 × 128(3 个相邻接缝各重叠 128),自洽。
常见错解归因:
| 错解 | 得到的数 | 错在哪 |
|---|---|---|
⌈3360/1024⌉ = 4(碰巧对) | 4 | 方法全错:既忽略重叠又忽略"首块单独算",只是因为两个误差方向相反在此题中抵消 |
⌊3360/1024⌋ = 3 | 3 | 向下取整且忽略重叠 |
| 忘记字符→token 换算 | 1 + ⌈3776/896⌉ = 6 | 直接用 4800 当 token 数 |
提示
遇到这类题先写公式再代数字。上面 ⌈3360/1024⌉ 碰巧等于 4 这件事说明:靠"感觉差不多"做题在换一组参数后必然出错。换 C = 512, O = 64, T = 3360 检验:1 + ⌈2848/448⌉ = 1 + 7 = 8,而 ⌈3360/512⌉ = 7,两者不再相等。
17.4 检索:稀疏、稠密与混合
17.4.1 稀疏检索与 BM25
稀疏检索用"词是否出现"作为特征,向量维度等于词表大小、绝大多数分量为 0。最经典的是 BM25,它给每个查询词 q_i 对文档 d 的贡献打分再求和:
| |
三个部件的直觉:
IDF:稀有词更重要。“的"“是"出现在几乎所有文档中,IDF → 0,对排序没有贡献;“宕机"只出现在少数文档中,IDF大。- 词频饱和项
f/(f + k₁·…):词出现 1 次到 5 次很重要,5 次到 50 次几乎没区别。它抑制了"堆砌关键词”。 - 长度归一化
|d|/avgdl:长文档天然更容易命中查询词,需要除以长度做惩罚。
BM25 的优势是精确匹配能力强:型号、错误码、人名、缩写这类"稀有但决定性"的字符串,稀疏检索往往比稠密检索更可靠。它无需训练、可解释、可增量更新。
17.4.2 稠密检索与双塔
稠密检索把查询和文档各自编码成固定维度的向量,用向量相似度衡量语义相关:
| |
之所以叫"双塔”,是因为查询塔与文档塔不交互——文档向量可以离线算好存进索引。这正是它能支撑十亿级检索的关键:在线只算一次查询编码 + 一次向量近邻查找,而不是把查询与每个文档拼起来过一遍模型。
代价是表达力受限:查询与文档在编码阶段从未"见面”,无法建模细粒度的词级交互。它能召回"意思相近"的文档(哪怕用词完全不同),但对"数字、否定、精确约束"不敏感。
17.4.3 交叉编码器重排
交叉编码器(cross-encoder)把查询和文档拼成一个输入,让模型做逐 token 的交互后输出一个相关性分数:
| |
它精度显著高于双塔,但无法预计算:每个 (查询, 文档) 对都要单独跑一次前向,成本随候选数线性增长。因此工程上的分工是:
| |
为什么这个两段式结构是对的? 因为"找出所有可能相关的"和"精确判断哪个最相关"是两个不同的目标,前者要求高召回(不能漏)、可离线、可近似;后者要求高精度(不能错)、可交互、代价高。把两件事交给同一个模型,要么慢到不可用,要么准到不够。
17.4.4 混合检索
**混合检索(hybrid retrieval)**同时跑稀疏与稠密两路,再融合结果。最常见的融合是 RRF(Reciprocal Rank Fusion,倒数排名融合):
| |
RRF 只使用排名而不使用原始分数,因此天然回避了"BM25 分数与余弦相似度量纲不同、无法直接加权"的问题——这是一个很实用的设计:分数不可比时,就只比较名次。
| 场景 | 更该依赖 |
|---|---|
| 错误码、型号、专有名词、罕见术语 | 稀疏(BM25) |
| 同义改写、口语提问、跨语言 | 稠密 |
| 生产系统(默认) | 混合 + RRF + 交叉编码器重排 |
17.5 向量索引与近似最近邻
设向量库有 n 条、维度 d。精确检索(暴力检索,flat) 是把查询与全部 n 条逐个算相似度,复杂度 O(n·d)。n = 10⁸、d = 1024 时单次查询是千亿次浮点运算,在线不可接受。于是有了近似最近邻(Approximate Nearest Neighbor, ANN):牺牲少量召回率换取数量级的速度提升。
| 索引 | 核心思想 | 时间复杂度(查询) | 内存 | 精度 | 适用 |
|---|---|---|---|---|---|
| Flat(暴力) | 全部算一遍 | O(n·d) | 原始向量 | 100% | n ≤ 10⁵,或作为评测基准 |
| IVF(倒排文件) | 先聚类,只在最近的若干簇里搜 | O(nprobe·n/nlist·d) | 原始向量 + 聚类中心 | 中高(可调) | 百万~十亿级,召回可调 |
| HNSW(分层可导航小世界图) | 多层图,上层稀疏用于快速跳转、下层稠密用于精确定位 | 近似 O(log n) | 原始向量 + 图边(额外开销大) | 很高 | 低延迟高召回场景 |
| PQ(乘积量化) | 向量切段,每段用一个小码本近似,查表算距离 | O(n·m)(m 为段数,m ≪ d) | 压缩到 1/8 甚至更低 | 中(有损) | 内存受限的超大规模 |
IVF 的直觉:先把 n 个向量用 k-means 聚成 nlist 个簇(类比"把图书馆分成若干书架"),查询时只访问与查询最近的 nprobe 个簇(“只去最像的几个书架找”)。检索精度随 nprobe 单调上升,速度随 nprobe 下降——这是一个可以在线调节的旋钮。
HNSW 的直觉:构建一个多层图,最上层节点极少、边长很长(用于跨越大距离),越往下节点越密(用于精确定位)。检索从顶层入口开始贪心地向查询点靠近,逐层下降细化。它把"找最近邻"变成了"在一个设计良好的图上做贪心游走",因此查询复杂度近似对数级。代价是图结构本身占用大量内存(每条边都是一个指针),且插入新点需要维护图。
PQ 的直觉:把 d = 128 维向量切成 m = 16 段,每段 8 维,对每段单独做 k-means 得到 256 个中心,于是每段只需 8 bit 存"属于哪个中心"。整个向量从 128 × 32 bit = 4096 bit 压缩到 16 × 8 = 128 bit,压缩 32 倍。距离计算变成"查 m 张表再相加",极快。代价是量化误差——它是有损压缩,且压缩后的距离是近似值,排序会出错。工程上常用"PQ 先粗筛 + 原始向量精算"的混合方案。
注意
ANN 的本质是在召回率、延迟、内存三者之间选一个工作点。没有任何 ANN 索引能在三者上同时最优。选型时应先测"暴力检索的 Top-k 结果"作为金标准,再测各索引在目标 k 下的 Recall@k 是否满足要求(见 17.8 节),而不是凭直觉选 HNSW。
17.6 相似度度量
三种度量在内积层面统一:
| |
结论一:归一化后内积与余弦等价。 若 ‖x‖ = ‖y‖ = 1,则 xᵀy / (‖x‖‖y‖) = xᵀy。这就是为什么工程上普遍在嵌入后做 L2 归一化——归一化之后可以直接用内积(矩阵乘法,最快),而不必为每个向量额外算范数。
结论二:归一化后欧氏距离与余弦是单调关系。 由第 2 章的展开式(08-05 Q4、09-04 Q2 的原型):
| |
因此 cosθ 越大,‖x − y‖² 越小,且这是严格的线性单调关系。推论:在归一化向量的前提下,“内积最大"与"欧氏距离最小"给出完全相同的排序,这是向量库可以把内积检索与欧氏检索视为可互换的依据。
结论三:未归一化时两者不等价。 此时 xᵀy 会被向量的长度污染:一个内容无关但范数极大的文档可能因内积大而排到前面。只有当嵌入模型是"内积对齐"训练的时候(部分模型如此),才应保留未归一化的内积。
17.6.1 匹配等级算例(【08-05 选择 Q10】)
题目:查询向量 q = [0.8, 1.2, 1.6, 2.0],历史故障案例向量 c = [1.0, 0.8, 1.4, 1.8]。求余弦相似度(保留 3 位小数)并判断匹配等级(≥ 0.95 极高、0.85~0.95 高、< 0.85 中等)。
| |
判读要点:观察两个向量的分量,c ≈ 1.0~1.25 × q 的逐分量比例,方向几乎重合,因此可以先预判余弦接近 1,再看谁的长度更大——‖c‖ = 2.615 < ‖q‖ = 2.939,说明长度差异不影响余弦值,只会影响内积。
| 干扰项 | 数值 | 错因 |
|---|---|---|
| A | 0.842 | 把分母少乘/多乘了某个范数,或漏加某个分量 |
| B | 0.989 | 正确 |
| C | 0.948 | 落在高匹配区间,是把某个分量算错(例如把 3.60 写成 3.06) |
| D | 0.896 | 高匹配区间,常见于只对一个向量开根号(分母变成 √8.64 × 6.84) |
警告
余弦相似度只衡量方向,与向量长度无关(第 2 章结论)。因此"检索到的文档很长所以分数高"这个担忧在余弦度量下不成立;反过来说,如果系统用的是未归一化的内积,长度就会污染排序——这是选度量时必须明确的第一个决策。
17.7 嵌入模型选择与生成阶段
17.7.1 嵌入模型的四个决策
| 决策 | 依据 | 常见坑 |
|---|---|---|
| 模型选型 | 领域匹配度(中文/代码/多语言/医疗)、检索基准(MTEB 类)排名、是否支持非对称检索 | 用生成模型的隐藏层当嵌入、用通用模型做代码检索 |
维度 d | 高维表达力强但索引大、检索慢;低维(256/384)省内存可加速 | 盲目追高维;忽略"高维模型的低维截断”(MRL)是可选项 |
| 是否归一化 | 做余弦就归一化;模型声明内积对齐则不要归一化 | 训练时归一化了、检索时忘了归一化 |
| 是否需要指令前缀 | 部分模型要求查询加前缀(如"为这个问题检索相关段落:")、文档不加 | 训练有前缀而推理没加,导致分布不匹配、召回骤降 |
关键纪律:查询与文档必须用同一个嵌入模型的正确接口编码(同一版本、同一前缀约定、同一归一化)。混用两个模型的向量在几何上毫无意义,但代码不会报错——它会安静地给出垃圾结果。
17.7.2 上下文拼接顺序
lost-in-the-middle:把 k 个检索片段拼进提示词后,模型对开头和结尾片段的利用率显著高于中间片段。即使正确证据被检索到了,若它落在中间位置,模型仍可能忽略它并凭参数记忆作答。
工程对策:
- 把最相关的放首尾——按得分排序后做"两端插入":第 1 名的放最前,第 2 名的放最后,其余按序放中间;
- 减少片段数量:
k从 20 降到 5,噪声同比下降,模型更不容易分心; - 给每个片段加显式编号与来源标记,让引用可定位;
- 明确指令:“仅根据下列材料回答;材料中若无答案,回答’资料中未提及’"。
17.7.3 查询改写与多跳检索
用户的原始提问往往不适合直接检索。常见改写手段:
| 手段 | 做法 | 解决的问题 |
|---|---|---|
| 查询扩展 | 生成若干同义改写,多路检索后融合 | 单次表述覆盖不到正确文档 |
| HyDE | 先让模型生成一段"假想答案”,用它的向量去检索 | 问题短、文档长的语义鸿沟 |
| 查询分解 | 把复杂问题拆成若干子问题分别检索 | 多跳问题(“A 公司 CEO 的母校在哪"需要两跳) |
| 对话改写 | 用历史对话把"它呢?“补全成完整问题 | 多轮对话中的指代消解 |
| 自省检索 | 生成后判断证据是否充分,不足则再检索一轮 | 一次性检索不完备 |
**多跳检索(multi-hop retrieval)**的定义是:答案需要串联多条信息,且后一跳的查询依赖前一跳的结果。它无法用单次向量检索完成,必须做成"检索 → 推理出下一个查询 → 再检索"的循环。这也是 RAG 与 Agent 结合的主要切入点。
17.7.4 引用与归因
生产级 RAG 必须输出引用(citation):每个断言标注来自第几个片段。它有三重价值:可核查(用户能自己验证)、可调试(出错时能定位是检索错还是生成的错)、可合规(审计要求)。
实现方式从弱到强:① 提示词里要求模型输出片段编号;② 逐句生成后做"答案-片段"的 NLI/蕴含判定,给每句挂证据;③ 训练专门的引用模型。注意:模型自报的引用可能造假(引用了不支持的片段),因此高要求场景必须做第 ② 类的独立校验。
17.8 RAG 评估
RAG 有两个可独立评估的环节,必须分开测——否则无法定位问题。
17.8.1 检索侧
| 指标 | 定义 | 表达式 | 关注点 |
|---|---|---|---|
| Recall@k | 前 k 个结果中是否包含全部相关文档 | 相关文档被召回数 / 相关文档总数 | 上限:召回不到就不可能答对 |
| Precision@k | 前 k 个中有多少相关 | 前 k 中相关数 / k | 提示词噪声量 |
| MRR | 第一个相关结果排名的倒数 | (1/|Q|)·Σ_q 1/rank_q | 首个正确结果的靠前程度 |
| NDCG@k | 考虑多个相关文档及其等级折扣 | DCG@k / IDCG@k,DCG@k = Σ_i (2^{rel_i}−1)/log₂(i+1) | 多相关、带相关性强弱 |
| Hit Rate | 前 k 中至少有一个相关的查询占比 | 命中查询数 / 查询总数 | 宽松但有参考价值 |
为什么 Recall@k 最重要? 因为它是生成质量的上界。若正确文档没被召回,再怎么调生成提示词都无济于事。工业排错顺序固定为:先看 Recall@20,再看 Rerank 后的 NDCG@5,最后才看生成质量。
MRR 与 NDCG 的分工:MRR 只关心"第一个对的排多前”,适合"只有一个正确答案"的场景(FAQ);NDCG 关心"多个相关文档的排序是否合理”,适合"多个文档都有用"的场景(综述型问答)。
17.8.2 生成侧
| 指标 | 定义 | 怎么测 |
|---|---|---|
| 忠实度(faithfulness) | 答案中的断言是否被检索证据支持 | 把答案拆成原子断言,逐条做蕴含判定(NLI 模型或 LLM-as-judge) |
| 答案相关性(answer relevance) | 答案是否真正回答了问题 | 反向生成:从答案反推问题,看与原始问题的相似度 |
| 上下文相关性(context relevance) | 检索到的片段是否与问题相关 | 逐片段打分,衡量噪声比例 |
| 幻觉率(hallucination rate) | 不被证据支持的断言占比 | 未被支持断言数 / 总断言数 |
| 拒答正确率 | 材料无答案时是否正确说"不知道" | 构造"不可回答"测试集 |
忠实度与答案相关性必须同时看:一个模型可以给出忠实但答非所问的答案(只复述材料里最容易的一段),也可以给出相关但没有依据的答案(凭记忆答对了)。单看一个指标都会被误导。
17.8.3 端到端框架
主流做法是把上述指标组织成一条流水线(RAGAS、TruLens、ARES 类框架的思路一致):
| |
重要
不要只看端到端正确率。 端到端指标是黑箱:它下降了,你不知道是切分变差、嵌入模型换错、还是生成提示词退化。分层指标(检索侧 + 生成侧)才能把归因做出来,这也是把 RAG 当作可调系统而非单次调用的前提。
本章小结
| 关键结论 | 一句话表述 | 常见错误 |
|---|---|---|
| RAG 的定位 | 不改参数,把外部证据拼进提示词 | 以为 RAG 能替代所有微调需求 |
| 切分块数 | k = 1 + ⌈(T−C)/(C−O)⌉ | 用 ⌈T/C⌉ 忽略重叠 |
| 字符↔token | 必须先把字符数乘上 token 比例 | 直接用字符数当 token 数 |
| 检索两段式 | ANN/BM25 高召回粗排 → 交叉编码器高精度重排 | 用交叉编码器直接扫全库 |
| 混合检索 | 稀疏 + 稠密,用 RRF 按名次融合 | 直接加权两种不同量纲的分数 |
| ANN 三选一 | IVF/HNSW/PQ 是召回-延迟-内存的三角权衡 | 以为存在全面最优的索引 |
| 相似度 | 归一化后内积 = 余弦;‖u−v‖² = 2 − 2cosθ | 内积检索却不归一化 |
| 拼接顺序 | 最相关的片段放首尾,规避 lost-in-the-middle | 把所有片段按分数顺序平铺 |
| RAG 评估 | 检索侧看 Recall@k,生成侧看忠实度与幻觉率 | 只看端到端正确率 |
自测题
Q1(计算)某英文文档共 12000 个字符,1 字符 ≈ 0.25 token。设置 chunk = 512 token、overlap = 64 token,至少需要多少块?若把 overlap 改成 128,块数变成多少?
答案与解析
| |
答案:7 块和 8 块。
要点:重叠从 64 加到 128,块数只从 7 增到 8,但重复存储的 token 总量从 6 × 64 = 384 增到 7 × 128 = 896,嵌入与存储成本增加一倍以上。这说明重叠的代价主要不在块数,而在重复量。
Q2(计算)已知 q = [3, 0, 4]、d = [6, 0, 8]。分别计算内积、余弦相似度与欧氏距离,并说明为什么在这个例子里"内积最大"和"余弦最大"会给出完全不同的结论。
答案与解析
| |
用 ‖u−v‖² = 2 − 2cosθ 只对单位向量成立,这里两个向量都不单位,所以要用完整展开式:‖q−d‖² = ‖q‖² + ‖d‖² − 2qᵀd = 25 + 100 − 100 = 25 ✓
为什么结论会不同:d 与 q 方向完全相同,余弦都是 1,因此余弦度量下两者等价;但若库里还有另一个向量 d' = [0, 10, 0](范数 10,与 q 正交,余弦 0),d 的内积 50 远大于 d' 的内积 0,看似没问题。构造反例:d'' = [0, 20, 0],内积仍是 0。真正的分歧出现在长度不同但方向相似时:q = [3,0,4] 与 d''' = [300,0,400],余弦仍为 1,但内积变成 5000。若用内积排序,d''' 会压过任何内容更相关但范数小的文档——排序被长度主导,这就是必须归一化的理由。
Q3(辨析)判断:“Top-K 检索里 k 越大越好,因为召回率单调上升。“请指出这句话的问题,并说明生产中如何选 k。
答案与解析
召回率确实随 k 单调上升(这是定义决定的),但端到端质量不会。原因有三:
- 噪声稀释:更多片段意味着更多不相关内容进入提示词,模型更容易被误导;配合 lost-in-the-middle,额外片段常常只是占位。
- 成本:输入 token 数随
k线性增长,推理成本与延迟同步上升。 - 位置挤压:
k变大后正确片段更容易落在"中间被忽略"的位置。
正确的选法是把 k 分成两个数:召回阶段的 k_recall 取大一些(如 50200),保证 Recall@k_recall 满足要求;送进提示词的 k_context 取小(如 38),由交叉编码器重排后的 Top-n 决定。即”用大 k 保召回,用小 n 保信噪比",两个目标由两段式架构分别承担。
Q4(辨析/设计)某 RAG 系统上线后用户反馈"答案经常不对”。日志显示检索到的片段里有正确证据的概率只有 40%。请给出你的排错顺序,并说明每一步该看哪个指标。
答案与解析
由于检索是生成的上界,第一步必须解决检索,而不是调生成提示词。排错顺序:
- 确认评估集与指标:先构造带标准证据片段的测试集,测 Recall@k。没有评估集就没有排错依据。
- 检查切分:正确证据所在的知识是否被切断了(跨块断裂)?把
k加大后 Recall 是否上升?若是,考虑增大重叠或改用递归切分。 - 检查嵌入与归一化:查询与文档是否用了同一模型、同一版本、同一前缀约定、同一归一化?这里出错时程序不报错,只会安静地给出垃圾结果,必须逐项核对。
- 检查检索方式:如果查询包含型号、错误码等稀有字符串,稠密检索容易漏——加权 BM25 做混合检索往往能直接把 Recall 抬上去。
- 检查重排:若 Recall@50 高但 Recall@5 低,说明粗排召回到了但精排排错了,应该上交叉编码器重排。
- 检索指标达标后再看生成侧(忠实度、幻觉率),此时才轮到提示词与拼接顺序。
关键判断:要区分"没召回到"(Recall@大k 低 → 索引/切分/嵌入问题)与"召回到了但没排上来"(Recall@大k 高、NDCG@小k 低 → 重排问题)。两者的修复手段完全不同,这是这个指标分层设计的全部价值。
Q5(辨析)IVF、HNSW、PQ 分别在牺牲什么?请说明"用 PQ 索引后召回率下降"和"用 HNSW 后召回率下降"这两种现象的成因有何本质区别。
答案与解析
- IVF 牺牲的是搜索空间的完整性:只访问
nprobe个簇,若正确结果落在未被访问的簇里就永久丢失。它是搜索范围上的近似,nprobe可调,内存几乎不增加。 - HNSW 牺牲的是精确性换延迟:图是启发式构建的,贪心游走可能陷入局部最优而错过真正的最近邻。通常召回很高,但其代价在内存(图边结构开销大)。
- PQ 牺牲的是向量的表示精度:向量被量化成码本索引,距离计算是近似值。它是表示上的有损压缩,换来的是数量级的内存节省。
因此两种"召回率下降"有本质区别:
- PQ 的下降是系统性的、与数据无关的:即使把候选集扩大到全库,用 PQ 近似距离排序仍会错——因为距离本身算错了。补救方式是"PQ 粗筛 + 用原始向量重算精排"(re-ranking with exact vectors),精度可以补回来。
- HNSW 的下降是数据与参数相关的:提高
efSearch(搜索时的候选队列宽度)或重建图,召回会回升,因为向量表示本身没被破坏。它是"没找到",不是"算错了"。
这个区分直接决定了修复手段:PQ 要补一次精确重算,HNSW 要调搜索参数。
Q6(辨析)为什么 RRF 用"排名"而不是"分数"来融合稀疏与稠密检索结果?直接对 BM25 分数和余弦相似度加权求和有什么问题?
答案与解析
两个原因。
其一,量纲与尺度不可比。 BM25 分数是无上界的(取决于 IDF、文档长度、k₁、b),不同查询的分数范围差异极大;余弦相似度被限制在 [−1, 1],实际检索结果常集中在 [0.6, 0.95] 这个窄区间。直接加权求和时,某一路的分数尺度会主导排序,权重系数因此失去可解释性,且需要为每个查询分布重新调参。
其二,分布形状不同。 BM25 的分数差别可能主要来自文档长度而非相关性;余弦分数的差异可能整体很小。分数做归一化(min-max、z-score)又依赖当前结果集,同一文档在不同查询下的归一化分数不一致,导致融合结果不稳定。
RRF 用 1/(k + rank) 只保留名次信息,天然对单调变换免疫:只要某一路内部的排序是可靠的,融合就是稳定的——这正好匹配"两路检索器各自擅长的场景不同、内部排序都比绝对分数可信"的现实。
代价是丢失了分数所携带的置信度信息:一个以压倒性优势排第一的文档与勉强排第一的文档在 RRF 下没有区别。因此也有做法先用两路分数的校准(如保序回归)再融合,属于更精细但更繁琐的替代方案。
与真题的关联
| 真题 | 考查角度 | 对应本章小节 |
|---|---|---|
【09-04 选择 Q6】 带重叠 chunk 数量 | 先字符→token 换算,再用 1 + ⌈(T−C)/(C−O)⌉ | 17.3.2、17.3.3 |
【08-05 选择 Q10】 语义检索余弦相似度与匹配等级 | 未归一化向量的余弦计算、≥0.95 等级判定 | 17.6.1 |
【09-04 选择 Q2】(前置)单位向量下距离与余弦的关系 | ‖u−v‖² = 2 − 2cosθ,解释归一化后两度量等价 | 17.6 |
【09-04 选择 Q6】(关联)中文字符与 token 比例 | 第 14 章 Tokenization,与本章换算算例共用 | 17.3.3 |
延伸阅读
- Patrick Lewis 等,《Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks》,2020——RAG 的原始论文。
- Stephen Robertson、Hugo Zaragoza,《The Probabilistic Relevance Framework: BM25 and Beyond》,2009。
- Vladimir Karpukhin 等,《Dense Passage Retrieval for Open-Domain Question Answering》,2020——双塔稠密检索。
- Yury A. Malkov、Dmitry A. Yashunin,《Efficient and Robust Approximate Nearest Neighbor Search Using HNSW Graphs》,2018。
- Hervé Jégou 等,《Product Quantization for Nearest Neighbor Search》,2011。
- Nelson F. Liu 等,《Lost in the Middle: How Language Models Use Long Contexts》,2023。
- Luyu Gao 等,《Precise Zero-Shot Dense Retrieval without Relevance Labels》(HyDE),2023。
- Nandan Thakur 等,《BEIR: A Heterogeneous Benchmark for Zero-shot Evaluation of Information Retrieval Models》,2021;Shahul Es 等,《RAGAS: Automated Evaluation of Retrieval Augmented Generation》,2023。