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 一个空数组可能来自五种事实
功能设计里,工具返回一个列表似乎就够了。但同一段 [] 可能表示:
- 精确扫描确实没有找到这个字符串;
- 语义检索没有召回到相关候选;
- 指定章节根本不存在;
- query embedding 服务暂时不可用;
- Action 格式错误,Harness 根本没有执行工具。
它们对 Agent 的下一步完全不同。都返回空列表,就等于把“材料没有”“参数错了”“系统坏了”混成同一个事实。
因此结果必须按执行位置分层:
模型输出 Action
→ Harness 解析、权限检查
→ 工具执行业务动作
→ 上游依赖完成请求
| 层 | 返回位置 | 典型情况 | 是否错误 |
|---|---|---|---|
| 执行控制层 | harness_status | Action 无法解析、工具不在白名单、预算边界、Harness 异常 | 是 |
| 工具业务层 | payload.error | 参数无效、文档/章节不存在 | 是 |
| 工具执行层 | payload.error | collection 不可用、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 提供隔离边界”。答案如果错了,可能至少有四种原因:
- 模型没有调用相关工具,直接凭内置知识生成;
- 调用了检索,但没有召回目标块;
- 召回到了块,结果被预算准入拒绝,没有进入下一轮;
- 块进入了上下文,但模型生成时漏写、错写或忽略。
对应要修的层完全不同:提示词、检索、预算或生成约束。只看到 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 给最终回答;
- 系统只负责默认配置校验和执行,不替用户决定愿意花多少钱。
执行流程是:
- 工具按范围取全,并返回结果 token 估算;
- Harness 在结果进入 History 前,做唯一一次预算准入;
- 放得下,完整进入;
- 放不下,明确返回预算错误,并建议提高预算或缩小范围;
- 不允许只放一半,却让模型误以为已经覆盖全部。
这个方案承认:完整性与成本不能同时无限满足。 产品能做的是让用户知道边界在哪里,并在边界内保证结果完整。
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_blocks | query 必填;doc_ids 可选,省略表示全库,显式空数组非法;指定文档不存在时整体报错 | 返回 Top-5 块的引用、文档标题、来源、章节和全文,另附空结果原因与 token 估算 | 固定混合检索,模型不能切换检索模式;它是候选召回,不是全集;分数只进 Trace;未命中不能证明文档没讲;query embedding 失败不静默降级成关键词 |
list_blocks | 单篇 doc_id 必填;section_paths 可选,省略或空数组表示整篇;跨篇必须分别调用;任一路径不存在时整体报错 | 返回该范围全部块的引用、标题、来源、章节和全文,total 必须等于实际块数,另附结果 token 估算;按原文 block_index 排序 | 一个章节 path 包含该节及全部后代;多个 path 取并集并按原文顺序去重;工具不自行截断;路径缺失不能返回部分结果冒充完整 |
scan_term | term 去首尾空白后必须非空;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_errorObservation,让模型下一轮修正,不能当成检索证据; - 工具业务错误:作为 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 下一步 |
|---|---|---|
ok | Action 已解析并派发给工具 | 继续检查 payload |
protocol_error | Action 格式错误、缺失或无法解析 | 修正下一轮输出 |
tool_unavailable | 工具不在当前白名单或不可用 | 改用可用工具或结束 |
context_budget_exceeded | 下次模型调用超过单次上下文上限 | 缩小范围或结束 |
request_budget_exceeded | 本次请求累计 token 超过上限 | 结束并说明运行边界 |
harness_internal_error | Harness 自身异常 | 停止并报告系统错误 |
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_unavailable | query embedding 等上游失败 | 按 retryable 重试或报告失败 |
error.code=tool_internal_error | 工具自身异常 | 停止当前路径并报告错误 |
完整的工具契约、提示词结构、质量属性检查表和入库接口见:检索侧设计完整记录(HTML 源文件)。