第 17 章 检索增强生成(RAG)

大模型的参数里存着训练截止日期之前的世界,但没有存着你公司的内部文档、昨天的工单、刚刚发布的接口规范。RAG(检索增强生成)解决的就是"如何在不重训模型的前提下,把外部知识接进生成过程",本章给出它的三阶段架构、每一步的数学与工程权衡,以及可量化的评估方法。

学习目标

读完本章,你应当能够:

  1. 判断一个场景该用 RAG、微调还是长上下文,并给出成本量级对比;
  2. 推导带重叠切分的块数公式,并完成字符与 token 之间的换算算例;
  3. 说明 BM25、双塔稠密检索、交叉编码器重排三者的定位与差异;
  4. 解释 IVF、HNSW、PQ 三种 ANN 索引的核心思想与精度-速度权衡;
  5. 用余弦相似度完成检索打分,并判断匹配等级;
  6. 说明上下文拼接顺序、查询改写与引用归因的工程影响;
  7. 为 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 四类问题

大模型的原始形态有四个与知识相关的短板:

  1. 时效性:模型的知识冻结在训练数据截止时间。今天发布的政策、昨天上线的接口,模型一无所知。
  2. 幻觉(hallucination):当模型不知道答案时,它不会说"不知道",而是生成一个看起来合理的答案。因为语言模型的训练目标是"生成流畅的下文",而不是"在有把握时才断言"。
  3. 私有数据:企业内部文档、客服工单、代码库不可能进入公开预训练语料。
  4. 可归因性:即便模型答对了,也无法指出依据来自哪里;在合规场景中,“没有出处的答案"约等于"没有答案”。

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 三阶段架构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
                     ┌─────────── 离线:索引(Indexing)───────────┐
  原始文档 ──► 解析/清洗 ──► 切分(chunking) ──► 嵌入(embedding) ──► 向量库 + 元数据
                     └──────────────────────────────────────────────┘
                                        │
  用户查询 ──► 查询改写 ──► ┌─────────── 在线:检索(Retrieval)───────────┐
                            │  粗排:向量 ANN / BM25 → Top-k 候选           │
                            │  精排:交叉编码器 rerank → Top-n              │
                            └──────────────────────────────────────────────┘
                                        │
                     拼装提示词(指令 + 证据 + 提问) ──► 生成(Generation) ──► 答案 + 引用

三个阶段的分工与失败模式:

阶段关键决策典型失败模式
索引切分粒度、重叠、嵌入模型、元数据字段块太大导致噪声稀释;块太小导致语义断裂;嵌入模型与领域不匹配
检索召回方式、k 的大小、是否重排、是否混合召回了"字面相似但语义无关"的块;正确块排名靠后没进提示词
生成顺序、模板、引用要求、是否允许说"不知道"忽略证据凭记忆作答;被中间位置的材料干扰

重要

RAG 的失败大多不在生成阶段,而在检索阶段:“答案不在提示词里,模型不可能答对”。排错时应先检查召回率(正确块是否被检索到),再检查生成质量。这是 RAG 工程师最重要的一条纪律。


17.3 文本切分与重叠

17.3.1 三种切分策略

固定长度切分:按 token 或字符数硬切。实现最简单、块长度整齐(利于批处理),但会在句子、表格、代码块中间切断,产生语义不完整的块。

按语义/结构切分:按段落、标题层级、Markdown 分节、代码函数边界切分。块内语义完整,但块长度分布极不均匀——有的段落 20 token,有的 5000 token,超出嵌入模型的最大长度时必须二次切分。

递归切分(recursive splitting):也是工程上最常用的折中。按"分隔符优先级列表"逐级尝试,先试最粗的边界,若块仍超长就退到更细的边界:

1
分隔符优先级:  段落(\n\n)  →  换行(\n)  →  句号/问号/叹号  →  逗号  →  空格  →  字符

这样能尽量保住大结构,同时保证每个块不超过长度上限。

为什么必须重叠? 因为切分点是人为的。若某个关键结论横跨切分点(例如"因此,故障的根因是……"),两边的块各自都不完整,检索时两边都匹配度不高。重叠让边界信息在两块中都出现一次,代价是存储与嵌入成本上升。

17.3.2 块数公式推导

