1. 文档里明明有答案,模型却答错
一份三十页的产品手册里写着退款条件:"订单签收后七天内可申请全额退款,需提供订单号。"用户问"退款怎么弄",模型给出的答案却来自另一段关于换货的文字,条件说错了。
问题不在模型读不懂,而在于它读到的东西不对。把整份手册塞进上下文可以解决这一次,但手册会增长、文档会变多、每轮都带完整手册的成本会迅速超过收益。RAG 的思路是把"找相关片段"从生成里拆出来,先检索再回答。
拆分之后,质量的上限被前移到检索阶段:检索没找到正确片段,生成再强也只能基于错的材料作答。这就是为什么调 RAG 应该先调检索。
2. 管道的七个环节与两类成本
离线阶段有两步:分块与嵌入索引。在线阶段有五步:查询改写、双路召回、排名融合、精排、组装生成。
两个阶段的时间特性不同。离线可以慢,可以多次调用模型,可以重跑;在线必须快,因为用户在看。这个差异决定了投入方向——所有能用离线计算换在线质量的手段,都值得做。
把成本写成式子,选型就有依据。设语料总量 \(D\) 个 Token,块大小 \(b\) 个 Token,相邻块重叠比例 \(o\),单块嵌入价格 \(\pi\),索引存储成本 \(s\),则块数与索引成本为:
块越小,块数按 \(1/b\) 增长,嵌入成本同比上升;重叠越大,有效块数进一步增加。取 \(D = 10^7\)(一千万 Token,约一万五千页),\(b = 512\),\(o = 0.15\):
两万三千个块。这个数字决定了三件事:嵌入调用要批处理(每次几千块),索引结构要能承载两万多条(几乎任何方案都行),以及增量更新要支持按块替换(否则每加一份文档就全量重跑)。
在线侧的成本结构不同,它是四项之和:
四项依次是查询嵌入、索引检索、精排、生成。
量级上,嵌入一次查询是一次很短的前向,检索是索引查找(毫秒级),精排是前 \(k\) 个候选各一次前向(\(k\) 通常 20 到 100),生成是完整的一次模型调用。
这四项里,精排与生成占大头。所以优化在线延迟的顺序是:先减小 \(k\)(精排候选数),再考虑跳过精排(用融合结果直接生成),最后才是换更快的嵌入模型——嵌入本来就不是瓶颈。
3. 分块
分块是信息损失的第一现场。
一段话被切出来之后,"该公司""这份报告""上面提到的限制"这些指代就与原文脱钩了。嵌入模型看到的是一个语义不完整的片段,它生成的向量表达的是"一个模糊的片段",指代对象、来源与时间都不在其中。
三种常见策略。
固定大小切分。 按固定 Token 数切(常从 512 开始),相邻块保留 10% 到 20% 的重叠。实现简单、结果可预测,代价是完全无视文档结构——一个段落、一段代码、一张表格都可能被拦腰截断。重叠缓解但不消除切断带来的信息损失。
结构感知切分。 按章节标题、段落、句子这类自然分界递归切分。先尝试按大分界切,仍超长时降级到更小分界。Markdown 与 HTML 这类有显式结构的文档尤其适合。这是生产系统的常见默认。
语义切分。 计算相邻句子的嵌入相似度,在相似度骤降处下刀,让每个块内部主题尽量单一。质量更高,代价是需要额外嵌入计算,而且切点不稳定——同一份文档两次切分可能得到不同切点。
三种策略的取舍放在一张表里:
| 策略 | 索引成本 | 结果可预测性 | 对结构的利用 | 适用材料 |
|---|---|---|---|---|
| 固定大小 | 最低 | 最高 | 无 | 结构混乱的连续文本 |
| 结构感知 | 低 | 高 | 标题与段落 | Markdown、HTML、规范文档 |
| 语义切分 | 高 | 低 | 主题转折处 | 主题跳跃频繁的长文 |
固定大小与结构感知可以叠加:先按结构切到章节,章节仍超长时再按固定大小切。这个组合是多数生产系统的实际做法,因为结构感知解决"段落被切断",固定大小解决"章节太大"。
3.1 块大小的权衡
块大小是一对权衡。块太小,单块信息不完整,脱离上下文后语义模糊;块太大,一块混杂多个主题,向量被稀释,命中后还会带入无关内容。常见起点是每块 256 到 1024 Token、重叠 10% 到 20%,再按检索质量实测调整。
调整方向有一组实用信号:
- 如果检索经常命中"包含关键词但答非所问"的片段,说明块太大;
- 如果经常命中"信息对但不完整"的片段,说明块太小或重叠不足;
- 如果同一问题的检索结果在不同运行间跳动,说明切点不稳定,需要改用结构感知。
还有一个量化的判断方式。设一个块包含 \(n\) 个 Token,其中与查询相关的有 \(r\) 个,则信噪比是 \(r/n\)。块变大时 \(n\) 增长,但 \(r\) 通常不变(相关信息由一个段落承载),所以信噪比按 \(1/n\) 下降。这就是"块太大导致向量稀释"的算术解释。
反过来,块太小时 \(r\) 可能也下降——因为相关的上下文被切到了相邻块里。所以存在一个最优点,它出现在"该块刚好能自洽地表达一件事"的粒度上。这个粒度通常与文档的自然段落接近,这也是结构感知切分效果好的原因。
3.2 两类分块解决不了的问题
有一个问题分块本身解决不了:顶部-k 抽样会漏掉整体信息。知识库里有 100 个独立案例,用户问"比例是多少",即使每个案例都被正确索引,前 20 个结果也只能看到其中一部分。模型基于不完整样本算出错误结论,而且不会意识到自己没看全。
从概率上看更清楚。假设 100 个案例中 90 个是 A 类、10 个是 B 类,随机取 20 个,期望看到 18 个 A、2 个 B,比例 90%。这个例子恰好接近真值,但如果真实比例是 55 比 45,取 20 个的观察比例可能在 35% 到 75% 之间大幅波动——模型据此得出的结论基本是噪声。
解法在索引阶段:预先生成统计摘要并索引。回答"比例是多少"时,一次检索就能拿到完整数字。这条思路与后面《知识组织与更新》讲的结构化索引是同一条:把需要聚合才能得到的信息,在索引时就算好。
另有一类问题来自最近邻偏置。知识库是客服工单归档,每条记录一个个案的结论:退伍军人通过、医生拿到折扣、教师被告知不符合。用户问"护士能不能享受"时,"护士"与"医生"语义最近,医生那条排最前,模型顺势推断护士也可以;如果教师那条碰巧排得更前,同一个问题会得到相反答案。
这里缺的是"资格范围"这条规则本身,检索精度补不上——它不存在于任何单条工单里。同样只能在索引阶段补:通读全库,提炼出一条带适用范围的规则卡。
两类问题的根子在于"知识本身没有被写成可检索的形态",与检索准不准关系不大。个案散落在库里,规则不在任何一条个案里;统计数字需要遍历全部案例才能得到,而检索只能给出前 k 条。这一类需求必须靠索引期的额外计算解决,调大 k 或换模型都无效——调大 k 只会让更多个案进入上下文,模型反而更容易被最近邻偏置带偏。
4. 稠密嵌入
稠密嵌入把文本映射到高维向量空间,语义相近的内容向量距离也近。度量用余弦相似度,因为它关心方向而不关心长度——两篇内容相同但长短不同的文档,向量方向一致,余弦值接近 1。
归一化之后,余弦相似度与内积等价,所以实际系统多半把向量预先归一化,查询时只算内积。这不是微优化——内积可以直接用矩阵乘法批量算,而余弦的除法要逐条做。
早期方案(词向量)为每个词生成固定向量,无法处理一词多义。"bank"在"河岸"和"投资银行"里向量相同。上下文感知的编码器解决了这一点:同一个词在不同句子里得到不同表示,因为计算时参考了整句。
工程上要知道的三件事:
- 维数从几百到几千,存储与索引成本随之上升;
- 相似度计算是近似的,大规模时用近似最近邻索引(树结构或图结构),召回率与速度需要权衡;
- 增量更新能力因索引类型而异。树结构类索引通常需要重建,图结构类支持增量插入但长期插入后建议重建以保持精度。
4.1 两类近似索引的差别
| 维度 | 树结构 | 图结构 |
|---|---|---|
| 构建速度 | 快 | 慢 |
| 内存占用 | 低 | 较高 |
| 增量插入 | 不支持,需完全重建 | 支持 |
| 查询精度 | 较高 | 更高 |
| 适用数据 | 不常变的静态集合 | 需要持续插入的动态集合 |
选型的分界是更新频率。知识库每月更新一次,树结构更划算;每小时的工单都要入库,只能选图结构,并接受长期插入后精度下降、需要定期重建的代价。
有一个常被忽略的存储估算。设向量维度 \(d = 1024\)、用 32 位浮点存储,则每条向量占 4 KB。十万条就是 400 MB,只算原始向量;图结构索引还要额外存边,通常是原始数据的 1.5 到 2 倍。这个量级决定了向量索引通常不能放在内存受限的环境里,也解释了为什么量化(把 32 位压到 8 位甚至 4 位)是常见优化。
量化会牺牲精度。把 32 位压到 8 位,理论误差上界是每维 \(1/256\) 的相对误差,累积到 1024 维后余弦相似度的偏差可能达到千分之几。对"找相似片段"这个任务,千分之几的偏差通常无害;但对"阈值过滤"(相似度低于某值就丢弃)会有明显影响,因为阈值附近的结果会翻转。
4.2 稠密检索的弱项
稠密检索的强项是同义改写。"退款怎么弄"能命中"如何申请全额退款",即使两者没有一个共同的关键词。弱项是精确词——错误码、函数名、罕见人名,这些在语义空间里没有稳定的邻居。
这个弱项有一个具体成因:嵌入模型的词表来自通用语料。一个团队自造的缩写、刚起的函数名、某个内部系统的代号,在训练数据里没有出现过,模型只能把它拆成子词后拼出一个近似向量。同一个代号在不同上下文里会得到不稳定的向量,检索时排名随机。
工程上的缓解手段有两种:一是在查询时不依赖嵌入,把标识符交给词法检索;二是为内部词表做微调或补充同义词表。前者成本低,后者效果好但需要维护。多数系统选前者——把两路检索都跑起来,让词法侧兜住精确词。
5. 稀疏嵌入
稀疏嵌入来自信息检索传统,核心是精确词匹配。文本被表示成极高维的向量,绝大多数维度为零,只有出现的词对应的维度有值。
TF-IDF 的直觉是:一个词在当前文档里出现得越多、在整个语料里越少见,它越能区分文档。BM25 对两个局限做了修正。
第一,词频饱和。原始词频随出现次数线性增长,出现 10 次是出现 5 次的两倍。BM25 用饱和函数把这个增长压下来,重复出现的边际贡献逐渐降低。
第二,长度归一化。长文档容易仅因为字数多而获得高分。BM25 按文档长度与平均长度之比做惩罚。
BM25 的评分形式为:
其中 \(q\) 是查询、\(d\) 是文档、\(w\) 是词项,\(f(w,d)\) 是词频,\(\mathrm{df}(w)\) 是含该词的文档数,\(N\) 是文档总数,\(|d|\) 是文档长度,\(\mathrm{avgdl}\) 是平均文档长度。
\(k_1\) 控制饱和速度,\(b\) 控制长度归一化强度。分子从"文档总数"换成"不含该词的文档数",于是比值直接反映"不含它的文档是含它的多少倍";分子分母各加 0.5 做平滑,让 \(\mathrm{df}(w)\) 取 0 或 \(N\) 这两个极端时仍有定义。副作用是当一个词出现在超过半数文档中时取值会变成负数,实现里通常给它设一个下限。
5.1 两个参数的直觉
\(k_1\) 控制词频饱和的速度。取 \(k_1 = 1.5\) 时,一个词出现 1 次的贡献是 \(1 \times 2.5 / (1 + 1.5) \approx 1.0\)(忽略长度项),出现 2 次是 \(2 \times 2.5 / (2 + 1.5) \approx 1.43\),出现 5 次是 \(5 \times 2.5 / (5 + 1.5) \approx 1.92\),出现 20 次是 \(20 \times 2.5 / (20 + 1.5) \approx 2.33\)。理论上限是 \(k_1 + 1 = 2.5\)。
这条曲线说明一件重要的事:词频的边际收益衰减得很快。从 1 次到 5 次增加了约 92%,从 5 次到 20 次只增加了约 21%。这意味着 BM25 对"某个词反复出现"的奖励有限,它更看重"这个词是否出现"。
这也解释了为什么 BM25 在关键词检索上稳健:它不会因为某篇文档把关键词堆了十遍就把它顶到第一。
\(b\) 控制长度惩罚的强度。取 \(b = 0.75\) 时,一篇长度为平均两倍的文档,分母里的长度项变成 \(1 - 0.75 + 0.75 \times 2 = 1.75\),相当于把词频贡献压低约 40%。\(b = 0\) 表示完全不惩罚长度,\(b = 1\) 表示完全按长度归一化。
5.2 稀疏检索的强项与代价
稀疏检索的强项正是稠密检索的弱项:error_code_4021、handlePaymentCallback、某个人的全名,命中就是命中。
代价是它读不懂同义。搜"kitty"找不到只写了"cat"的文档。这个代价在技术文档里通常可接受(术语相对一致),在面向普通用户的场景里则很致命(用户不会用你的术语)。
还有一处实现代价:稀疏向量的维度等于词表大小,通常是几万到几十万。存储上它用倒排索引,只存非零项,所以实际占用远小于维度暗示的量级。但一旦要做稠密混合(把稀疏向量与稠密向量拼接),维度问题会回来——拼接后的向量既稀疏又高维,多数向量索引无法高效处理。
实践中混合检索的做法是两个索引分开建、分别查、结果融合,拼接成单个向量行不通。
6. 混合检索与重排
两路各有盲区,所以两个都跑,然后合并。难点在分数不可比。
稠密侧的余弦相似度落在零到一之间,稀疏侧的 BM25 得分可以到几十甚至更高。两类分布不同,直接相加没有意义——一个量纲会压过另一个。
倒数排名融合绕开了这个问题:完全抛开原始得分,只看排名。每个文档的综合得分是它在各路结果中排名的平滑倒数之和:
其中 \(\rho_j(d)\) 是文档 \(d\) 在第 \(j\) 路结果中的排名,\(\lambda\) 是平滑常数。
\(\lambda\) 常取 60,用来压低排名最靠前几个位置之间的差距。这个做法简单稳健,代价是丢掉了原始分数里的区分度——两路都是第一名与一路第一名一路第十名,在 RRF 里差别被压得很小。
6.1 平滑常数的取值与候选量
\(\lambda\) 的取值只影响"头部被压平的程度",不影响排序结果,所以调它不是主要调优手段。
算一下 \(\lambda = 60\) 时前几名的分数:第 1 名得 \(1/61 \approx 0.0164\),第 2 名 \(1/62 \approx 0.0161\),第 10 名 \(1/70 \approx 0.0143\),第 50 名 \(1/110 \approx 0.0091\)。前 10 名的差距只有约 13%,而第 10 名到第 50 名差距约 36%。
这个分布说明融合结果在小排名区间几乎是平的。如果两路都认为某条目在第 3 名,它得 0.032;如果一路第 1 一路第 10,得 0.030。两者几乎无差别。这是有意的设计——它表达的是"两路都认可比单路靠前更重要"。
真正需要调的是每路召回取多少条:取太少,两路各自的候选池小,融合后没有足够材料;取太多,会把大量低相关条目带进来,融合反而稀释了强信号。常见起点是每路取 50 到 100 条,融合后截到前 50 条送入精排。
6.2 精排的成本
融合之后是精排。精排不是补救融合丢失的信息,它换的是匹配范式:
- 双编码器(检索阶段)为查询与文档各自生成向量,靠向量运算比相似度。速度快,适合从海量数据里初筛,但两者从未"见面"。
- 跨编码器(精排阶段)把查询与候选文档拼成一段文本送进模型,逐词比对后输出相关性得分。慢得多,但判断更准。
精排的成本可以估。假设单次前向能处理 30 个候选(受批次与上下文长度限制),50 个候选约两次前向。如果对全库十万条做,就是三千多次前向,不可能放进一次请求。
这个数量级差异是精排只能作用于小候选集的根本原因,不是工程偷懒。
精排的另一个作用是纠偏。当融合把一条"关键词全中但意思不对"的片段排到前面时,跨编码器能通过逐词比对发现上下文不匹配,把它压下去。这个能力是检索阶段的双编码器不具备的——双编码器只比较两个独立向量的距离,看不到"这个词在这句话里是什么意思"。
两个阶段的失效方向不同,调优顺序也不同。精排只能在给定候选里排序,正确条目没进候选池时它无能为力。所以先保证召回率,再看排序质量。
7. 查询改写与上下文感知检索
前六节处理的是"给定片段怎么找到",还有两件事发生在两端。
查询改写。 用户的原始问题往往不适合直接检索:指代不明("这个怎么弄")、缺少范围("退款政策"没说哪个产品)、时间模糊("最近")。改写把任务状态、时间范围、来源限制带进查询。改写本身可能引入错误——改错了方向,后面召回全废——所以它值得作为一处可回退点:结果不佳时先回头检查改写,不要先调召回参数。
两种改写策略的差别值得分清:
| 策略 | 做法 | 适用 | 风险 |
|---|---|---|---|
| 扩展 | 补全指代、加入同义词与上下位词 | 查询过短、术语不一致 | 引入不相关方向 |
| 分解 | 把复合问题拆成子查询分别检索 | 需要多跳或并列答案 | 拆错导致漏掉整体性 |
扩展适合"用户说得太少",分解适合"用户说得太多"。两者可以叠加:先分解,每个子查询再扩展。但每加一层,引入错误的机会也多一层,所以常见做法是只做其中一种,或者让模型在检索工具里自行决定。
上下文感知检索。 这是修补分块缺陷的手段,发生在索引期。做法是在把块向量化之前,先用模型为它生成一段简短的上下文前缀,说明这段来自哪份文档、哪个章节、讲的是什么,然后把前缀与原文拼起来再索引。
举例:孤立的片段"该公司第二季度的收入增长了 3%"加上前缀"[本段节选自 ACME 公司 2025 年 Q2 财务报告的'关键业绩指标'章节]"之后,同时改善了两路检索——前缀为 BM25 增加了可精确匹配的关键词,也为稠密嵌入注入了语义背景。
一个片段的处理只做一次,前缀会随块一起存储。所以这项投入是索引期一次性成本,收益是此后所有查询的召回质量。
代价是索引期多一次模型调用。收益有公开数据:结合词法检索可把检索失败率降低约 49%,再加重排能降到约 67%。
7.1 前缀的写法
前缀的写法直接影响效果。三种常见形式,效果依次递增:
弱:本文档讨论财务数据。
中:本段节选自 ACME 公司 2025 年 Q2 财务报告。
强:本段节选自 ACME 公司 2025 年 Q2 财务报告的"关键业绩指标"章节,
讨论收入与利润率变化,数据截至 2025 年 6 月 30 日。弱形式几乎没用——它没有提供任何可区分的具体信息。中形式提供了主体与时间。强形式额外提供了章节主题、数据截止时间与讨论对象。
判断标准是:把这段前缀单独拿出来,能否回答"这段讲的是什么、关于谁、什么时候"。三个问题都能回答,前缀就够用。
成本上,前缀长度与索引成本成正比。强形式每块多 60 到 80 个 Token,两万块就是一百多万 Token 的额外索引成本。所以常见做法是让模型生成长度受控的前缀(例如不超过 100 Token),避免无限制地描述。
注意区分两个名字相近的概念:上下文感知检索在索引期做加法(给片段补背景),上下文感知压缩在运行期做减法(按当前任务裁剪历史)。作用时机与对象都不同。
8. 衡量检索质量
三个核心指标,都在带标注答案的测试集上计算:
| 指标 | 定义 | 直觉解释 |
|---|---|---|
| 命中率 | 前 \(k\) 个结果包含至少一篇相关文档的查询比例 | 该找的找到了吗 |
| 平均倒数排名 | 每个查询取第一个相关文档排名的倒数,再对全部查询取平均 | 找到的排得够不够靠前 |
| 归一化折损累积增益 | 按相关度加权、按排名折损后归一化 | 整个排序列表的质量 |
前两个的公式:
其中 \(Q\) 是查询集合,\(\rho_q\) 是查询 \(q\) 的第一个相关文档排名,\(g_i\) 是第 \(i\) 位结果的相关度等级,\(\mathrm{IDCG}@k\) 是理想排序下的 \(\mathrm{DCG}@k\)。
命中率严格说应叫命中率,叫召回率并不准确:学术上的召回率指"前 \(k\) 个结果中相关文档数除以该查询全部相关文档数",一个查询有多篇相关文档时两者不等。跨来源比较数字时需要留意各自的口径。
三个指标各有盲区:
- 命中率不看排序,把正确答案排在第 1 与第 20 都是满分;
- 平均倒数排名只看第一个相关文档,忽略其余的;
- 归一化折损累积增益需要相关度分级标注("高度相关"与"部分相关"),标注成本高。
实践中常用组合是命中率加平均倒数排名:前者保证不漏,后者保证靠前。归一化折损累积增益用在需要区分相关度等级的场景。
工业界还常用"检索失败率":正确答案不在前 20 个结果里的查询比例。它比前三个指标更贴近使用体验,因为它直接对应"这次回答是猜的"。
8.1 指标之外的四项检查
- 命中片段是否真的回答了问题,只包含关键词不算;
- 片段之间的信息是否一致,有无互相矛盾;
- 引用能否追溯到原文的具体位置;
- 检索路径能否复现——同一个查询两次运行是否得到相同结果。
最后一条在调优时特别重要。近似最近邻索引、语义切分、融合参数都可能让结果在不同运行间漂移,没有可复现的检索路径,就没法判断改动是否真的起了作用。
8.2 怎么定位问题出在哪一环
检索质量差的时候,需要先判断是哪一环的问题,否则容易在错的环节反复调参。四个检查按顺序做。
第一步,检查标注集。 拿一个已知答案的问题,人工写出一条"正确片段"。如果连人都写不出唯一正确答案,说明这个问题不适合做评测样本,换一个。
第二步,检查召回。 把正确片段所在的文档直接放进查询,看能否被召回。如果不能,问题在索引或切分——可能块切错了位置,可能嵌入没建好。这一步排除掉"库里有但找不到"的情况。
第三步,检查排序。 正确片段被召回但排在第 30 位,问题在融合或精排。此时可以打印两路各自的排名,看它在稠密侧排第几、稀疏侧排第几:如果只有一路命中且排名靠后,说明该路的权重不足;如果两路都靠后,说明片段本身的相关性信号弱,需要回到索引期补上下文前缀。
第四步,检查组装。 片段被正确召回并排在前三,但模型仍然答错。问题不在检索,在组装或生成——可能片段被截断、可能来源标记被当成内容、可能片段之间的顺序打乱了推理。
四步分别对应四类失败,跳过任何一步都可能把问题归错。实际排障中,最常见的错误是把第四步的问题当成第一步——反复调检索参数,而真正的毛病是片段进了上下文却被截断。
9. 什么时候不必用 RAG
RAG 不是所有"需要外部知识"场景的答案。三种情况下它的收益低于成本。
语料能整个放进上下文。 一份两万 Token 的内部规范,直接放进系统提示即可,检索反而会漏掉跨章节的关联。判断标准是"整份材料是否稳定地每轮都需要"。
查询需要遍历全部材料。 需要统计、需要核对全部条款、需要找"唯一一条不符合的",这类需求靠前 k 条检索永远不完整。解决办法是工具化:让模型用代码去遍历,避免用检索做抽样。
材料高频变动且没有稳定标识符。 每次改动都要重建索引,而重建间隙里索引是过时的。这种情况下直接读文件更可靠。
RAG 真正无可替代的场景是:语料远大于窗口、查询只需要其中很少一部分、且语料相对稳定。三个条件同时成立时,检索是唯一可行的手段。
三个条件的量化形式:设窗口 \(W\)、语料 \(D\)、单次查询实际需要的材料 \(d\)。RAG 有意义的条件是 \(D \gg W\) 且 \(d \ll W\)。若 \(D \lesssim W\),直接放进去更简单;若 \(d \approx D\),说明查询需要遍历,检索无法保证完整。
这两个不等式也解释了成本结构的非对称:离线成本随块数增长(块越小越贵),在线成本随候选数增长(精排候选越多越贵),两者的调优目标不同——离线追求召回率,在线追求排序精度。把两者混在一起调,通常会把其中一头推向极端。