Agentic RAG 能检索还不够:从质量属性反推错误、预算、Trace 与版本管理

6 阅读33分钟

30 秒判断:检索侧功能设计完成,只说明“系统能取证据”;一旦按质量属性逐条检查,工具返回、快照、预算、日志、版本和部署都会反过来改架构。本文以质量属性表为目录,逐项说明每条要求为什么出现、最终改变了什么。

核心结论:可运行不是“接口返回 200”,而是系统能说清一次答案用了哪些证据、花了多少成本、基于哪一版知识库,以及在什么条件下能够复现、回滚和安全部署。

适合谁读:RAG/Agent 已经跑通、但开始遇到“答案变了却查不出原因”的人;准备把原型交付给普通用户的人;正在设计 Trace、预算、版本或升级流程的人。

本文怎么读:这是检索侧设计下篇。上篇完成功能设计;本篇从质量属性表出发,检查功能设计遗漏了哪些运行约束。每节先讲“质量属性发现了什么”,再给设计结果。

前置阅读:系列前四篇。①《Agent 开发为什么要测试驱动升级版》;②《需求定义四步:用一个文档问答 Agent 演示“先定义、再动工”》;③《金标准:用一个文档问答 Agent 演示“什么算对、怎么判”》;④《由检索需求反推存储》。上篇:Agentic RAG 检索侧设计。

结构速览:§0 质量属性总表;§1 正确性与可靠性;§2 一致性;§3 成本;§4 可观测性;§5 可复现性;§6 可维护性;§7 可安装性;§8 安全性;§9 属性之间的冲突;§10 最终设计快照;§11 一页速查。

证据标记:✅ 实测=有可复现实验数据支撑;🤔 推断=由机制或设计要求推出。本文主要是架构设计推导,尚未完成完整系统验收。

先交代几个名词

  • Trace:一次请求的结构化执行轨迹。
  • Span:Trace 树中的一个步骤,例如一轮 Agent、一次模型调用或一次工具调用。
  • Artifact:关联到 Span 的完整内容,例如提示词、工具返回和最终答案。
  • 运行清单(manifest) :一次运行使用的应用、提示词、循环、工具、模型和 collection 版本。
  • prompt bundle:一起发布、一起加载的一套提示词模板与协议文案。
  • 证据复现 / 检索重放 / 模型重跑:分别指还原模型输入、重跑旧检索、同配置重新生成答案。

0. 功能设计之后,用质量属性表逐条反查

实例|文档学习问答 Agent

上一篇已经完成检索侧功能设计。功能上,它已经有模式选择、五个工具、Agent 循环和提示词。

但“功能能跑”离“产品能交付”还差一层。把质量属性逐条拿来问,会得到一张明确的缺口表:

质量属性反查时发现的问题最终改变了什么
正确性/完整性合法空结果、失败和部分入库可能被混淆分层返回状态;暴露快照完整性
可靠性所有错误都被当成空列表;重试没有统一边界区分 Harness/工具/上游错误;统一重试与降级
一致性一次请求可能中途切换 collection请求固定快照;排版与排序规则稳定
成本单次上下文有上限,累计调用仍可能失控上下文/请求两套预算;Harness 唯一准入
可观测性日志只有文本,无法还原多轮结构本地 Trace 树与 Artifact
可复现性知道失败,却不知道当时用了哪一版运行版本清单;只读快照;模型重跑统计
可维护性升级、迁移和回滚没有兼容边界版本契约、copy-on-write、变更触发回归
可安装性开发环境能跑,不等于普通用户能装桌面化、内置依赖、双 Key、仅回环
安全性文档可能包含恶意指令,模型也可能越权只读工具、Harness 白名单、不可信输入

后面的章节就按这张表展开。每一项都不是孤立功能,而是在补功能设计没覆盖到的系统约束。


1. 正确性与可靠性:先让结果语义可判定

1.1 一个空数组可能来自五种事实

功能设计里,工具返回一个列表似乎就够了。但同一段 [] 可能表示:

  1. 精确扫描确实没有找到这个字符串;
  2. 语义检索没有召回到相关候选;
  3. 指定章节根本不存在;
  4. query embedding 服务暂时不可用;
  5. Action 格式错误,Harness 根本没有执行工具。

它们对 Agent 的下一步完全不同。都返回空列表,就等于把“材料没有”“参数错了”“系统坏了”混成同一个事实。

因此结果必须按执行位置分层:

模型输出 Action
  → Harness 解析、权限检查
  → 工具执行业务动作
  → 上游依赖完成请求