设总长度 T(token),块大小 C,相邻块重叠 O(0 ≤ O < C)。

因为相邻块共享 O 个 token,所以滑窗每前进一步,新覆盖的长度是 C − O:

1
2
3
4
5
块 1:  [0, C)
块 2:  [C−O, 2C−O)
块 3:  [2(C−O), 2(C−O)+C)
...
块 k:  起点 = (k−1)·(C−O),终点 = (k−1)·(C−O) + C

第 1 块覆盖 C 个 token,此后每块新增 C − O 个。要让总覆盖达到 T,需要

1
C + (k − 1)·(C − O) ≥ T

解出 k:

1
2
3
k ≥ 1 + (T − C) / (C − O)

块数 k = 1 + ⌈ (T − C) / (C − O) ⌉          (当 T > C;T ≤ C 时 k = 1)

警告

两个必须避免的错误:① 用 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?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
第一步  字符换算成 token
        T = 4800 × 0.7 = 3360 token

第二步  每前进一步的新增量
        C − O = 1024 − 128 = 896

第三步  代入公式
        k = 1 + ⌈ (3360 − 1024) / 896 ⌉
          = 1 + ⌈ 2336 / 896 ⌉
          = 1 + ⌈ 2.607… ⌉
          = 1 + 3 = 4

第四步  验算覆盖范围
        第 4 块起点 = 3 × 896 = 2688
        第 4 块终点 = 2688 + 1024 = 3712 ≥ 3360   ✓ 已覆盖到文末

        第 3 块终点 = 2 × 896 + 1024 = 2816 < 3360  ✗ 不够

答案:4 块。

反向验算重叠总量:4 块总 token 数 4 × 1024 = 4096,其中新增 3360,故重复内容 4096 − 3360 = 736 = 3 × 128(3 个相邻接缝各重叠 128),自洽。

常见错解归因:

错解得到的数错在哪
⌈3360/1024⌉ = 4(碰巧对)4方法全错:既忽略重叠又忽略"首块单独算",只是因为两个误差方向相反在此题中抵消
⌊3360/1024⌋ = 33向下取整且忽略重叠
忘记字符→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 的贡献打分再求和:

1
2
3
score(q, d) = Σ_i  IDF(q_i) · ( f(q_i, d)·(k₁+1) ) / ( f(q_i, d) + k₁·(1 − b + b·|d|/avgdl) )

IDF(q_i) = log( (N − n(q_i) + 0.5) / (n(q_i) + 0.5) + 1 )

三个部件的直觉:

  • IDF:稀有词更重要。“的"“是"出现在几乎所有文档中,IDF → 0,对排序没有贡献;“宕机"只出现在少数文档中,IDF 大。
  • 词频饱和项 f/(f + k₁·…):词出现 1 次到 5 次很重要,5 次到 50 次几乎没区别。它抑制了"堆砌关键词”。
  • 长度归一化 |d|/avgdl:长文档天然更容易命中查询词,需要除以长度做惩罚。

BM25 的优势是精确匹配能力强:型号、错误码、人名、缩写这类"稀有但决定性"的字符串,稀疏检索往往比稠密检索更可靠。它无需训练、可解释、可增量更新。

17.4.2 稠密检索与双塔

稠密检索把查询和文档各自编码成固定维度的向量,用向量相似度衡量语义相关:

1
2
3
4
双塔(bi-encoder):
   q ──► Encoder_q ──► e_q ∈ R^d
   d ──► Encoder_d ──► e_d ∈ R^d
   score(q, d) = cos(e_q, e_d) = e_qᵀe_d / (‖e_q‖‖e_d‖)

之所以叫"双塔”,是因为查询塔与文档塔不交互——文档向量可以离线算好存进索引。这正是它能支撑十亿级检索的关键:在线只算一次查询编码 + 一次向量近邻查找,而不是把查询与每个文档拼起来过一遍模型。

代价是表达力受限:查询与文档在编码阶段从未"见面”,无法建模细粒度的词级交互。它能召回"意思相近"的文档(哪怕用词完全不同),但对"数字、否定、精确约束"不敏感。

17.4.3 交叉编码器重排

交叉编码器(cross-encoder)把查询和文档拼成一个输入,让模型做逐 token 的交互后输出一个相关性分数:

