上一章《向量嵌入与检索》把「意思相近」变成「距离很近」。 RAG 接住它:把原文递过去,模型什么都不用记。 想法简单到不像技术——但它解决的是三个最实际的问题。
| 问题 | 模型自身的困境 | RAG 怎么办 |
|---|---|---|
| 知识过时 | 训练完知识就冻结了。今天发布的产品,模型明年也不会知道 | 资料在你手里,随时改文档就行,不用重训模型 |
| 幻觉 | 不知道答案时,编一个像样的和说"不知道",没有天然的偏向——除非专门用拒答数据教过它 | 把原文递过去,让它对着材料回答,并且能给出处 |
| 私有数据 | 公司内部文档从来没出现在 |
数据不出内网,只把检索到的片段发出去 |
微调像让学生把整本教材背下来再考试——背得慢、忘得快、改一个字要重背。
RAG像开卷考试:学生什么都不用背,只要会翻书就行。
代价也很像:开卷考试的分数,很大程度上取决于"你翻到的是不是那一章"。
这就是为什么 RAG 的成败几乎全在检索质量上——生成的部分反而不太容易出错。
这个类比管到"翻书"为止:翻到的资料本身可能是错的,模型也可能拿着对的材料答错——
所以第 9 节要把"检索"和"生成"分开评测。
第一站「解析」就是把 PDF / 网页变成干净文本,表格和分栏最容易出错; 后面还有切块、嵌入、索引、检索、重排、拼上下文、生成。八站里最差的那一站,决定整个 RAG 的效果。
互动 · 点每一站看它会怎么坏
很多人以为 RAG 效果不好是"模型不够强"。实际上大多数失败发生在检索阶段: 文档没解析对、块切得不对、检索没召回、重排没排上——模型拿到的是一堆错材料, 它要么照着错材料胡说,要么凭自己的记忆编。 所以调试 RAG 的第一步永远是:把检索回来的原文打出来看一眼。
文档太长了塞不进上下文,所以要切成块。听起来是个无害的操作—— 但切块会真的把语义切断。
互动 · 按不同参数切分,标出被切断的地方
把块之间留一段重叠,是为了让被切断的那句话在下一块里完整出现一次。
上面的图里,绿色部分是重叠区域。
重叠的代价是冗余:overlap 20% 就意味着索引体积和
另一个更根本的问题:块的大小决定了它能装下多少上下文。
块太小 → 检索准但信息不全;块太大 → 一个块里有好几个主题,
整块过一遍编码器得到的那个向量,把几件事平均成了四不像,检索就不准。
这就是"父子块"策略出现的原因——用小块的向量去检索,但把大块的原文喂给模型。
最差的切法就是"每 500 字切一刀"——它完全不理解文档结构。
更好的做法是沿着文档自己的边界切:
· 先按标题(Markdown 的 # / ##)切
· 再按段落切
· 再按句子切(句号、问号、换行)
· 实在不行才按字符硬切
而且切完之后,一定要把所属的标题路径拼到每个块前面——
比如「第 3 章 > 配置 > 超时设置 > 正文……」。
这样即使块被单独取出来,它也还带着自己的上下文。这一招几乎零成本,效果很明显。
向量检索擅长"意思",但它在精确匹配上有硬伤。回看阶段 3 的分词那一章—— 同一个编号、同一个型号、同一段代码,在向量空间里可能和完全无关的文本更近。
| 查询类型 | 向量检索 | BM25(关键词) |
|---|---|---|
| 换个说法问同一件事 「怎么把超时时间调长」 vs 文档写「timeout 参数配置」 |
✅ 强项 | ❌ 一个词都不重合,完全召回不到 |
| 找精确的错误码 / 型号 「E5032」 |
❌ 在向量空间里"E5032"和"E5033"几乎一样 | ✅ 强项,精确匹配 |
| 专有名词、内部代号 | ❌ 模型没见过这个词,向量是随机的 | ✅ 有就有,没有就没有 |
| 长句语义问题 | ✅ 理解整体意图 | 部分有效 |
| 代码符号、公式 | ❌ 分词之后全是碎片 | ✅ 原样匹配 |
互动 · 三套检索方案的真实召回率对比
BM25 是任意实数、余弦是 −1~1,尺度完全不同,所以最简单的融合做法是
只用排名、不用分数——就是下面这个 RRF。
一个文档如果在两路里都排第 3,它的得分就比只在一路排第 1 的更高,
这就是"两路都认可的,大概率是真的相关"。
工程上两路各取 50 → 融合成一份候选 → 重排取前 5——每一路都得给足候选,单路漏了就补不回来。
两阶段检索
双塔(Bi-Encoder)把查询和文档分开编码,可以在线算一次、离线算全库,快但精度略低;
交叉编码器(Cross-Encoder)把查询和文档拼在一起过一遍模型,精度高得多,但每算一对就要跑一次完整前向。
100 万篇文档上跑交叉
重排只能重新排序,不能找回。
如果粗筛阶段没把正确文档放进这份候选(top-50),重排再强也没用——"重排的上限由召回决定"。
所以工程上的顺序是:先把召回率做上去(混合检索、调 chunk、多路查询),再上重排。
反过来的话,你会在一个本来就漏的漏斗上加一层精细筛子。
互动 · 把最相关的材料放在不同位置(曲线是按论文趋势搭的示意,不是实测数据)
研究发现了一个稳定的现象:把关键信息放在上下文的开头或结尾,模型用上它的概率远高于放在中间。 上面那条是示意图:放在第 1 个位置时几乎总能用上,放中间明显下降—— 论文(Lost in the Middle)里的真实数字见拓展阅读。
实践含义:检索回来多个片段时,别按分数降序直接拼。 把最相关的放开头和结尾,次相关的放中间。 只留一个位置时放开头——论文的多文档实验里,开头和结尾都明显好于中间,开头通常是最高的那一端。 这一条几乎不花任何成本,却经常能带来可观的提升。
点一下看每一代在修什么
| 代际 | 做法 | 修了什么 |
|---|---|---|
| Naive RAG | 切块 → 向量检索 → 拼进上下文 | 最基础的版本。问题是召回不准就全盘皆输 |
| Advanced RAG | 查询改写、混合检索、重排、元数据过滤 | 在检索的每一环都加了修正。今天实际部署的基本都是这个 |
| HyDE | 先让模型"幻想"一段答案,用这段假答案去检索 | 解决"问题太短、和文档表述不像"的问题。 用一个假答案当查询,往往比原问题召回得更准 |
| Self-RAG | 让模型自己决定"要不要检索""检索结果够不够""答案有没有依据" | 不再是"永远检索固定条数",检索变成模型主动调用的动作 |
| GraphRAG | 从文档里抽取实体和关系,建成知识图谱再检索 | 解决全局性问题:「这批反馈里最主要的主题是什么」—— 答案不在任何单独一段里,要先把整个文档集汇总一遍 |
| Agentic RAG | 把检索当成 Agent 的一个工具,可以多轮检索、改写、验证 | 检索不再是流水线上的一站,而是一个可以反复调用的动作(见下一章) |
这是这几年被问得最多的问题。模型上下文从 4K 涨到 1M, "那我直接把整个知识库塞进去不就行了?" 先看量级:1 个汉字约 1~2 个 token,几万字就是几万到十几万 token——放得下就先别上 RAG;要检索的规模到了几十万 token 以上,才轮到它。
| 论点 | 实际情况 |
|---|---|
| "上下文够长就不用检索" | 就算塞得下,效果也不好。
Lost in the Middle 说明中间的信息会被忽略;
|
| "长上下文更简单" | 每次调用都要为全部输入付钱(未计 prompt caching:重复的前缀能打折)。 100 万 token 塞一遍,每问一次都要付这个成本;而 RAG 每次只付检索到的 2000 token |
| "100 万上下文装得下我的库" | 企业文档库动辄几百万篇,一亿 token 起。永远装不下 |
| "检索会丢信息" | 这是真的、也是 RAG 的真实弱点。所以出现了 "先检索再长上下文"的组合:用检索把范围缩到 10 万 token, 再用长上下文模型精读 |
互动 · 每问一次要付多少 token:全塞 vs 只塞检索回来的
这条账是第 8 节「长上下文更简单」那句话的反面: 上下文越长,你为每一次提问付的钱越多,而 RAG 每次只付检索到的那一点点。
检索负责"从一亿个东西里选出 500 个",长上下文负责"把这 500 个读透"。 两者解决的是不同的问题:检索解决"规模",长上下文解决"深度"。 所以今天最好的系统是长上下文 + 好的检索,而不是二选一——那种"上下文够长就不用检索了"的说法, 混淆了"装得下"和"用得上"这两件事。
前面说「RAG 的成败几乎全在检索」。检索听起来像黑魔法,拆开只有三步算术: ① 文字变成一个点(嵌入)、② 比谁和问题夹角小(余弦)、 ③ 两路名次合成一路(RRF)。下面三张图用的是同一批 6 个块、同一个问题, 所以它们能互相验证——每一步的数字都是当场算出来的,没有一个写死。
第一步 · 谁和问题夹角最小:余弦(真的在算)
那一步最没画面:为什么要「除以长度」
第二步 · 关键词那一路:BM25 的全部数学
关键词那一路(BM25)算的是三个数:这个词在全库有多罕见(idf)、 在这个块里出现几次、这个块有多长。结果是任意实数、没有上界——所以它和余弦不能直接相加。
第三步 · 两路名次合成一路:RRF
把查询切到「E5032 报错」。看第一步那张表:E5031 和 E5032 的余弦只差 0.005——
向量根本分不出谁是谁。
再看第二步:同一个查询下,BM25 给 E5032 满分、给 E5031 0.746——
因为 E5031 的文本里根本没有「E5032」这个词。
最后看第三步:RRF 把两路的名次一合,最终第一名就定了。
这就是第 4 节那句话的全部内容:两路的错法完全不同,所以必须同时跑。
| 问题 | 说明 | 什么时候真的会痛 → 换什么 |
|---|---|---|
| 检索错了会放大幻觉 | 这是最危险的一条。模型拿到错误的材料后, 不但会答错,还会很有底气地引用它——因为它的任务就是"根据材料回答"。 没有 RAG 时它至少可能说"我不知道" | 答案要担责、要能追责 → 把检索质量做上去,并让回答带引用、找不到就拒答 |
| 分块必然丢上下文 | 无论怎么切,都会出现"这个代词指代的是上一块里的东西"这种情况。 块切得越碎,检索越准,但块越没头没尾 | 文档里代词、指代密集 → 按结构切,或上父子块(小块检索、大块喂给模型) |
| 多跳问题需要多次检索 | 「A 公司的 CTO 之前在哪个学校读书」——这个答案需要先找到 CTO 是谁, 再去找他的履历。一次检索解决不了 | 答案散在两处、要分几轮查 → 多轮 / 迭代检索(下一章 Agent) |
| 评测要拆成两段 | 最终答案错了,可能是检索没召回,也可能是召回了但模型没用好。 必须分开测:先测召回率(有没有找到),再测生成质量(有没有用好)。 混在一起测,你永远不知道该修哪一段 | 上线后要定位错在哪一段 → 先测召回,再测生成,分开看指标 |
| 文档本身可能自相矛盾 | 公司文档里有两个版本的流程图、政策已经更新但旧文档没删—— 检索会把矛盾的证据一起递给模型,然后它随机挑一个 | 旧文档没删、政策多次改版 → 先治理知识库:去重、只留最新版 |
| 检索结果可能被投毒 | 如果知识库里有用户可写的内容,攻击者可以植入一段 "忽略之前所有指令"的文本——一旦被检索到就成了提示注入(见安全那章) | 知识库有用户可写入口 → 权限控制,过滤指令型文本(见安全那章) |
互动 · 最终正确率 = 召回率 × 生成正确率
只盯着最终那个数字,你分不清是检索漏了还是模型没用对—— 所以这张表最后一条才说「评测要拆成两段」。试一下:把两个滑块调成 70% / 85% 和 85% / 70%,终值一模一样。
回到第 6 节那根 U 形曲线图。拖「关键材料放在第几个位置」这个滑块, 让高亮的材料从第一个慢慢移到正中间——盯住下面的百分比。
材料一个字没变、长度没变、答案也没变, 只是它从「第一个」挪到了「中间」,被用上的概率就掉了一大截。 你以为你在管理「放什么」,决定成败的却是「放在第几」。
再把滑块拖回 0 或最后一个位置,看它涨回去。然后是第 4 节: 那里三套方案的召回率差异,是同一个反直觉的另一面。
这一章在「信息流动」和「参数账本」两条上最典型,第三条是它没意识到的假设。
| 暗线 | 这一章的回答 |
|---|---|
| A 信息流动 | 全站形状变化最多的一条链,有一处不可逆:
PDF 页面 → 纯文本 →
块(约 180 字) → 向量(1024 维) →
top-k 块 → 拼成 prompt →
答案。块变成向量之后,后面的比较都只在这个 1024 维的点上进行——原文里没被这 1024 个数装下的东西,对向量这一路就不可见了(关键词那一路还留着原文)。 |
| C 参数账本 | 「不用重训」这个好处标在四个数上:BM25 零参数、嵌入模型约 3.3 亿参数;
1 亿块 × 1024 维 × 4 字节(float32) ≈ 410 GB(float16 是 205 GB);
全扫一遍约 10¹¹ 次乘加;而把一条新政策写进权重要一次 |
| E 它假设了什么 | 三条隐藏假设都会破:「语义相关 = 向量距离近」在否定、比较、精确编号上系统性失效(→ 混合检索); 「答案就在某一个连续的块里」在多跳问题上不成立(→ 多轮 / 迭代检索); 「答案要汇总整个库」的全局问题上也不成立(→ GraphRAG)。 「检索到的材料是真的」,可写的知识库会被投毒(→ 知识库治理,见第 9 节)。 |
RAG = 开卷考试:一个参数都不改,靠把检索回来的原文递给模型,解决知识过时、幻觉和私有数据。 成败几乎全在检索——按「解析 → 切块 → 混合检索(向量 + BM25,RRF 融合)→ 重排 → 摆位置」排查; 单靠向量一定会漏精确匹配,混合检索不是可选优化,长上下文也没有取代它。
下一章《Agent 与工具调用》接住。
上面讲的都是「够用」的版本。想往下挖,这里有三个入口—— 它们不是必修内容,是给想再往前走一步的读者准备的。