层返回位置典型情况是否错误
执行控制层harness_statusAction 无法解析、工具不在白名单、预算边界、Harness 异常是
工具业务层payload.error参数无效、文档/章节不存在是
工具执行层payload.errorcollection 不可用、embedding 失败、工具内部异常是
合法空结果payload.empty_reason没有匹配、范围内没有块、知识库为空否

harness_status=ok 只说明 Action 被执行,不代表工具业务成功。完整状态和错误码见附录 A。

1.2 为什么会有 partial:批次可以部分成功,单篇不能残缺上线

实例|一次导入 10 篇文档

其中 8 篇完整成功;1 篇 PDF 解析失败;1 篇因为 embedding 服务限流而失败。

如果要求 10 篇全部成功才能使用,那么一条坏数据就会让另外 8 篇也无法查询。对个人学习场景,更合理的策略是:让完整成功的文档先可用,同时把失败文档和失败原因明确暴露出来。

因此快照需要区分四种状态:

状态含义是否可用于问答
building新快照仍在构建否;继续使用旧快照或提示等待
ready批次中所有目标文档完整成功是
ready_partial批次结束,部分文档完整成功、部分失败是,但必须披露缺口
failed没有形成可用快照否

这里必须把“partial”限定在快照/批次级:

  • 允许 8 篇成功、2 篇失败;
  • 不允许某一篇只解析了一半就伪装成成功文档;
  • 单篇文档只有解析、切分、embedding 和索引全部完成,才进入成功集合;
  • 失败文档不进入块检索,而是写入 failed_documents 清单。

否则 list_blocks 会对一个已经缺章、缺页的文档承诺“整篇/整节取全”,完整性红线直接失效。

ready_partial 必须把失败文档清单注入模型上下文。模型可以基于已有文档回答,但必须说明缺失来源,不能把“8 篇成功”说成“知识库完整”。

1.3 错误与重试要有统一边界

情况处理
工具业务错误:参数错、范围不存在不自动重试,让 Agent 修正
协议错误:Action 无法解析转成 Observation,让下一轮修正
瞬时依赖错误:超时、限流最多自动重试 1 次
部分来源失败允许带缺口回答,但必须具名声明
终结轮再次失败返回确定性错误,不伪造最终答案

重试不设独立预算,所有实际模型 token 仍受请求预算约束。


2. 一致性:一次请求必须固定在同一份知识库上

功能设计中的工具调用是连续的。一次问答可能先列文档、再搜索、再按章节读取;与此同时,后台也可能在重建 collection。

如果前半轮读旧索引、后半轮读新索引,同一份答案里的证据就来自两个时点。用户看到的是一段连续答案,系统内部却已经换了知识库。

因此:

请求开始时固定 collection 快照;本次所有工具共用该快照;新快照只原子切换活动指针,不影响进行中的请求。

一致性还要求工具顺序稳定:检索分数相同时,要有确定性的平局规则;相同快照、输入和参数下,结果不能因容器遍历顺序而漂移。


3. 成本:单次上下文和整次请求都要有边界

3.1 为什么只有单次上下文不够

Agent 每轮都会把 History 重新交给模型。上一轮的 Observation 即使不再使用,也会在后续调用中继续占据上下文。

因此至少需要四类运行边界:

  • 单次上下文 token;
  • 本次请求累计对话 token;
  • 最多逻辑工具调用数;
  • 为最终回答预留的输出 token。

embedding token 要按 embedding 供应商口径单独记录,不能和对话模型 token 直接相加。两者 tokenizer 和计价单位不同,混算后的数字没有解释力。

3.2 预算由 Harness 做一次

工具返回范围内完整结果和 token 估算,自身不做预算判断。Harness 在结果进入 History 前做唯一一次准入:

  • 放得下:完整进入;
  • 放不下:丢弃本次结果,返回明确预算错误;
  • 整次请求累计耗尽:停止继续调用,必要时进入终结轮。

3.3 预算本身不是最难的

单次上下文和累计 token 都可以调参,真正困难的是:工具返回结果时,成本约束可能和完整性直接冲突。

例如 list_blocks 要把一整节取全,scan_term 要返回范围内所有命中;内容越多,完整性越高,所需上下文也越大。

因此不能只问“每次返回多少”,还要问“完整覆盖成立时,结果是否还能进入模型”。这个冲突单独放在 §9 展开。


4. 可观测性:Trace 不是更长的日志