1
2
cross-encoder:
   [CLS] query [SEP] document [SEP] ──► 相关性打分(一个标量)

它精度显著高于双塔,但无法预计算:每个 (查询, 文档) 对都要单独跑一次前向,成本随候选数线性增长。因此工程上的分工是:

1
2
3
4
5
第一阶段  粗排/召回(recall-oriented)
          双塔 ANN 或 BM25,从百万级降到 Top-50~200        要快、要召回全
              │
第二阶段  精排/重排(precision-oriented)
          交叉编码器对这 50~200 个候选重新打分并排序        要准、候选少所以算得起

为什么这个两段式结构是对的? 因为"找出所有可能相关的"和"精确判断哪个最相关"是两个不同的目标,前者要求高召回(不能漏)、可离线、可近似;后者要求高精度(不能错)、可交互、代价高。把两件事交给同一个模型,要么慢到不可用,要么准到不够。

17.4.4 混合检索

**混合检索(hybrid retrieval)**同时跑稀疏与稠密两路,再融合结果。最常见的融合是 RRF(Reciprocal Rank Fusion,倒数排名融合):

1
RRF(d) = Σ_{r ∈ 检索器集合}  1 / (k + rank_r(d))            k 常取 60

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 相似度度量

三种度量在内积层面统一:

1
2
3
内积(IP)      : s = xᵀy = Σ_i x_i·y_i
余弦(cosine)  : s = xᵀy / (‖x‖·‖y‖)
欧氏距离(L2)  : d² = ‖x − y‖² = ‖x‖² + ‖y‖² − 2xᵀy

结论一:归一化后内积与余弦等价。 若 ‖x‖ = ‖y‖ = 1,则 xᵀy / (‖x‖‖y‖) = xᵀy。这就是为什么工程上普遍在嵌入后做 L2 归一化——归一化之后可以直接用内积(矩阵乘法,最快),而不必为每个向量额外算范数。

结论二:归一化后欧氏距离与余弦是单调关系。 由第 2 章的展开式(08-05 Q4、09-04 Q2 的原型):

1
2
3
‖x − y‖² = ‖x‖² + ‖y‖² − 2xᵀy
         = 1 + 1 − 2·cosθ            (单位向量,xᵀy = cosθ)
         = 2 − 2·cosθ

因此 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 中等)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
分子(内积)
  qᵀc = 0.8×1.0 + 1.2×0.8 + 1.6×1.4 + 2.0×1.8
      = 0.80 + 0.96 + 2.24 + 3.60
      = 7.60

分母的两个范数
  ‖q‖² = 0.64 + 1.44 + 2.56 + 4.00 = 8.64   ⟹  ‖q‖ = √8.64 ≈ 2.9394
  ‖c‖² = 1.00 + 0.64 + 1.96 + 3.24 = 6.84   ⟹  ‖c‖ = √6.84 ≈ 2.6153
  ‖q‖·‖c‖ ≈ 2.9394 × 2.6153 ≈ 7.6874

余弦相似度
  cosθ = 7.60 / 7.6874 ≈ 0.9886 ≈ 0.989        ⟹  落在 ≥ 0.95 区间,属于「极高匹配」

判读要点:观察两个向量的分量,c ≈ 1.0~1.25 × q 的逐分量比例,方向几乎重合,因此可以先预判余弦接近 1,再看谁的长度更大——‖c‖ = 2.615 < ‖q‖ = 2.939,说明长度差异不影响余弦值,只会影响内积。

干扰项数值错因
A0.842把分母少乘/多乘了某个范数,或漏加某个分量
B0.989正确
C0.948落在高匹配区间,是把某个分量算错(例如把 3.60 写成 3.06)
D0.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. 把最相关的放首尾——按得分排序后做"两端插入":第 1 名的放最前,第 2 名的放最后,其余按序放中间;
  2. 减少片段数量:k 从 20 降到 5,噪声同比下降,模型更不容易分心;
  3. 给每个片段加显式编号与来源标记,让引用可定位;
  4. 明确指令:“仅根据下列材料回答;材料中若无答案,回答’资料中未提及’"。

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 类框架的思路一致):

