# Harness 信息增益停止:让无证据诊断正常收敛 **更新日期**:2026-07-29 **主题**:Information Gain、Progress Tracker、STOP_REQUIRED 与过程型 Fallback **术语与状态**:[CONTEXT.md](CONTEXT.md) · [Harness生命周期与状态.md](Harness生命周期与状态.md) **上位设计**:[Harness设计-非确定性Agent的确定性控制边界.md](Harness设计-非确定性Agent的确定性控制边界.md) ## 1. 预算能限制损失,不能判断何时完成 在知识库只有通用说明、日志持续为空或查询条件不足时,ReAct Agent 容易出现一种看似合理的行为:不断修改关键词、时间范围或查询表达,再调用一次 Tool。 每一次调用单独看都可能合法,但整个 Run 没有获得新信息。旧系统只能等到模型次数、Tool 次数、Token 或 deadline 耗尽,再以 `BUDGET_EXHAUSTED` 或 `INTERNAL_FAILURE` 结束。 这暴露了两个被混淆的问题: ```text 资源预算:这次 Run 最多允许消耗多少? 业务收敛:继续查询是否仍可能推进当前诊断? ``` 预算是最后一道资源保护,不能承担正常停止策略。否则“当前证据不足”会被错误表达成“系统执行失败”。 信息增益停止契约的目标是:在硬预算之前识别连续无进展,使 Agent 停止调用 Tool,并把已经完成的有限检查发布为诚实、可验证的业务结果。 ## 2. 先划清三方判断权 设计停止机制时最危险的做法,是让某一层承担它无法可靠完成的判断。 | 参与者 | 能可靠判断什么 | 不能判断什么 | |---|---|---| | Tool / Projector | 是否执行成功、结果是否为空、返回数量、实际 scope、是否截断 | 内容是否支持当前诊断假设 | | Diagnosis Agent | 非空内容是否确认、排除或缩小当前假设 | 是否还能绕过预算、重复和饱和门禁 | | Harness | scope 是否重复、连续 NO_GAIN、协议是否合规、是否允许继续 | 业务根因是什么 | | Release | 已有进展可以发布成哪种安全结果 | 是否应该重新调用 Tool | 最终原则是: > Tool 提供客观结果,模型判断语义价值,Harness 拥有最终停止权。 模型可以选择主动结束,但不能通过继续发 Tool Call 绕过 Harness 已经确定的饱和或预算状态。 ## 3. 为什么信息增益只有两个值 当前契约只保留: ```text information_gain = GAINED | NO_GAIN ``` 没有 `HIGH / MEDIUM / LOW`,也没有置信分数。停止控制只需要知道结果是否推进了当前诊断;引入更多等级会带来阈值解释、跨 Tool 标定和模型输出不稳定,却不一定改变最终动作。 判定标准是: | 结果 | Information Gain | 生产者 | |---|---|---| | Tool 执行失败 | 不产生 | 进入技术失败流程 | | `evidence_status=NO_EVIDENCE` | `NO_GAIN` | Harness | | 相同 `tool_name + normalized_scope` | `NO_GAIN` | Harness,并拒绝重复执行 | | 成功非空,确认/排除/缩小假设 | `GAINED` | Diagnosis Agent | | 成功非空,但只是通用知识、重复内容或无关事实 | `NO_GAIN` | Diagnosis Agent | RAG 的 `REFERENCE` 不能自动映射为 `NO_GAIN`。它只表示检索相关度一般;一段一般相关的资料可能仍排除一个假设,也可能完全无用,需要模型结合诊断上下文判断。 ## 4. 为什么模型在下一次 Tool Call 中评价上一轮 模型只有在读到 Tool Observation 后,才能判断它是否有语义增益。但如果要求模型单独输出一条 progress 消息,就必须增加新的协议轮次或 Progress Judge。 当前设计利用模型已经要做的下一步行为: - 输出 DiagnosisDraft,表示主动结束; - 发起下一次 Tool Call,表示希望继续。 当模型选择继续时,下一次 Tool Call Envelope 必须携带对上一轮的评价: ```json { "previous_observation": { "tool_call_id": "call-123", "information_gain": "NO_GAIN" }, "input": { "query": "新的业务查询参数" } } ``` Interceptor 在调用业务 Tool 之前完成三件事: 1. 校验 `previous_observation.tool_call_id` 是否正好是当前 pending 调用; 2. 应用 `GAINED / NO_GAIN`,更新连续计数和收集状态; 3. 剥离 `previous_observation`,只把 `input` 传给原业务 Tool。 这是 Agent-facing Tool Schema 的协议变化,但 RAG、日志和 MySQL 的业务 request 并没有被控制字段污染。 ```mermaid sequenceDiagram participant M as Diagnosis Agent participant I as Tool Interceptor participant P as Progress Tracker participant T as Business Tool M->>I: Tool Call #1 + input I->>P: no pending observation I->>T: business input T-->>I: successful non-empty observation I->>P: mark call #1 pending evaluation I-->>M: bounded observation M->>I: Tool Call #2 + previous_observation(#1, NO_GAIN) I->>P: validate ID and apply NO_GAIN alt 仍为 COLLECTING I->>T: strip control fields, execute input #2 else 达到 SATURATED I-->>M: STOP_REQUIRED end ``` 如果模型读完 observation 后直接输出 Draft,就不需要额外评价最后一轮。因为它已经通过实际行为表达“停止”,Harness 也不需要为收集一个统计字段强迫模型再调用 Tool。 ## 5. ProgressTracker 保存什么,不保存什么 `DiagnosisProgressTracker` 是 `RunContext` 中的线程安全状态句柄,保存: - 已完成的 `tool_name + normalized_scope`; - 已完成 Tool Call identity; - 当前等待模型评价的 `pendingToolCallId`; - 连续 `NO_GAIN` 次数; - 连续 progress protocol violation 次数; - `COLLECTING / SATURATED` 状态; - stop reason; - STOP_REQUIRED 是否已经交付。 它不保存 request、raw response、agent result 或模型 thought。完整 Tool 事实仍属于 Canonical Store。Tracker 只保存作出停止决策需要的最小 identity 和计数,避免出现第二份 Tool 真相。 结束时,`DiagnosisProgressProjector` 根据 completed identity 回读 canonical READY records,投影成有界 `ProgressSnapshot`。无法验证、不可读取或格式非法的记录不会被发布,只会形成安全 limitation。 ## 6. 重复检测为什么只做参数级 Harness 使用 `ToolScopeNormalizer` 将业务输入转换成稳定 scope,再比较: ```text tool_name + normalized_scope ``` 这可以识别字段顺序、格式差异下的完全相同查询,并在调用 backend 前拒绝重复执行。 首版没有做自然语言语义去重,例如以下两条 query 可能语义相同,但不会被代码证明为同一 scope: ```text “查询支付超时日志” “查找支付请求 timeout 记录” ``` 原因是语义去重需要 embedding、模型判断或跨 Tool 指纹,会引入新的不确定性和误杀风险。当前边界选择了可以确定性证明的参数重复;语义近似由模型的 `NO_GAIN` 义务约束。 ## 7. 收集状态机 连续无增益阈值由配置控制,当前默认值为 2。`GAINED` 会清零连续计数,避免一次早期空查使后续有效取证被过早停止。 ```mermaid stateDiagram-v2 [*] --> COLLECTING COLLECTING --> COLLECTING: GAINED / consecutiveNoGain=0 COLLECTING --> COLLECTING: NO_GAIN / 未达阈值 COLLECTING --> SATURATED: NO_GAIN / 达到阈值 SATURATED --> STOP_CHANCE: 交付一次 STOP_REQUIRED STOP_CHANCE --> RELEASE: 模型输出 Draft STOP_CHANCE --> TERMINATED: 模型再次请求 Tool ``` 当某次 READY 结果是 NO_EVIDENCE 时,Harness 可以立即累计 `NO_GAIN`。当成功非空结果需要模型评价时,Tracker 设置 pending ID;上一轮未评价前,新的 Tool Call 不会被执行。 达到 `SATURATED` 后,Harness 给模型一次合法完成机会,而不是在 Tool response 中立刻抛出通用错误。STOP_REQUIRED 只交付一次;模型仍继续请求 Tool 时,`DiagnosisCollectionStoppedException` 将受控停止穿出框架 ReAct loop。 ## 8. 协议错误为什么不能计为 NO_GAIN 真实 E2E 曾出现 9 次 `INVALID_PROGRESS_PROTOCOL` Tool 请求拒绝和 13 轮 Diagnosis Agent 模型调用。模型没有正确回传上一轮评价,但这些拒绝也没有增加 NO_GAIN,最终持续空转。 一种看似简单的修复是把协议错误也计为 NO_GAIN。但这会混淆两个事实: - `NO_GAIN` 表示 Tool 结果没有推进诊断; - 协议错误表示模型没有遵守控制契约,Tool 根本没有执行。 因此引入独立的协议错误计数和 `PROGRESS_PROTOCOL_VIOLATED`: 1. 第一次错误返回可修正 observation,包含 violation type、缺失字段、期望的上一轮 ID 和允许值; 2. 连续错误达到配置阈值后进入 SATURATED; 3. 交付一次 `STOP_REQUIRED / PROGRESS_PROTOCOL_VIOLATED`; 4. 再次请求 Tool 时受控停止。 协议错误 Trace 使用 `TOOL_REQUEST_REJECTED`,不能记录成 `TOOL_INVOCATION`,因为 backend 从未被调用,Tool 预算和实际执行数也不应被污染。 ## 9. 三种停止原因必须分开 | Stop Reason | 含义 | 是否等于技术失败 | |---|---|---:| | `INFORMATION_SATURATED` | 连续结果没有推进诊断 | 否 | | `BUDGET_LIMIT_REACHED` | 达到模型、Tool、Token 或 bytes 预算边界 | 不一定;有安全进展时可发布过程 Fallback | | `PROGRESS_PROTOCOL_VIOLATED` | 模型连续违反 progress Envelope | 协议失败;有安全进展时仍可保留过程价值 | 信息饱和不能伪装成预算耗尽,否则无法判断阈值是否合理;预算耗尽也不能伪装成信息饱和,因为可能是在持续获得有效证据时资源不足。 这些 stop reason 是 Harness 内部控制语义,不直接作为第二套公开生命周期。最终仍由 Release 映射为 `SUCCESS / FALLBACK / FAILED / CANCELLED`。 ## 10. 停止以后如何形成用户结果 停止本身不是答案。系统需要把已经完成的检查转换成可发布内容,又不能依赖预算耗尽后额外调用模型。 `DiagnosisProgressProjector` 从 canonical READY results 生成: - verified sources; - observed facts,包括限定范围的空结果; - 实际查询 scope; - 投影失败、截断或不可读取形成的 limitations; - stop reason。 Release 根据现有进展决定: ```mermaid flowchart TD S["Agent 主动无结论
或 Harness 受控停止"] --> P["ProgressSnapshot"] P --> Q{"存在已验真 observed facts?"} Q -->|"是"| I["INSUFFICIENT_EVIDENCE
展示已检查内容和下一步"] Q -->|"否"| M{"Draft 声明 missing_info?"} M -->|"是"| C["MISSING_REQUIRED_CONTEXT"] M -->|"否"| F["FAILED / fail closed"] ``` `conclusion=null` 是合法 Draft。它允许 Agent 在缺少企业、时间、服务或错误信息时零次调用 Tool,直接报告 `missing_info`,避免为了表现“已经排查”而执行无明确范围的查询。 ## 11. 为什么没有引入更多控制字段 ### 11.1 不使用 `new_count` “新记录数量”需要跨 RAG 文档、日志事件和数据库行建立稳定指纹,而且新数据不等于对当前假设有用。它增加了复杂度,却不能替代语义增益判断。 ### 11.2 不使用 `next_action` 模型发起 Tool Call已经表示继续,输出 Draft 已经表示结束。再要求 `CONTINUE / STOP` 只会形成一套可能与实际行为冲突的声明状态。 ### 11.3 不增加 Progress Judge 独立 Judge 会为每轮 Tool 结果增加模型调用、延迟和失败面。空结果和完全重复 scope 本可由代码判断;其他内容由正在做诊断的 Agent 评价即可。 ### 11.4 不向模型公开剩余预算和阈值 模型只需要知道是否必须停止,不需要围绕“还剩几次”规划消耗。阈值、计数和预算属于 Harness Control View,只有 STOP_REQUIRED 等必要指令进入模型上下文。 ## 12. Prompt 与硬门禁如何分工 Prompt 仍然需要告诉模型: - 不必须得出根因; - `conclusion=null` 是合法完成; - 缺少必要上下文时可以零 Tool 结束; - 正确但不能推进假设的内容也是 NO_GAIN; - 不要通过改写相似关键词重复查询; - 收到 STOP_REQUIRED 后必须停止。 但 Prompt 只是帮助模型做出正确选择,不构成系统保证。重复 scope、pending evaluation、饱和状态、预算和一次性 STOP_REQUIRED 都由代码门禁执行。 ## 13. 真实问题如何改变设计 | 真实现象 | 被证伪的假设 | 设计修正 | |---|---|---| | 空日志、通用知识仍不断改写查询 | 模型会自然意识到没有进展 | 明确信息增益义务和 Harness 饱和状态 | | 最终以 BUDGET_EXHAUSTED 结束 | 硬预算可以充当正常停止 | 预算与信息饱和分离 | | `REFERENCE` 非空结果持续触发查询 | 非空候选就是有价值证据 | 检索相关度与诊断增益分离 | | 相同 scope 被反复执行 | Prompt 足以禁止重复 | Harness 参数级去重并在 backend 前拒绝 | | 9 次协议拒绝仍消耗 13 轮模型 | 错误 observation 会让模型自修复 | 可修正反馈 + 独立协议阈值 + STOP_REQUIRED | | 非法 Draft 使已完成检查丢失 | 只有合法最终 Draft 才有用户价值 | 已验真 ProgressSnapshot 可形成过程型 Fallback | | 缺少企业/时间仍被迫调用 Tool | Tool 调用次数大于零才算诊断 | 允许零 Tool、missing context 合法结束 | ## 14. 代价与当前边界 1. 模型侧 Tool Schema 增加了 `previous_observation + input`,这是明确的 Agent-facing 协议变化。 2. 首版只能确定性识别参数相同的重复 scope,不能阻止所有自然语言近义改写。 3. 默认连续 NO_GAIN 阈值 2 是工程起点,需要依靠固定评测集校准;太小会过早停止,太大会增加空转。 4. 最后一轮非空 Tool 结果如果模型直接输出 Draft,Tracker 不强制收集其 information gain;这是减少无意义协议轮次的主动取舍。 5. ProgressSnapshot 依赖 canonical record 仍在 TTL 内且可解析,无法验真的进展不会被发布。 6. 受控预算停止只有在已有安全进展时才能转为 Fallback;没有可验证内容仍然 fail closed。 ## 15. 如何验证 | 需要证明 | 代表性测试或 E2E | |---|---| | NO_EVIDENCE 自动累计 NO_GAIN,GAINED 清零 | `DiagnosisProgressTrackerTest` | | 重复 scope 在 backend 前被拒绝 | `HarnessToolInterceptorTest`、`ToolScopeNormalizerTest` | | pending evaluation 的缺失、乱序和意外回传被拒绝 | Interceptor protocol focused cases | | 连续协议错误达到阈值并只交付一次 STOP_REQUIRED | Tracker + Interceptor tests | | 模型主动停止、饱和停止和预算停止都能进入 Release | `DiagnosisAgentUseCaseTest`、`DiagnosisReleaseUseCaseTest` | | 非法 Draft 只有在存在安全进展时降级 | `DiagnosisChatExecutorTest` | | Tool 拒绝与实际 Tool 执行分开审计 | exact-run Trace | | 未知 Query 不再以通用内部错误结束 | ISS-016 named SSE E2E | 其中一条 live E2E 曾准确暴露“协议拒绝不累计 NO_GAIN”的盲区。这说明停止机制不能只验证最终 SSE,还要核对模型轮次、Tool 实际执行数、Tool 拒绝数、Token 和 Timeline 序列。 ## 16. 与另外两项设计的关系 信息增益依赖 [Tool 双视图](Harness-Tool双视图-从原始结果到可验证证据.md)提供客观 evidence status、scope 和有界 observation;停止后的 ProgressSnapshot 和最终 Fallback 依赖[证据安全链](Harness证据安全链-从引用真实到结论可发布.md)确保只发布当前 Run 可验证的事实。 三者组合后,Harness 才能完整回答:模型看到了什么、为什么继续或停止、最终哪些内容可以发布。