Trace 的价值不只是“出问题时方便调试”。对这个 Agent 来说,便捷地分析一次答案为什么形成,是评测和真实反馈能够转化成改进的前提。

4.1 普通日志为什么不够

文本日志记录的是按时间排列的事件,Agent 需要的是把事件串成因果链。

例如最终答案说“Claude Cowork 通过 VM 提供隔离边界”。答案如果错了,可能至少有四种原因:

  1. 模型没有调用相关工具,直接凭内置知识生成;
  2. 调用了检索,但没有召回目标块;
  3. 召回到了块,结果被预算准入拒绝,没有进入下一轮;
  4. 块进入了上下文,但模型生成时漏写、错写或忽略。

对应要修的层完全不同:提示词、检索、预算或生成约束。只看到 search_blocks succeeded 和 finish received,无法判断该改哪里。

因果链需要回答:

  • 哪一轮模型看到了哪些 Observation;
  • 哪个工具结果支持了最终哪一句;
  • 上一次工具调用和当前模型调用是什么父子关系;
  • 重试的是同一操作,还是模型改变了方案;
  • 预算在哪一步拒绝结果,版本何时发生变化。

Agent Trace 至少需要有树结构:

一次请求
├─ Agent 第 1 轮
│  ├─ LLM 调用
│  └─ 工具调用
├─ Agent 第 2 轮
│  ├─ LLM 调用
│  └─ 工具调用
└─ 最终回答

每个 Span 记录 ID、父子关系、时间、状态和关键属性;完整提示词、工具 payload、最终回答作为 Artifact 关联保存。

这正是 Agent 的改进闭环:

真实反馈 / 评测结果
  → 定位失败发生在哪一层
  → 提出针对性修改
  → 重跑对照
  → 固化有效改动

没有因果链,反馈只能说明“答案不好”;有因果链,才能回答“哪一步让答案变坏”。否则提示词优化只能退化成凭感觉修改,既无法证明改动有效,也无法防止修好一个问题的同时破坏另一个。

4.2 本地 viewer 是主路径

这个判断不是从通用 Agent 平台直接抄来的,而是由产品定位推出来的。这个产品是单用户、本地知识库、Windows 桌面应用,主要使用者又可能是不懂运维的普通用户。

产品约束对可观测性的影响
问题和资料都在用户本机发生问题时,需要诊断的 Trace 也首先在本地
用户是普通非技术用户不能要求他安装 Collector、Langfuse 或数据库后再排障
单机应用不应依赖额外服务Viewer 不能把外部观测平台当作运行前提
我们开发和测试也在自己机器上进行本地一键查看能支持日常调试,不必每次搭建后端

因此默认路径必须是:应用把 Trace 写入本地数据目录,产品自己的 viewer 直接列出请求并打开轮次、工具、预算和证据。

OTLP、Langfuse 等平台适合更深入的分析或团队共享,但它们只能作为可选出口:用户不配置时,产品仍能完成安装、问答和本地排障。

这也是“单一产品”约束的延伸:如果每次运行都要先上传或导入外部平台才能查看,单机自诊断就把自身变成了新的运维负担。

“便捷”不是体验问题,而是反馈闭环能否成立的问题:如果打开现场需要多处查找、拼接多个文件,开发者和用户都不会持续做这套分析,再完整的 Trace 也无法转化为 Agent 的改进。

4.3 Trace 要回答哪些问题

  • 模型实际看到了什么提示词?
  • 工具收到什么参数,返回了哪些块?
  • 检索排名与分数是什么?
  • 预算在哪一步拒绝了结果?
  • 错误来自协议、工具、上游还是 Harness?
  • 本次用了哪一版 prompt、模型和 collection?

Trace 写入失败不能影响主回答。事件先进入有界队列,由后台 writer 落盘;失败时只把 Trace 标成部分/丢弃并告警。


5. 可复现性:先说清版本,再谈重放

5.1 版本清单

代码不是唯一会改变答案的东西。一次运行至少涉及:

  • 应用与构建版本;
  • prompt bundle;
  • Agent 循环与 Action/Observation 协议;
  • 工具契约;
  • 对话模型与 embedding 模型;
  • collection 快照;
  • 运行预算和其他影响行为的配置。

这些版本随 Trace manifest 记录。版本号供人阅读,内容 hash 才是精确身份。

5.2 提示词采用“发布基线+本地实验”

只把提示词绑在 Git commit 上,适合发布,不适合快速实验:同一版代码无法方便比较多个提示词版本,每次试验也会和代码提交混在一起。

