30 秒判断:存储方案定了以后,检索侧真正要回答的不是"用哪个 RAG 框架",而是一串更具体的问题:经典 RAG 还要不要?路由放在哪里?需要哪些检索动作?模型和系统怎样交接?提示词负责什么?本文按一个设计者实际推进的顺序,把这些决定串起来,最后落成五个工具、一个 Agent 循环和一套提示词分工。
核心结论:默认纯 Agent,不做前置分类器;工具负责取证据,Harness 负责解析、执行与运行边界,模型负责根据 Observation 选择下一步。
适合谁读:已经选好向量/关键词存储,开始设计 Agent 检索的人;正在纠结经典 RAG 与 Agentic RAG 的人;工具越加越多、循环和提示词却越来越难维护的人。
本文怎么读 :沿用系列前四篇的结构------先讲设计问题,再用一个文档学习问答 Agent演示。本文只做检索侧功能设计;预算、可观测、可复现、部署等质量属性留给下篇。
前置阅读 :系列前四篇。①《Agent 开发为什么要测试驱动升级版》讲方法由来;②《需求定义四步:用一个文档问答 Agent 演示"先定义、再动工"》定义问题空间与质量属性;③《金标准:用一个文档问答 Agent 演示"什么算对、怎么判"》把"答对"落成可执行判定;④《由检索需求反推存储》先定了"块+元数据+双索引"的存储形态。
结构速览:§0 存储之后还缺什么;§1 模式与路由:为什么默认纯 Agent;§2 功能级工具契约;§3 单工具设计;§4 Agent 循环;§5 提示词;§6 引用与证据交付;§7 对入库侧的功能要求;§8 刻意不做什么;§9 一页速查。
证据标记 :✅ 实测 =有可复现实验数据支撑;🤔 推断=由机制、既有实验或设计要求推出。检索侧目前主要是设计推导,尚未完成 pure Agent 与预路由方案的对照实验,因此不宣称性能已经胜出。
先交代几个名词
- 经典 RAG:查询一次,取 top-k,然后直接生成。
- Agentic RAG:Agent 根据当前证据决定下一步:继续检索、换范围、澄清或回答。
- 前置路由:在进入 Agent 之前先分类,决定本次走哪条检索路径。
- 纯 Agent:入口不做检索路径分类,所有问题都由同一个循环决定下一步。
- Action/Observation:模型选择一个动作;系统执行后,把结果作为观察返回。
- Harness:模型外面的执行器,负责解析 Action、执行工具、维护 History,并执行权限和运行边界。
- 工具契约:工具的输入、输出、空结果、错误、范围和排序规则,以及它"不能证明什么"。
- collection 快照:某一时刻完整、不可变的一套文档、块、向量和关键词索引。
0. 存储之后,还缺什么
实例|文档学习问答 Agent
上一篇已经决定:第一阶段用增强的向量块,经典 RAG 与 Agentic RAG 共用一套块索引。
但这个结论只回答了"知识存在哪里",没有回答实际运行时的问题:
- 简单问题也走 Agent,会不会太慢?
- 要不要先判断该走经典 RAG 还是 Agentic RAG?
- Agent 需要哪些检索动作?
- 每个动作返回什么,能证明什么,不能证明什么?
- 模型怎样知道下一步该继续还是回答?
- 哪些规则放提示词,哪些必须由代码保证?
这些问题有先后关系。先定的决定会改变后面的结构,不能倒着拼。
1. 模式与路由:为什么默认纯 Agent
1.1 先问能力是否够,再问哪条路径更快
经典 RAG 的优势是路径短:检索一次就生成。Agentic RAG 的代价是可能多几轮模型调用和工具调用。
但"简单问题"并不自动意味着"必须再维护一条经典路径":
- 单点事实确实通常一次检索就够;
- 同一套系统仍要处理枚举、证否、跨源、消歧和整节读取;
- 增加经典路径,就同时增加入口判断、提示词、失败语义和测试面。
所以先回答能力问题:
Agentic 能否覆盖经典单点场景?如果可以,是否值得为了延迟再维护第二条路径?
单点问题可以看成复杂问题的子集:Agent 检索一次,发现证据足够,随后 Finish。代价是简单问题多了一次模型决策;是否值得,要靠延迟与通过率实测。
1.2 前置路由看起来省,实际只是把判断换了个位置
另一种方案是入口先做分类器,判断应该走经典 RAG 还是 Agentic RAG。
| 前置路由 | 纯 Agent | |
|---|---|---|
| 判断发生在哪 | 入口 | 每轮 Thought |
| 分错后 | 整条路径走错 | 下一轮仍可修正 |
| 出错定位 | 分类器+下游两处 | 一条循环轨迹 |
| 简单问题 | 路径更短 | 需要模型判断是否直接回答 |
| 复杂问题 | 依赖分类器预见所有类型 | 可以随 Observation 动态展开 |
分类器没有消除"问题要不要多轮检索"的判断,只是把这个判断提前。
更麻烦的是,入口一旦分错,后面的检索再正确也没有用;排查时还要先证明是分类器错,还是下游环节错。
1.3 当前选择:默认纯 Agent
当前把全部检索判断收敛到 Agent 循环的 Thought:
- 简单问题:一次检索后直接回答;
- 复杂问题:根据 Observation 继续换范围或补检索;
- 对象不明确:先消歧或澄清;
- 证据不足:继续找,或明确说明缺口。
这不是说纯 Agent 一定更快或更准。🤔 推断:它只是让不确定性集中在一个可观察位置,系统不需要同时维护"入口判断"和"循环判断"两套逻辑。
1.4 快车道什么时候才值得加
只有当简单问题的实际延迟不达标,并且短路径能稳定通过同类问题时,才增加快车道。
触发条件应包括:
- 首字延迟有稳定测量;
- 简单问题通过既定重复运行门槛;
- 快车道没有破坏证据和边界规则。
在此之前,增加第二条路径只会放大维护成本。
2. 功能级工具契约:先定义工具能做什么
工具不是"函数名+参数"就够了。模型首先要能判断:这个工具完成什么动作、需要什么输入、返回的是结构还是证据,以及什么情况下不能使用它的结果。
本阶段的契约只解决功能问题:
| 要素 | 要确定什么 |
|---|---|
| 粒度 | 一个工具只完成一类检索动作 |
| 输入 | 哪些选择交给模型,哪些参数由系统固定 |
| 输出 | 返回元数据、结构还是正文证据 |
| 适用范围 | 什么时候该用,什么时候应换另一个工具 |
| 证明边界 | 这个工具能支持什么结论,不能支持什么结论 |
例如,search_blocks 的功能是找相关候选,因此它不能承诺枚举完整;list_blocks 的功能是按范围取全,因此它不能提供相关度排序;scan_term 的功能是检查确定字符串,因此它不能证明概念不存在。
这时工具设计只完成了"功能上能做什么"。空结果与错误的表达、请求快照、预算、稳定排序和版本治理,都是功能链路完成后再过质量属性时才发现的问题,见下篇。
3. 单工具设计:每个工具解决哪种失败
3.1 先把问题归成能力
| 能力 | 代表问题 | 检索要做的动作 |
|---|---|---|
| 单点事实 | 是什么/在哪里/对吗 | 语义或关键词命中目标块 |
| 单点但需多块 | 为什么/怎么做 | 命中后补同节上下文 |
| 证否与归属 | 有没有 X/这是谁说的 | 精确字符串扫描 |
| 跨源对照 | 多篇异同/哪个对 | 逐篇限定范围再比较 |
| 要全/枚举/整节 | 有哪些/所有要点 | 按范围取全部块 |
| 整体图景/换形态 | 概览/整理成表 | 先看大纲,再按节读取 |
| 消歧 | "OpenAI 那篇" | 列文档、缩候选、再澄清 |
一句话:
单点类靠候选召回;证否、枚举、跨源、整节靠范围控制和多轮。
3.2 五种能力落成五个工具
五个工具不是为了把功能拆碎,而是因为下面五种失败不能靠同一种证据补:
| 工具 | 它回答的问题 | 没有它会怎样 | 关键限制 |
|---|---|---|---|
list_documents |
库里有哪些文档,用户指的是哪篇 | 多篇同名或同来源时,Agent 只能猜对象 | 只返回元数据,不证明正文内容 |
get_outline |
这篇文档怎样组织,应该读哪一节 | 只能盲目全文读取,或不知道范围从哪里来 | 返回结构,不是内容证据 |
search_blocks |
哪些块可能回答这个问题 | 只能全文扫描,或让模型凭空猜位置 | 固定 Top-5,不声称全集 |
list_blocks |
选定范围里到底有哪些块 | 用户要"整节"时,Top-k 无法承诺完整 | 每次一篇;章节包含后代 |
scan_term |
某个确定字符串是否出现 | 无法做可解释的字面证否 | 只证明指定字符串是否出现 |
3.3 list_documents:先确定"说的是哪篇"
要解决的问题:检索最前面的失败往往不是没找到内容,而是找错了文档。用户说"OpenAI 那篇",库里可能有三篇 OpenAI 文档;如果 Agent 直接检索全库并且答得很像,错误反而更难发现。
输入:来源、语言、标题片段。来源和标题都支持用户常用别名,但别名只用于定位,不改变规范身份。
输出:文档 ID、标题、来源显示名、别名、章节数、块数。
为什么只返回元数据:这个工具的任务是确定对象,不是提供答案证据。如果同时返回正文,模型很容易把"列文档时顺带看到的一句话"当成完整证据。
关键边界:
- 多个候选无法唯一确定时,应要求用户澄清,不能自行猜一篇;
- 零结果只说明没有匹配到文档元数据,不能证明正文里没有某个内容;
- 文档标题要能用于最终引用展示,但内部身份始终是
doc_id。
3.4 get_outline:先看地图,再决定读哪里
要解决的问题:用户问"这篇文档的结构""整理成表"或"把某一节讲全"时,Agent 不能一上来读整篇。它需要先看到章节树,选一个可解释的范围。
输入 :一个或多个 doc_id。
输出:每个文档的章节路径、ordinal、depth、直属块数、直属 token、子树块数、子树 token 和首句。
为什么同时给直属和子树统计 :用户选择父章节时,真正要读的通常是整棵子树;但模型也需要知道该章节自己有没有正文。只给一个含糊的 blocks 数量,无法判断下一步该不该继续下钻。
为什么返回首句:章节标题有时不足以判断该节在讲什么,首句能提供少量导航信息。但首句只是结构提示,不是"这一节讲了什么"的完整证据。
关键边界:
- 同一父节下重名标题必须生成唯一 path;
- 没有章节结构的文档返回根节,避免模型遇到空大纲后无从下手;
- 多文档时保持输入顺序,重复
doc_id只取首次。
3.5 search_blocks:找候选,不承诺全集
要解决的问题:单点事实、机制解释和多跳问题的第一步,通常是先找到"可能相关的位置"。这就是经典语义检索最擅长的部分。
输入 :自然语言 query,可选 doc_ids 限定范围。
输出:最多 5 个候选块,每块包含来源、文档标题、章节、原文和块 ID;另附空结果原因与 token 估算。
为什么固定 Top-5:Top-k 是工具内部策略,不该让模型在每次查询时重新拍一个数字。固定值让行为更容易比较、调参和回归;未来若失败样本证明某些问题需要更宽召回,再按问题类型调整。
为什么不让模型选向量/关键词/混合模式 :模型不知道当前索引质量、两路分数分布和词面覆盖率。把技术模式交给它,只是在没有依据的参数上增加一个决策点。当前固定混合检索,精确字面需求交给 scan_term。
为什么分数只进 trace:未校准的分数对模型没有可解释意义,却容易诱导它自己设阈值。分数应该用于开发者调试和调参。
关键边界:
- 没有命中不代表文档没讲,只代表候选召回没有返回;
- query embedding 上游失败必须报错,不能静默降级成纯关键词检索;
- 排序稳定:分数相同按文档、块顺序和块 ID 决定平局。
3.6 list_blocks:在明确范围里取全
要解决的问题:用户问"这一节都有什么""把所有要点列出来"时,Top-k 原理上无法提供完整性。需要一个能对范围承诺"我返回了其中全部块"的工具。
输入 :单篇 doc_id;可选章节路径数组。空数组表示整篇。
输出:该范围内全部块、总数和 token 估算。
为什么一次只处理一篇:跨多篇同时取全,会让"这个 section path 属于哪篇文档"产生歧义。跨篇需求可以先在每篇内取全,再由 Agent 合并;只有实测证明轮次过多,才考虑更复杂的 scopes 结构。
为什么选父章节要包含后代:用户说"这一节"时,通常把子节也视为该节的一部分。如果实现只返回直属块,"取全"会静默失效。当前明确:一个 path 选择该节及全部后代;重叠路径取并集。
为什么路径不存在要整体报错:如果返回其他路径的结果再附一个警告,模型可能忽略缺失部分,把半套结果当成完整范围。明确失败更安全。
关键边界:
- 返回顺序按原文
block_index,不是相关度; - 工具不做预算截断,能否进入上下文由 Harness 判断;
- 缺失章节不能退化为空列表。
3.7 scan_term:为字面问题提供可解释的证否
要解决的问题:有些问题问的是确定字符串、原词归属或是否出现。语义检索"没召回到"无法支持"没出现"的结论。
输入:确定 term;可选文档范围。
输出:全部命中块、snippet、命中数和 token 估算。
为什么先做 Unicode NFC 和英文大小写折叠:不同平台产生的等价字符和英文大小写不应影响字面判断;但不做分词、词干、同义词、空白或标点折叠,否则"精确出现"会悄悄变成模糊匹配。
为什么只返回 snippet:这个工具先回答"在哪里出现了",不负责提供完整上下文。需要核对完整语义时,再用块 ID 取全文或相邻章节。
关键边界:
- 空 term 是参数错误;
- 显式范围中有文档不存在时整体报错,不能给出
count=0; count=0只证明指定字符串在有效范围内没出现;- 同义词扩写可以提高召回,但不能把"没找到"升级成"概念不存在"。
3.8 为什么不是一个大工具,也不是十几个小工具
| 方案 | 问题 |
|---|---|
一个大而全的 retrieve |
"选对象、找候选、取全、精确扫描"混在一个参数空间里;模型难判断该传什么,返回形态也无法稳定解释 |
| 每个细节一个工具 | 工具选择成本高,schema 和提示词膨胀,模型容易在相近工具间反复试探 |
| 五个按证据动作划分的工具 | 每个工具只承诺一种能力;失败后能判断该换路径、换范围还是澄清 |
3.9 三条最有价值的边界
search_blocks没有命中,不代表文档没讲。 它只返回候选,不是全集。scan_term只能证明指定字符串没有出现,不能证明概念不存在。list_blocks要全,search_blocks要找相关。 两者的完整性承诺不能混。
实例|精确字符串 vs 语义概念
- "文档里有没有出现字符串
MCP?" →scan_term。 - "文档讨论 MCP 吗?" →
search_blocks;没有召回只能说没找到相关证据。 - 已知别名可以逐个精确扫描,但
count=0只对"实际扫描过的写法集合"负责。
4. Agent 循环:模型选择动作,Harness 控制执行
前面的工具定义了"能做什么",循环定义"什么时候做、做完之后怎么办"。
4.1 为什么模型只做语义选择,Harness 掌握执行控制
Text ReAct 里,模型本来就只能生成文本。即使使用原生 function calling,真正发请求、访问数据库和执行工具的仍是宿主程序。所以这里不存在"把执行交给模型"这个选项。
真正要划分的是:哪些判断允许交给概率模型,哪些控制必须由代码确定性执行。
| 角色 | 负责什么 | 不负责什么 |
|---|---|---|
| 模型 | Thought、Action、是否继续检索 | 文件 I/O、执行权限、伪造 Observation |
| Harness | 解析 Action、执行工具、维护 History、生成 Observation | 判断内容是否充分、替模型写答案 |
| 工具 | 执行一个检索动作,返回业务 payload | 决定下一步、判断内容质量 |
这样划分的原因是两类能力性质不同:
- 模型负责语义:用户要什么、下一步查什么、已有证据是否足够;
- Harness 负责确定性约束:解析、权限、状态、错误归一和运行边界;
- 权限与预算一旦只写进提示词,就只能"希望模型遵守";放进 Harness 才是真正执行;
- 同一条循环里的每次尝试都走同一套协议,失败和 Trace 才能比较。
停止条件也要分成两类:
- 语义停止:"证据已经足够"或"必须澄清",由模型判断;
- 运行停止:轮次、上下文和累计 token 到达上限,由 Harness 强制执行。
质量型运行边界放在下篇展开;这里只保留功能判断。
4.2 一次请求怎样在循环里推进
模型输出 Thought + Action
→ Harness 解析
→ 执行工具
→ 生成 Observation
→ 追加到 History
→ 下一轮模型判断
→ 继续 / 澄清 / Finish
终止来自四种功能状态:
- 证据足够,可以回答;
- 对象无法唯一确定,需要澄清;
- 必需证据无法取得,需要说明缺口;
- 重复动作不再产生新证据,继续没有意义。
质量型运行边界------轮次、上下文与累计 token------统一放到下篇讨论。
4.3 一次完整请求
实例|查一篇文档中的具体机制并给出依据
用户问题:
《How we contain Claude》里 local VM 的隔离边界是什么?请给出依据。
第 1 轮:确定文档对象
css
Thought: 先确认用户点名的是哪篇文档,并取得 doc_id。
Action: list_documents[{"title_contains":"How we contain Claude"}]
Observation:
documents = [
{doc_id: "doc_a5", title: "How we contain Claude", source: "Anthropic"}
]
第 2 轮:找相关候选
makefile
Thought: 范围已确定,先找 local VM 隔离边界的候选块。
Action: search_blocks[{
"query": "local VM 隔离边界 文件系统 进程 网络",
"doc_ids": ["doc_a5"]
}]
Observation:
blocks = [
{block_id: "a5#c020", section_path: ["Pattern 3: The local VM"], ...}
]
第 3 轮:补全目标章节
lua
Thought: 候选落在 Pattern 3;为完整说明边界,读取该节及其子节。
Action: list_blocks[{
"doc_id": "doc_a5",
"section_paths": [["Pattern 3: The local VM"]]
}]
Observation:
blocks = [该节及全部后代块,按原文顺序]
结束
css
Thought: 已取得局部机制与完整章节上下文,可以回答并标注块级来源。
Action: Finish[答案......【来源:doc_a5#c020】【来源:doc_a5#c021】]
这条路径不是"工具越多越好",而是每轮解决一个新的不确定性:对象、位置、范围。
5. 提示词:把跨工具规则写清楚,而不是复述工具说明
5.1 为什么不能把所有规则都塞进提示词
有些规则不是"提醒模型尽量遵守",而是系统必须确定执行的约束:
- 不存在的工具不能调用;
- 格式错误的 Action 不能被执行;
- 权限、运行边界不能被模型改写;
- 没有文档块支持的句子不能被标成文档来源。
这些必须由 Harness 和工具契约保证。提示词负责跨工具协作:什么时候继续、什么时候停止、怎样比较多个来源、什么时候需要澄清。
5.2 提示词分层
| 区块 | 负责什么 |
|---|---|
role |
产品定位与能力边界 |
tools |
注入工具描述 |
response_protocol |
Thought/Action 格式 |
observation_protocol |
Harness 状态与 payload 的关系 |
scope |
越界、歧义与澄清 |
evidence |
文档事实、模型补充与证据边界 |
coverage |
枚举、跨源和遗漏透明 |
citation |
什么内容必须标来源 |
answer |
答案形态 |
termination |
何时停止 |
5.3 引用是内部键,展示是另一层
模型输出机器引用键:
bash
【来源:doc_id#block_id】
前端再解析成文档标题、来源名和块预览。内部 ID 不应直接成为最终用户文案。
6. 检索结果交给模型的形态
第一阶段让检索工具直接返回块全文,不先做摘要或剪枝。
原因不是"别的方案不好",而是:
- 数字、限定词和归属判断最怕摘要失真;
- 金标准里的精确事实需要能回到原文;
- 初版少一层摘要生成和版本链,问题更容易定位。
摘要和剪枝需要它们自己的触发证据:当上下文压力成为主要瓶颈,并且摘要路线在完整性、事实准确性和 token 成本上都有可比收益时再引入。
7. 检索侧对入库侧提出什么功能要求
工具能取到什么,取决于入库侧交付什么。功能设计阶段只要求入库产物支持五个检索动作,不在这里追加可靠性和版本治理要求。
7.1 文档可以被识别和消歧
- 每篇文档有稳定的
doc_id; - 提供规范标题、用户可读的来源名和常用别名;
- 提供语言、章节数和块数;
- 别名只用于定位,不能替代规范身份。
这支撑 list_documents:Agent 能先把"OpenAI 那篇"收敛成确定文档,而不是直接猜一篇开始检索。
7.2 章节可以被选择和取全
- 章节 path 在同一文档内唯一;
- 提供父节、ordinal、depth 和原文顺序;
- 同时提供直属块/token 与子树块/token;
- 没有章节结构的文档返回根节。
这支撑 get_outline 和 list_blocks:Agent 能先确定"读哪里",再对选定范围承诺取全。
7.3 块可以被定位、读取和引用
- 每块有稳定
block_id、doc_id、section_path和block_index; - 保存完整原文,而不是只保存向量或摘要;
- 保留原文中的字符位置;
doc_id#block_id能解析回文档标题、来源和块预览。
这支撑 search_blocks、list_blocks、scan_term 和最终引用展示。
7.4 两种检索入口可以被构建
- 语义检索需要与块绑定的向量索引;
- 精确锚点需要基于原文的关键词索引;
- 两种索引都从同一批块构建,不能让两路结果指向不同的文本版本。
这支撑固定的混合检索:语义路找相关候选,关键词路补专名和数字。
功能设计只要求这些内容"能被工具正确取到"。快照完整性、双 token 口径、构建状态、检索常数和历史快照生命周期属于质量属性追加要求,见下篇。
8. 这次刻意不做什么
| 暂不做 | 为什么 | 什么信号会推翻 |
|---|---|---|
| 前置检索路由器 | 增加判断入口,纯 Agent 尚未证明低效 | 简单题延迟持续不达标,且快车道通过门槛 |
| 自动重排 | 先看混合检索+Agentic 是否已足够 | 候选已召回,但排序持续错误 |
| 上下文自动剪枝 | 会改变 History,影响审计 | 多轮上下文成为主要瓶颈 |
| 独立证据组装器 | 完整 payload 已可进入 History | 重复证据和噪声持续导致失败 |
| 摘要/Wiki 硬依赖 | 第一阶段问题不要求预编译知识形态 | 面状覆盖、关系取全出现稳定缺口 |
9. 一页速查
模式:先让一套 Agentic 路径覆盖全部问题;简单题是否加快车道,由延迟实测决定。
判断位置:默认纯 Agent;不把"单点还是复杂"提前交给分类器。
五工具 :list_documents/get_outline/search_blocks/list_blocks/scan_term。
三条边界:
search_blocks是候选召回,不是全集;scan_term证否字符串,不证否概念;list_blocks的范围包含后代章节。
一个循环:模型选择下一步,Harness 执行并返回 Observation;对象、位置、范围分别收敛。
一个分工:工具描述局部能力,提示词描述跨工具规则,Harness 执行确定性约束。
一句话 :先选执行模式,再定工具与循环,最后才写提示词;不要反过来用一段提示词补系统结构。
附录 A|工具能力摘要
| 工具 | 核心输入 | 核心输出 | 不能证明什么 |
|---|---|---|---|
list_documents |
来源/语言/标题片段 | 文档 ID、标题、来源、别名、规模 | 正文内容是否存在 |
get_outline |
一个或多个 doc_id | 章节 path、直属/子树规模、首句 | 章节正文事实 |
search_blocks |
query、可选 doc_ids | Top-5 块、空结果原因、token 估算 | 全集与"文档没讲" |
list_blocks |
单篇 doc_id、可选 section_paths | 范围内全部块与 token | 相关性排序 |
scan_term |
term、可选 doc_ids | 全部命中、snippet、count | 概念等价、语义近似 |
附录 B|运行质量与入库接口
预算、Trace、可复现、版本与部署见下一篇《让检索 Agent 可运行:预算、Trace、版本与回滚》。
完整的工具契约、提示词结构、质量属性检查表和入库接口见:检索侧设计完整记录(HTML 源文件)。