1
2
3
4
5
测试集(问题 + 标准答案 + 标准证据片段)
   │
   ├─ 检索评估:拿标准证据比对系统召回结果  →  Recall@k / MRR / NDCG
   ├─ 生成评估:拿标准答案与检索证据评答案  →  忠实度 / 相关性 / 幻觉率
   └─ 端到端评估:整体正确率、拒答率、平均延迟、单次成本

重要

不要只看端到端正确率。 端到端指标是黑箱:它下降了,你不知道是切分变差、嵌入模型换错、还是生成提示词退化。分层指标(检索侧 + 生成侧)才能把归因做出来,这也是把 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,块数变成多少?

答案与解析
1
2
3
4
5
6
7
8
9
T = 12000 × 0.25 = 3000 token

overlap = 64:
  每步新增 = 512 − 64 = 448
  k = 1 + ⌈ (3000 − 512) / 448 ⌉ = 1 + ⌈ 2488 / 448 ⌉ = 1 + ⌈ 5.554 ⌉ = 1 + 6 = 7

overlap = 128:
  每步新增 = 512 − 128 = 384
  k = 1 + ⌈ 2488 / 384 ⌉ = 1 + ⌈ 6.479 ⌉ = 1 + 7 = 8

答案:7 块和 8 块。

要点:重叠从 64 加到 128,块数只从 7 增到 8,但重复存储的 token 总量从 6 × 64 = 384 增到 7 × 128 = 896,嵌入与存储成本增加一倍以上。这说明重叠的代价主要不在块数,而在重复量。

Q2(计算)已知 q = [3, 0, 4]、d = [6, 0, 8]。分别计算内积、余弦相似度与欧氏距离,并说明为什么在这个例子里"内积最大"和"余弦最大"会给出完全不同的结论。

答案与解析
1
2
3
4
内积      qᵀd = 3×6 + 0 + 4×8 = 18 + 32 = 50
范数      ‖q‖ = √(9+16) = 5,  ‖d‖ = √(36+64) = 10
余弦      cosθ = 50 / (5 × 10) = 1.0        (方向完全相同)
欧氏距离  ‖q−d‖² = (−3)² + 0 + (−4)² = 25  ⟹  ‖q−d‖ = 5

用 ‖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 单调上升(这是定义决定的),但端到端质量不会。原因有三:

  1. 噪声稀释:更多片段意味着更多不相关内容进入提示词,模型更容易被误导;配合 lost-in-the-middle,额外片段常常只是占位。
  2. 成本:输入 token 数随 k 线性增长,推理成本与延迟同步上升。
  3. 位置挤压:k 变大后正确片段更容易落在"中间被忽略"的位置。

正确的选法是把 k 分成两个数:召回阶段的 k_recall 取大一些(如 50200),保证 Recall@k_recall 满足要求;送进提示词的 k_context 取小(如 38),由交叉编码器重排后的 Top-n 决定。即”用大 k 保召回,用小 n 保信噪比",两个目标由两段式架构分别承担。

Q4(辨析/设计)某 RAG 系统上线后用户反馈"答案经常不对”。日志显示检索到的片段里有正确证据的概率只有 40%。请给出你的排错顺序,并说明每一步该看哪个指标。

答案与解析

由于检索是生成的上界,第一步必须解决检索,而不是调生成提示词。排错顺序:

  1. 确认评估集与指标:先构造带标准证据片段的测试集,测 Recall@k。没有评估集就没有排错依据。
  2. 检查切分:正确证据所在的知识是否被切断了(跨块断裂)?把 k 加大后 Recall 是否上升?若是,考虑增大重叠或改用递归切分。
  3. 检查嵌入与归一化:查询与文档是否用了同一模型、同一版本、同一前缀约定、同一归一化?这里出错时程序不报错,只会安静地给出垃圾结果,必须逐项核对。
  4. 检查检索方式:如果查询包含型号、错误码等稀有字符串,稠密检索容易漏——加权 BM25 做混合检索往往能直接把 Recall 抬上去。
  5. 检查重排:若 Recall@50 高但 Recall@5 低,说明粗排召回到了但精排排错了,应该上交叉编码器重排。
  6. 检索指标达标后再看生成侧(忠实度、幻觉率),此时才轮到提示词与拼接顺序。

关键判断:要区分"没召回到"(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。