因此采用两层:

  • 发布基线:随源码/安装包分发,只读、不可原地修改;
  • 本地实验注册表:数据目录中的候选 bundle,同一版应用可加载多个版本;
  • 实验一旦用于正式评测立即冻结;
  • 候选经人工复核后才能提升为新的发布基线;
  • Trace 记录来源、bundle ID、版本、hash 和父版本。

这不要求部署远程 prompt 管理服务。

5.3 可复现分成三层

能力成立条件能回答什么
版本审计有 metadata Trace 和运行清单哪一版应用、提示词、模型和 collection 参与了
证据复现full Trace 的 artifacts 完整模型当时精确看到了什么,工具返回了什么
检索重放旧 collection 快照仍可加载相同查询能否得到相同块和排序

模型重跑是另一类验证:即使输入与配置相同,也只承诺同分布,不承诺逐字一致。应重复运行 N 次,报告通过率、失败类型和跨次翻转。


6. 可维护性:版本变化怎样兼容和回滚

6.1 需要版本化的契约

  • Trace schema;
  • 配置 schema;
  • 文档/块/索引 schema;
  • Action/Observation 协议;
  • 工具参数与返回;
  • prompt bundle;
  • collection 快照。

会改变字段语义的修改属于破坏性变更;能追加可选字段、保持旧读取路径的修改才属于兼容变更。

6.2 升级优先 copy-on-write

安全顺序:

检查兼容性
→ 备份必要配置与数据
→ 迁移或构建新快照
→ 启动自检
→ 原子切换
→ 确认成功后清理旧版本

不在唯一一份用户数据上直接做不可逆迁移。

6.3 三种回滚要分开

  • 提示词回滚:切回上一发布 bundle;
  • 应用回滚:切回上一安装版本;
  • 数据回滚:切回旧 collection 快照或恢复迁移前备份。

“代码能回滚”不等于“数据也能回滚”。任何不可逆迁移都必须在发布前说明最低回滚边界。

6.4 质量属性对入库产物的追加要求

上一篇只要求入库产物“能被工具取到”。经过质量检查后,还要追加:

  • 快照报告完整、部分成功或失败;
  • 向量索引、关键词索引、文档和块绑定同一快照;
  • 对话上下文 token 与 embedding token 分开统计;
  • 检索参数进入 manifest;
  • 旧快照支持生命周期管理;
  • 历史引用映射不随后续重命名而漂移;
  • 切分、embedding、索引和失败阶段都有可对账的构建统计。

7. 可安装性:用户只应感知一个应用

实例|Windows 单机版

部署约束不是最后打个包,而是会直接改变技术边界:

  • Windows x64 桌面应用;
  • 用户不需安装 Docker、数据库、Python 或 Node;
  • 对话使用用户自带的 DeepSeek API Key;
  • embedding 使用另一家云端供应商,另填一个 Key;
  • 不要求 GPU,也不提供本地 LLM;
  • 应用只监听本机回环地址;
  • 导入与建索引在后台执行,可暂停和续跑;
  • Trace 默认记录元数据,用户可主动开启完整诊断;
  • 更新由用户确认,升级前备份,失败可回退。

API Key 存操作系统凭证保护机制,不进入配置、日志、Trace、诊断包或备份。

目标不是“开发机可以启动”,而是:干净 Windows 机器上的普通用户,能完成安装、配置两个 Key、通过连通性测试并问出第一个问题。


8. 安全性:只读工具也需要边界

当前产品的主要安全风险不是取得宿主机控制权,而是:

  • 文档中的恶意指令影响答案、范围和引用;
  • 模型尝试调用产品没有开放的工具;
  • 权限和网络能力被无意扩大。

最低防线不是“在提示词里写不要作恶”,而是:

  • 工具只读;
  • Harness 使用不可变白名单;
  • 文档内容始终作为不可信资料;
  • 模型和文档都不能修改权限;
  • 未来若加入写文件、网络或第三方工具,重新评估安全架构。

9. 质量属性之间怎样互相拉扯

质量属性很少能分别达到最优。提高完整性通常会增加成本;为了复现保留更多快照,会占用磁盘;把观测组件做重,会破坏单机安装;为了安全收紧权限,又可能损害易用性。

因此,通过质量属性检查不是逐项打勾,而是找出它们相互限制的位置,再明确当前产品选择哪一边、承担什么代价、在什么条件下推翻。

下面只完整展开“完整性与成本”这一个例子,其余冲突用简表收口。

