docs(mvp): add harness design and progressive guides

This commit is contained in:
zhuyongxin
2026-07-29 19:04:44 +08:00
parent 584639fa2a
commit 3a7eee8af4
15 changed files with 3550 additions and 0 deletions
@@ -0,0 +1,302 @@
# 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 主动无结论<br/>或 Harness 受控停止"] --> P["ProgressSnapshot"]
P --> Q{"存在已验真 observed facts?"}
Q -->|"是"| I["INSUFFICIENT_EVIDENCE<br/>展示已检查内容和下一步"]
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 才能完整回答:模型看到了什么、为什么继续或停止、最终哪些内容可以发布。