前十一篇搭出来的那条链路,是一条写死的流水线:改写 → 召回 → 融合 → 重排 → 构造 → 生成,每个请求走一遍,参数全局一致。这套东西在「问一个事实、答一段材料」上很稳,成本也可预测。直到有人问:「对比一下 v2 和 v3 的重排策略差异,再找出当时决定改版的那个讨论记录。」
这个问题一次检索答不了。它需要先确定「v2/v3 各指什么」,分别找材料,判断两边证据够不够,再拿「讨论记录」这个新线索去查第二轮。流水线没有第二轮,也没有「证据够不够」这个判断节点。
本系列第 12/15 篇 · 上一篇:RAG 评测:没有评测集就没有优化 · 下一篇:Agent 记忆系统的读写与遗忘
从 pipeline 到 tool:变的到底是什么
第 3 篇把工具调用拆成协议时留了个伏笔:工具里最常见的一个是「检索知识库」。把整条 RAG 链路封装成一个工具,Agent 侧的接口可以很薄:
1 | { |
变化不只是「多了个函数」。真正的差别有三处:
检索次数从固定变成由模型决定。0 次(不需要外部知识)、1 次(普通事实题)、N 次(多跳、对比、验证)都可能。
查询从用户原话变成模型构造的子问题。工具描述里那句「一个独立、完整的子问题」是硬要求,模型如果把整句复合问题原样传进来,召回必然糊。
检索结果从终点变成观测。它回到第 1 篇的闭环里,成为下一步决策的输入,而不是直接拼进答案。
查询分解的几种做法
多路改写(multi-query):把一个查询改写成 3–5 个不同角度的变体,并行召回后合并去重。主要解决措辞敏感,成本低,实现简单。缺点是它仍然只有一轮,不处理依赖。
Step-back 提问:先生成一个更抽象的问题(「这类改版一般考虑哪些因素」),把抽象层材料和具体层材料一起召回。对需要背景知识才能理解具体条款的问题有帮助。
HyDE:先让模型凭印象写一段「假想答案」,用这段假想答案的向量去检索,而不是用原查询向量。思路是让查询侧和文档侧在文体上对齐。它短查询上有时有效,风险也很明确——模型编出来的细节会把检索带偏,所以我只在原查询极短、且明确要求「召回概念性材料」时才用,绝不用于精确编号类查询。
子问题分解:把复合问题拆成有依赖关系的子问题序列,前一个的结果可能成为后一个的参数。这才是多跳的核心,也和前三篇的规划层直接衔接:第 2 篇那份带 depends_on 的计划,在这里的每一步就是一个检索动作。
一个实际的分解例子,开头的那个问题会被拆成:
1 | q1: v2 版本的重排策略是怎样的? scope=changelog |
注意 q3 的参数依赖前两步的产物,这正是第 2 篇 $sN.field 显式引用的用法。让模型自己记住「刚才那个代号」并填进参数,比让它声明依赖关系不可靠得多。
迭代检索循环与停止条件
1 | def agentic_retrieve(question, budget): |
四个设计点决定了它能不能用:
证据账本(EvidenceLedger),不是消息历史。每条证据记 chunk_id、来源、日期、它支撑哪个子问题、以及是否与其他证据冲突。第 10 篇的引用校验在这里就能提前做:只在账本里登记过的材料允许进入最终上下文。多轮检索最容易出的问题是「模型把第 1 轮看到的和第 3 轮看到的混在一起,来源全丢了」,账本就是防这个的。
缺口显式化。find_gaps 把「还差什么」写成结构化列表,比让模型在对话里自己感觉「差不多了」可靠。它可以是规则(子问题清单里哪些未标记完成),也可以是模型判断,但必须落成可检查的数据。
停止条件必须有上限和平台检测。max_retrieval_rounds(我一般 3–5)、总 token 预算、总墙钟超时,以及「连续两轮没有带来新证据」就停。没有平台检测,模型会一直用略微改写的同一个问题反复检索,日志里每一轮都很努力,证据一条没多。
生成阶段的证据约束。检索结束不等于可以随便答。我把 finish 时的账本快照交给第 10 篇的上下文构造层,由它编排、包装、加引用要求;如果账本里存在未覆盖的缺口,指令里要明确写「对以下问题说明材料不足」,而不是让模型补白。
自我纠偏:什么时候该承认这一轮检索废了
Self-RAG、CRAG 这一类思路的共同点是:在生成之前或生成之中,加一个对检索结果的质量判断,并据此决定动作。工程上我把它落成三档:
1 | correct → 证据直接支撑答案,进入生成 |
判断依据尽量用可观测的信号,而不是让模型打分说「相关度 0.6」:候选 top1 与中位数的分数差距、命中片段里是否出现查询的关键实体、同一子问题的不同改写是否召回同一批文档(一致性高说明稳定,一致性低说明查询没定准)、以及 scope 是否选错(比如 changelog 库里根本没有 tickets 类内容)。
incorrect 那一档有个容易忽略的动作:丢弃。很多实现会把不合格的检索结果留在上下文里「供参考」,模型照样会拿它编答案。纠偏要真的删,包括从账本里移除。
还有一个便宜但有效的技巧:把「检索充分性」判断放在只看材料标题与摘要的阶段做。第 10 篇讲拒答时用的两阶段思路在这里同样成立——先看目录决定要不要继续查,比把全部片段读完再判断省得多。
路由:不是每个问题都该检索
Agentic RAG 最容易被浪费的地方,是给每个问题都跑一遍完整链路。我在闭环前面放一个极薄的路由,只判断三件事:
- 要不要检索:闲聊、格式转换、纯计算、已在上下文里给出材料 → 直接答;
- 检索哪个库:
scope由规则或模型选,选错库是很多「查不到」的真实原因,比参数没调好更常见; - 要不要用别的工具:结构化统计类问题(「过去三个月有多少条工单」)应该走 SQL/API,而不是检索几段文本再让模型数数。
路由判断本身不该再调一次大模型。关键词规则 + 少量 few-shot 的小模型足够,省下的延迟和成本很实在。第 3 篇说过「无条件改写会洗掉精确字面线索」,路由也一样:含错误码或编号的查询直接跳过改写、直接进稀疏优先通道。
优劣与边界
收益:能处理多跳、对比、需要验证和补查的问题;检索次数与预算按难度分配,简单问题反而更便宜;每一轮的查询、材料、判断都在 trace 里,可解释性比固定流水线好;一个检索通道故障时,Agent 可以换 scope 或换工具,而不是整条请求失败。
代价很实在:多次模型调用和多次检索,延迟从一次变成数倍,长尾很难看;成本方差变大,同一入口的简单问题和复杂问题可能差一个数量级;失败模式变多——分解错、路由错、缺口判断错、无限循环、证据账本与最终上下文不一致;评测也更难,第 11 篇的分层指标之外还得加「检索轮数分布」「平台命中率」「单位答案成本」。
什么时候别用:查询分布以单跳事实题为主、延迟预算严格、或者成本需要可预测(比如按次计费的产品)。这时固定流水线更稳、更便宜、更好评测。我的实际做法是混合:默认走固定流水线,只有当路由判定为「多子问题 / 对比 / 需要验证」时才升级到 Agentic 循环,并给它单独的预算上限和降级路径。别因为架构更”先进”就全量替换。
多轮检索会不断往账本里写东西,长任务里这些证据很快就会撑满窗口。什么该留在上下文里、什么该外置、什么时候该压缩或遗忘,是下一篇的主题。你现在这个检索工具,一轮拿不到证据时是继续换着查,还是直接把不相关材料塞进答案里?
评论