9.1 示例:完整性与成本

9.1.1 最初的方案:先按成本定 32K/160K

预算设计最先受到成本约束。最开始给检索循环设了两个上限:

  • 单次模型调用上下文:32K token;
  • 一次请求累计对话 token:160K token。

这个方案的假设是:单次检索几个块、模型正常回答,32K 已经够用;累计 160K 也足以覆盖几轮 ReAct。先设小一点,可以避免 Agent 多轮检索失控。

单点事实问题基本能在预算内完成,因此这个初值没有立刻暴露问题。

9.1.2 完整性检查推翻初始预算

实例|比较四篇文章的主旨

假设四篇文档各约 40k token,合计约 160k;再叠加系统提示词、工具定义、历史轮次和最终回答,所需上下文更大。

这时矛盾出现了:

  • 完整性要求“四篇都要看”,不能用每篇的 Top-5 后声称已经比较完整;
  • 最初 32K 的单次上下文连一篇 40K 的文档都放不下;
  • 原方案虽然省成本,但会把“合法、常见的多文档问题”直接判死。

这不是简单调大 Top-k 能解决的。search_blocks 是候选召回,固定 Top-5 本来就是它的功能;真正承担完整性的是 list_blocks 和 scan_term,它们必须返回范围内全部结果。

于是问题从“预算数字是多少”变成了:

当完整取证所需内容超过上下文时,系统应该怎样处理?

9.1.3 先比较所有候选方案

方案用户会得到什么主要问题结论
一次返回完成问题所需的全部原文,不设上限小范围可以完整拿全内容总量超过上下文时直接失败不采用
固定只返回前 N 个块单次成本稳定用户要“全部”时只能得到一部分不采用
分页读取,再逐页交给模型单次工具响应较小最终总内容没有减少,调用次数和状态复杂度增加不采用
工具返回范围内全部结果,Harness 统一预算准入预算内完整;预算不足时明确失败超大范围不会直接得到答案采用
预摘要/Wiki summary总览更快、更省 token视角取舍、编译成本和回源问题实测后再引入

9.1.4 为什么分页没有成为答案

分页是个很自然的想法:整章有 120 块太大,那就每次返回 20 块,让 Agent 逐页读取。

它确实能缓解“单次响应过大”,但没有解决最终容量:

  • Agent 要完整回答整章,最终仍需看到全部 120 块;
  • 每取一页都要追加一轮模型调用,累计 token 可能更高;
  • Agent 要维护 cursor 和“已经看到哪里”,增加状态;
  • 模型可能看前几页就误判证据已经足够;
  • 如果全部内容本身放不进上下文,分页后仍然放不下。

所以分页适合 UI 列表、数据导出或人工浏览,不适合这里的完整证据循环。

9.1.5 最终方案:用户配置预算,Harness 负责执行

完整性与成本的冲突没有单一最优值。成本由用户自己承担,不同任务的规模也不同,所以最终没有把 32K/160K 写死,而是改成:

  • 用户可配置 max_context_tokens 与 max_request_tokens;
  • 系统提供默认值:单次上下文 100K,本次请求累计对话 token 400K;
  • 从累计预算中预留 4K 给最终回答;
  • 系统只负责默认配置校验和执行,不替用户决定愿意花多少钱。

执行流程是:

  1. 工具按范围取全,并返回结果 token 估算;
  2. Harness 在结果进入 History 前,做唯一一次预算准入;
  3. 放得下,完整进入;
  4. 放不下,明确返回预算错误,并建议提高预算或缩小范围;
  5. 不允许只放一半,却让模型误以为已经覆盖全部。

这个方案承认:完整性与成本不能同时无限满足。 产品能做的是让用户知道边界在哪里,并在边界内保证结果完整。

9.1.6 这个选择的代价与推翻条件

代价是:合法但过大的请求会明确失败,而不是慢慢分页或悄悄返回一部分。

这是有意的:

  • 用户会知道需要提高预算或缩小范围;
  • 系统不会返回“看起来完整”的半套结果;
  • Token、模型调用和失败原因都能进入预算与 Trace。

如果后续经常出现“合法请求在允许预算内仍无法完成”,就应评估抽取、摘要或 Wiki;不能先把完整性问题伪装成分页问题。

9.2 其他质量属性冲突

冲突选择
可复现性与磁盘占用保留 collection 快照并允许用户配置期限;快照失效后如实降级能力
可观测性与可安装性本地 Trace 是主路径,Langfuse/OTLP 只作可选出口
安全性与易用性只读工具+Harness 白名单;权限不交给提示词,也不让用户逐次确认

