从写死的链到会变通的代理 — 书自称的落点,以及它为什么没落住
这一章讲三件事: 链的那个根本毛病到底是什么; 「让模型自己决定要不要再查一次」这件事,拆开来是哪几步; 以及这本书自称的落点,为什么一个字都没落住。 它在全书链条里的位置: 这是原书的最后一章,也是它自己给全书安排的高潮。 而全书对这件事最完整的定义,不在这一章,在第 7 章的结语里。
1. 顶层全景
链(第 10 章):
问题 ──► 检索 ──► 拼提示 ──► 生成 ─ ─► 答案
│
└─ 这一步什么都没找到?**它照样往下走。**
想要的样子:
问题 ──► 检索 ──► 先给个初步答案
│
▼
┌─► 手上这些证据够不够、对不对题?
│ │
│ 够 ──┴──► 交出答案,结束
│ 不够
│ │
└──── 改写问题/换检索条件,**再查一遍**
│
每转一圈都问一次:该停了吗?
图说:两张图的差别只有一样东西 —— 那个回头的箭头。 这一章的全部内容,就是这个箭头存在还是不存在。
一句话链条: 链是写死的顺序 → 第一次没查到它也不会重来 → 出路是让模型自己判断证据够不够、不够就再查一次 → 而只要有循环就必须有停下来的条件 → 书把这一整套讲成了「用了某个框架」,于是一步都没做。
2. 链的根本毛病:第一次没找到,它照样往下走
现象
用户问: 我们公司的年假是怎么算的?
库里: 压根没有这份制度文件
链的走法:
检索 → 取前 3 条(**永远凑够 3 条**,第 05 章讲过)
→ 这 3 条讲的是报销流程、考勤打卡、差旅标准
拼提示 → 把这 3 条原样贴进去
生成 → 「年假按工龄计算,满一年 5 天……」 ← **编的**
图说:整条链没有一个环节报错。它**不知道自己没找到**,
因为链里没有任何一步在问这个问题。
为什么链修不了这件事
因为链的形状就是「一步接一步,走完为止」。 它没有回头的路 —— 要回头,就得有人决定「回不回」,而链里没有这个人。
第 09 章说过,不确定的时候有三条路:报出来、拒答、再查一次。 链能做前两条(在最后一步加 个判断),做不了第三条 —— 再查一次意味着回到前面的步骤,而链只能往前。
3. 代理式 RAG 的真定义,藏在第 7 章的结语里
先说这个名字
「让程序自己决定下一步做什么」这件事,这一行叫代理,英文就叫 agent。
把它用在 RAG 上,就是「让模型自己控制检索」—— 这套做法叫代理式 RAG。
定义在哪儿
书给这件事下过一次完整的定义,而那段话出现在第 7 章的结语里 —— 第 7 章讲的是怎么生成答案,下一章讲提示写法,这段话和前后都接不上1。
它说的是四步加一个条件:
| 步 | 做什么 |
|---|---|
| ① | 先生成一个初步答案 |
| ② | 模型评估:检索回来的文档,证据够不够、对不对题 |
| ③ | 不够就改写查询,或者发出更有针对性的子问题,重新检索一遍(可以换检索器、换过滤条件、换时间范围) |
| ④ | 回到 ①,再来一轮 |
| 停止条件 | 重复到满足停止条件为止 —— 够有把握了、转够几轮了、或者花的钱到顶了 |
书还给了它的适用场景:问题问得含糊、或者库里文档质量参差不齐时,这套做法能减少胡编。
「停止条件」为什么必须单独拿出来说
只要有循环,就必须有停下来的理由 —— 否则它会一直转。
书给的三种,各管一件事:
| 停止条件 | 它拦的是什么 | 定不好会怎样 |
|---|---|---|
| 够有把握了 | 拦「已经够好还在瞎折腾」 | 定太高就永远停不下来 |
| 转够几轮了 | 拦「怎么改都找不到」 | 定太小等于没循环 |
| 花的钱到顶了 | 拦「一个问题烧掉一天的预算」 | 不设就会有失控的账单 |
实际系统里三种一般同时设,任何一条满足就停。
判断(我们的,不是书里的): 我们认为这三条里**「转够几轮」是唯一不可省的一条**。 另外两条都依赖一个数值判断 —— 而第 09 章刚讲过,那种数值本身可能是假的; 只有轮数是绝对可靠的刹车。 如果错,会错在: 如果每一轮的成本很低而问题很值钱, 那么把轮数上限设得很大、主要靠把握度停下来,是更划算的。 判据是「多转一轮的收益 vs 多转一轮的成本」—— 这需要评测,而评测是这本书最大的洞(第 12 章)。
4. 主走查:拿这把尺子,量一量书自己的最后一个配方
这一节是本章的主走查。上面那张定义表的每一步,在这条线上都占一格。
量的对象是原书的第 121 个配方,也就是全书最后一个 —— 它的标题叫「查询扩展代理」2。
它的库只有五条文档:
doc1 Alice wrote about intermittent fasting and health.
doc2 The 2008 financial crisis impacted global markets.
doc3 The French Revolution began in 1789.
doc4 Research shows fasting improves insulin sensitivity.
doc5 Global financial markets collapsed in 2008.
逐格对照
| 定义要求的 | 这个配方实际做了什么 | 判定 |
|---|---|---|
| ① 先生成一个初步答案 | 它一个字的答案都没生成 —— 整个程序只返回检索结果 | 缺 |
| ② 评估证据够不够 | 没有任何判断语句在问这件事 | 缺 |
| ③ 不够就改写查询重检索 | 有改写,但不是「不够才改」 —— 它一上来就无条件把查询扩成 8 个变体,各查一次 | 半个 |
| ④ 回到 ① 再来一轮 | 没有循环。 扩展 → 各查一次 → 合并 → 返回,一趟走完 | 缺 |
| ⑤ 停止条件 | 不需要,因为它只跑一趟 | 缺 |
五格里四格是空的。而这是全书的最后一个配方,书名给它的定位是「代理」。
第 ③ 格展开:它是怎么改写查询的
它用的是一本英文同义词词典 —— 一份把英语单词按意思分组编好的现成词库, 查一个词,能得到和它同组的其他词3。
做法是:把问句拆成词,每个词都去查同义词,然后拿查到的词替换原词,得到一批新问句。
书印出来的实际结果是4:
原问句:Tell me about health (跟我讲讲健康)
扩展出的 8 个变体:
Tell me about health ← 原句,被保留了下来
Tell me about wellness ← health → wellness,**有用**
Tell me almost health ← about → almost(几乎)
Tell me approximately health ← about → approximately(大约)
Tell me around health ← about → around(周围)
Tell me astir health ← about → astir(苏格兰方言:活动着的)
Tell Maine about health ← **me → Maine(缅因州)**
Tell Pine Tree State about health ← **me → 松树之州(缅因州的别称)**
图说:「me」变成了「缅因州」,因为在那本词典里,
ME 是缅因州的邮政缩写、「松树之州」是它的别名,三者在同一组。
8 个变体里,**只有 1 个是有用的改写,1 个是原句,其余 6 个是噪声。**
另一个查询更离谱4:
原问句:History before 1900 (1900 年以前的历史)
变体: History earlier 1900
History **ahead** 1900 ← before → ahead(在前面)
History **in front** 1900 ← before → in front(在前方)
图说:before 的时间含义被换成了空间含义。
**「1900 年之前」变成了「1900 的前面」——方向整个飞了。**
而它给出的结果是对的
「Tell me about health」 → doc1 1.2788 (Alice 写过间歇性禁食与健康)
doc4 1.5752 (研究显示禁食能改善胰岛素敏感性)
「History before 1900」 → doc3 1.3599 (法国大革命始于 1789)
doc2 1.7255 (2008 金融危机)
图说:这些数是 L2 距离,**越小越近**(第 04 章讲过)。
两个查询的第一名都对。
它凭什么还能对?两个兜底设计
这才是这个配方真正值钱的地方,而书一个字没说。
兜底一:**原查询被留在了变体集合里。**
代码建 那个集合时,第一个放进去的就是原句。
→ 所以最坏情况下,结果不会比「不做扩展」更差。
兜底二:**同一篇文档在所有变体里,只保留它拿到过的最好分数。**
「Tell Maine about health」当然也会返回一堆文档,
但它给的分数很差,**顶不掉原句给出的好分数**。
图说:这两条合起来的效果是——
**一个坏变体只能往候选池里多扔几条,不能把好结果挤下去。**
它把「扩展」从一个有风险的操作,变成了一个只增不减的操作。
判断(我们的,不是书里的): 我们认为这个配方最该被学走的不是同义词扩展, 而是这两条兜底。 扩展本身在这里的表现是净负的(8 个变体里 6 个是噪声), 而它之所以没有害处,完全靠这两条设计。 推广开来:凡是「多试几种问法」的做法,都应当把原问法留在候选里、并且按最优分合并。 如果错,会错在: 如果换一个语料,某个坏变体恰好在某篇不相关的文档上拿到了极高的分, 那么「只增不减」就不成立了 —— 坏变体确实会把一条垃圾送进前几名。 判据是拿一个含有噪声文档的库重跑 —— 书里只有 5 条干净文档,做不了这个对照,我们也没有跑。
5. 十个「代理」,逐条对照那张定义表
这一章一共十个配方。拿第 3 节那五格去量,结果是这样的:
| 配方 | 书给它的名字 | 代码实际做的事 | 有循环吗 |
|---|---|---|---|
| 112 | 自查询代理 | 让模型把问题转成一份带过滤条件的数据 —— 失败了,走了兜底分支,过滤条件是空的5 | 无 |
| 113 | 任务导向工具代理 | 句子里有数字就走计算器,否则走检索 | 无 |
| 114 | 上下文感知的对话代理 | 维护了一个对话历史列表,但从来不读它6 | 无 |
| 115 | 动态重排代理 | 重排 = 数问句和文档有几个共同的词,连「is」「the」都算7 | 无 |
| 116 | 自适应摘要代理 | 库里只有一篇文档,而分支条件是「只有一篇就直接返回」—— 摘要那条路是死代码8 | 无 |
| 117 | 思维链检索代理 | 提示里写了「一步一步想」,模型把提示里的 Context: 标签一起抄了回来9 | 无 |
| 118 | 混合检索代理 | 两路结果直接拼起来去重,没有打分也没有融合,按意思找的那一路永远占前排 | 无 |
| 119 | 时间感知检索代理 | 取一条结果,然后对这一条按日期排序10 | 无 |
| 120 | 流式检索代理 | 先把整段答案生成完,再逐词打印 —— 代码注释自己写着「模拟」11 | 无 |
| 121 | 查询扩展代理 | 第 4 节那条走查 | 无 |
十个配方,零个循环。没有一次自我评估,没有一次重试,没有一个停止条件。 分支最多的那个,是一句「有没有数字」的判断。
三处值得单独看一眼
第 114 个配方最能说明问题。 它叫「上下文感知」, 代码确实把每一轮问答都存进了一个列表 —— 而这个列表从未进入任何一次提示。
后果就印在输出里: 问「法国的首都是什么」,答案是 France ——
一个词,而且是问题里的词6。
而同样的库、同样的问题,第 115 个配方答对了:The capital of France is Paris.7
差别在于第 114 个配方调了两次模型:先让模型把文档「答」一遍,
再把这个答案当资料喂进第二次。两道小模型串起来,信息被磨没了。
第 119 个配方是全书最短的一处空转: 它取回 1 条结果,然后对这 1 条按日期排序10。 对一个元素排序,等于什么都没做。