这些取舍说明:质量属性不是附录检查表,它们会重新定义接口和运行结构。


10. 质量属性调整后的最终设计快照

前面按质量属性逐项修正了工具、循环和提示词。为了便于对照,这里把三份最终设计集中列出。

10.1 工具契约最终版

五个工具仍按“功能动作”划分,但经过质量属性检查后,增加了统一运行约束。

工具输入约束输出约束行为与证明边界
list_documents来源、语言、标题片段均可选;source 匹配规范来源名或别名,大小写不敏感;title_contains 只匹配标题,不搜索正文返回 doc_id / title / source_label / aliases / lang / sections / blocks、total 和结果 token 估算;按来源 → 标题 → doc_id 排序只用于对象定位和消歧;多候选无法唯一确定时应澄清;零结果只表示没有匹配到文档元数据,不能证明正文没有内容
get_outline传入一个或多个 doc_id;重复 ID 只取首次,输出保持输入文档顺序每个文档返回标题、来源和章节列表;每节包含唯一 path、ordinal、depth、直属/子树块数与 token、首句;另附结果 token 估算返回结构,不是正文事实;重名标题必须唯一化;无章节文档返回根节;章节按原文顺序;不返回 block ID
search_blocksquery 必填;doc_ids 可选,省略表示全库,显式空数组非法;指定文档不存在时整体报错返回 Top-5 块的引用、文档标题、来源、章节和全文,另附空结果原因与 token 估算固定混合检索,模型不能切换检索模式;它是候选召回,不是全集;分数只进 Trace;未命中不能证明文档没讲;query embedding 失败不静默降级成关键词
list_blocks单篇 doc_id 必填;section_paths 可选,省略或空数组表示整篇;跨篇必须分别调用;任一路径不存在时整体报错返回该范围全部块的引用、标题、来源、章节和全文,total 必须等于实际块数,另附结果 token 估算;按原文 block_index 排序一个章节 path 包含该节及全部后代;多个 path 取并集并按原文顺序去重;工具不自行截断;路径缺失不能返回部分结果冒充完整
scan_termterm 去首尾空白后必须非空;doc_ids 可选,显式范围内文档不存在时整体报错返回全部命中的块引用、标题、来源、章节、snippet、count 和结果 token 估算;按文档、再按块顺序排序只做 Unicode NFC 与英文大小写折叠;不做分词、词干、同义词或标点归一;count=0 只证明指定字符串未出现,不证明概念不存在;snippet 是定位信息,不是完整上下文

所有工具共享:

  • 同一请求固定同一 collection 快照;
  • 返回合法空结果、工具错误或运行状态的明确语义;
  • 由 Harness 在结果进入 History 前做唯一一次预算准入。

10.2 Agent 循环最终版

一个请求只有一个 Agent 状态机;工具调用不是独立循环,而是一轮决策中的可选动作。

术语含义
question / history当前问题,以及本次请求内已累积的 Thought、Action、Observation
agent_turns模型产生决策的次数,包括最终 Finish
tool_calls逻辑工具动作数;自动重试不增加该计数
model_calls实际模型请求数,包括自动重试
context_tokens / used_tokens下次调用预计上下文 token/本次请求累计对话 token
max_context_tokens / max_request_tokens单次上下文/整次请求的预算上限
finalization_reserve_tokens为最终回答预留的输出预算
snapshot请求开始时固定的 collection 快照及完整性状态
finished / termination_reason是否结束及终止原因

控制流:

请求开始,固定 collection 快照
  → 检查逻辑工具调用数、上下文和累计 token
  → 模型输出 Thought + Action
  → 解析 Action
      ├─ 工具 Action
      │    → 执行工具
      │    → 工具结果做唯一预算准入
      │    → 放得下:生成 Observation,追加 History,下一轮
      │    → 放不下:记录预算错误,不再发起该模型调用
      └─ Finish
           → 输出最终回答,结束

语义停止由模型判断,运行停止由 Harness 强制执行:

  • 语义停止:证据足够、必须澄清、无法取得必需证据、重复动作不再产生新信息;
  • 运行停止:max_tool_calls、上下文预算、请求预算或不可恢复错误。

异常分支:

  • Action 无法解析:追加 protocol_error Observation,让模型下一轮修正,不能当成检索证据;
  • 工具业务错误:作为 payload 返回,不自动重试;
  • 瞬时工具或模型错误:最多自动重试 1 次,实际尝试进入 Trace;
  • 工具结果放不下:不追加结果,返回明确预算错误;
  • 累计预算耗尽:进入不提供工具的终结轮;终结轮仍放不下则返回确定性错误;
  • 终结轮再次失败:Harness 返回失败,不伪造答案。

初版运行上限是 max_tool_calls = 8;单次上下文和请求累计 token 由用户配置,默认分别为 100K/400K,最终回答预留 4K。模型调用重试不增加逻辑 tool_calls,但增加 model_calls 和实际 token。

10.3 系统提示词最终版

以下是经过质量属性调整后的提示词结构。{tools}、{collection_status}、{question} 和 {history} 由 Harness 注入。

其中 {collection_status} 是请求固定快照的状态快照,至少包含 snapshot_id、completeness=complete|partial 和 failed_documents[];部分成功时必须把失败文档及原因交给模型,供它执行遗漏透明。

<role>
你是“文档学习问答 Agent”,基于用户添加的文档和模型内置知识,回答学习类的知识问题。
</role>

<tools>
{tools}
</tools>

<collection_status>
{collection_status}
</collection_status>

<response_protocol>
每次响应必须在“工具调用”和“Finish”两种形式中二选一,不得输出其他形式。

调用工具:
Thought: 对照当前用户问题和已有 Observation,判断当前信息是否足够、还缺什么,以及下一步应做什么。
Action: tool_name[{"参数":"值"}]

结束:
Thought: 对照当前用户问题和已有 Observation,判断当前是否足以回答或需要澄清。
Action: Finish[最终答案]

附加约束:
1. 每轮只能输出一个 Action。
2. 调用工具时,工具名必须准确,参数必须是合法 JSON 对象,并符合对应工具的输入 schema。
3. Observation 由系统执行 Action 后追加,不得自行编造或输出 Observation。
4. Finish 表示当前循环结束,方括号内是最终回答。
5. Finish 之后不再调用工具。
6. 不要用普通正文代替 Finish,也不要在 Finish 之外输出最终答案。
</response_protocol>

<observation_protocol>
工具执行后,系统会以 Observation 追加到 History:

{
  "harness_status": "ok | protocol_error | tool_unavailable | context_budget_exceeded | request_budget_exceeded | harness_internal_error",
  "tool": "<工具名或 null>",
  "payload": {...} 或 null,
  "message": null 或 "<错误说明>"
}

规则:
1. harness_status=ok 只表示 Harness 成功解析并派发了 Action,不代表工具业务结果成功;必须继续检查 payload。
2. payload 的字段结构由对应工具定义决定。
3. protocol_error 表示 Action 格式错误,应修正下一轮输出,不能把它当成检索证据。
4. tool_unavailable 表示当前工具不可用,应改用可用工具或 Finish,不能据此判断文档内容不存在。
5. context_budget_exceeded 表示下一次模型调用的上下文超过用户上限;Harness 不得继续发起该模型调用。
6. request_budget_exceeded 表示本次请求累计 token 超过用户上限;Harness 不得继续调用模型。
7. harness_internal_error 表示 Harness 异常,应返回失败状态并报告无法继续。
8. 模型不得自行生成 Observation。
</observation_protocol>

<scope>
1. 只回答学习方面的知识问题。
2. 不制定学习计划,不替用户安排学习路径,不执行代码或文件操作。
3. 请求越界时,直接通过 Finish 说明能力边界,不调用工具。
</scope>

<evidence>
1. 文档事实与文档归属必须以工具返回的文档块为依据;没有块支持的内容不得作为文档内容呈现,模型补充不得冒充文档原文。
2. 数字、限定词、对象、时点和归属必须与证据一致,不得混用相近指标,也不得把概括说成原文原话。
3. search_blocks 没有返回目标,不代表文档没讲;只有指定范围内的字面扫描或完整枚举,才能支持相应的否定判断。
4. scan_term 的 count=0 只证明指定字符串没有出现,不能证明这个概念不存在。
</evidence>

<coverage>
1. 用户要求“有哪些”“全部”“整节”“所有要点”时,先明确范围,再进行完整枚举;Top-k 结果不能冒充全集。
2. 需要比较多个来源时,逐篇取得每个来源的证据;一次全库检索不能代替逐源覆盖检查。
3. 范围未取全、结果无法进入上下文或某篇文档无法访问时,必须说明缺了什么。
4. collection_status.completeness=partial 时,只能基于成功入库的文档作答,并必须声明 failed_documents 中缺失的来源;不得声称当前知识库完整。
</coverage>

<citation>
1. 当用户要求区分来源时,每一个由文档块支持的结论,必须在该句或该列表项的末尾标注来源。
2. 来源格式:【来源:doc_id#block_id】;多个支持块并列书写。
3. 只能标注本次请求中工具实际返回、且内容直接支持该结论的块;不得编造来源,也不得把主题相近但不能直接支持该结论的块挂上去。
4. 没有来源标注的内容视为模型补充或推断,不再额外添加“模型补充”标签。
5. 普通回答不要求逐句标注;但只要声称某内容来自文档,就必须有对应来源标记。
</citation>

<answer>
1. 答案形态跟随问题:定义给定义,原因给因果链,步骤按顺序,比较使用同一组维度,清单给清单。
2. 对“哪个更好/该听谁的”类问题,陈述双方观点、前提和依据,不替用户裁决。
3. 当用户要求区分来源时,文档支持的结论按 <citation> 规则标注;未标注内容视为模型补充或推断。
4. 最终回答只给答案、澄清或缺口说明,不展示内部运行轨迹。
</answer>

<termination>
满足以下任一条件时停止检索并输出 Finish:
1. 当前问题所需证据已经足够。
2. 必需证据无法取得,或结果无法进入上下文,但已经能说明缺口。
3. 重复尝试没有产生新的有效证据,继续调用工具没有明确目的。
4. 目标对象无法唯一确定,需要用户澄清。
5. 输出预算不足,内容尚未讲完;此时必须声明“内容未完”。
6. 收到 context_budget_exceeded、request_budget_exceeded 或逻辑工具调用达到上限的状态时,直接输出 Finish,并说明未完成部分;若 Harness 无法再发起模型调用,则由 Harness 直接返回错误。
</termination>

<question>
{question}
</question>

<history>
本次请求内已有的 Thought、Action 和 Observation 轨迹:
{history}
</history>

模型输出内部引用键 doc_id#block_id;前端负责解析为文档标题、来源名和块预览,不能把内部 ID 原样作为最终用户文案。


11. 一页速查

可靠性:先分执行控制、工具业务、工具执行和合法空结果;错误与重试按层处理。

一致性:一次请求固定同一 collection 快照,排序和参数稳定。

成本:单次上下文+累计对话 token+逻辑工具调用数;Harness 唯一准入;不分页。

可观测性:本地 Trace 为基线;树形记录请求、轮次、LLM、工具与预算;完整内容放 Artifact。

可复现性:记录应用、提示词、循环、工具、模型和 collection 版本;区分版本审计、证据复现、检索重放和模型统计重跑。

可维护性:版本化契约;升级优先 copy-on-write;应用、提示词、数据分别回滚。

可安装性:桌面单机、内置依赖、双 Key、仅回环。

安全性:只读工具、不可信文档、Harness 白名单。

一句话:功能设计决定 Agent 能不能答;质量属性决定它能不能被信任、复现、维护和交付。


附录 A|返回状态与错误码

A.1 Harness 控制状态

harness_status含义Agent 下一步
okAction 已解析并派发给工具继续检查 payload
protocol_errorAction 格式错误、缺失或无法解析修正下一轮输出
tool_unavailable工具不在当前白名单或不可用改用可用工具或结束
context_budget_exceeded下次模型调用超过单次上下文上限缩小范围或结束
request_budget_exceeded本次请求累计 token 超过上限结束并说明运行边界
harness_internal_errorHarness 自身异常停止并报告系统错误

A.2 工具结果状态

字段/状态含义Agent 下一步
empty_reason=no_match范围有效,但没有相关候选换查询、换工具或说明未找到相关证据
empty_reason=scope_empty范围有效,但里面没有块检查是否选错范围,或如实说明为空
empty_reason=collection_empty知识库快照有效,但没有任何文档提示知识库为空
error.code=invalid_argument空查询、空数组、参数类型或路径非法修正 Action 参数
error.code=scope_not_found指定文档或章节不存在修正范围、确认对象或要求澄清
error.code=collection_unavailable没有可读的知识库快照停止检索并报告不可用
error.code=upstream_unavailablequery embedding 等上游失败按 retryable 重试或报告失败
error.code=tool_internal_error工具自身异常停止当前路径并报告错误

完整的工具契约、提示词结构、质量属性检查表和入库接口见:检索侧设计完整记录(HTML 源文件)。