Files
SuperBizAgent-java/mvp/engineering/harness/Harness-Tool双视图-从原始结果到可验证证据.md

281 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Harness Tool 双视图:从原始结果到可验证证据
**更新日期**:2026-07-29
**主题**:ToolBoundary、Canonical Invocation、Harness Control View 与 Agent Observation
**术语与状态**:[CONTEXT.md](CONTEXT.md) · [Harness生命周期与状态.md](Harness生命周期与状态.md)
**上位设计**:[Harness设计-非确定性Agent的确定性控制边界.md](Harness设计-非确定性Agent的确定性控制边界.md)
## 1. 要解决的不是 Tool 调用,而是 Tool 结果的所有权
Agent 调用 Tool 后,最直接的实现是把 backend 返回的 JSON 原样放进模型上下文。这在 Demo 中可以工作,但进入可验证的诊断系统后会出现一个根本矛盾:同一份结果需要同时服务推理、控制、验真和审计,而这些消费者需要的数据范围完全不同。
以日志查询为例:
- Agent 需要少量匹配事件、模式、实际时间范围和是否截断;
- Harness 需要返回数量、规范化 scope、重复身份和客观证据状态;
- EvidenceGuard 需要证明这份结果确实来自当前 Run 的某次 READY 调用;
- 审计需要 Tool 名、调用 ID、状态、耗时和字节数;
- 安全边界又要求密码、Token、主机、IP、SQL 字面量和无限日志正文不能进入模型或长期审计。
如果只保留一份 JSON,只能在两个错误方向中选择:要么信息过多导致泄露和上下文膨胀,要么信息过少导致后续无法验真。
因此这项设计的核心不是“增加一个 Redis Store”,而是重新定义 Tool 数据所有权:
> backend raw 属于 Harness;模型只能获得为推理目的生成的有界观察;长期审计只保留允许运营保存的元数据。
## 2. 三个消费者,三种数据责任
虽然实现中常称为“Tool 双视图”,完整的数据分层实际上包含三种用途:
| 数据形态 | 消费者 | 解决的问题 | 生命周期 |
|---|---|---|---|
| Canonical Invocation | ToolBoundary、EvidenceGuard、ProgressProjector | 这次 Tool 真实执行了什么、属于哪个 Run、能否引用 | Redis 短 TTL |
| Harness Control View / Agent Observation | Harness / Diagnosis Agent | 是否重复、是否为空、模型下一步需要看到什么 | 当前 Run / 模型上下文 |
| Durable Audit | Trace、运营排障 | 何时调用、状态、耗时、大小、关联 Step | MySQL 长期 metadata |
所谓“双视图”,特指 canonical 标准结果被进一步分成:
1. Harness 使用的 Control View;
2. 模型使用的 Agent Observation。
Durable Audit 是第三个持久化面,但它不是 Tool 内容视图,不参与推理或证据验真。
```mermaid
flowchart LR
A["Typed Tool Request"] --> B["ToolBoundary"]
B --> C["Backend Raw Response"]
C --> D["Tool-specific Projector"]
D --> E["Canonical agent_result"]
B --> F["Redis Canonical Invocation<br/>request + raw + agent_result"]
E --> F
E --> G["Harness Control View<br/>count / status / scope"]
E --> H["Agent Observation<br/>白名单、有界、脱敏"]
B --> I["Durable Audit<br/>identity / status / latency / bytes"]
G --> J["重复检测、NO_GAIN、停止"]
H --> K["Diagnosis Agent Context"]
F --> L["EvidenceGuard / ProgressSnapshot"]
```
## 3. 为什么不能让 Agent Observation 充当真相源
Agent Observation 是为了控制上下文而生成的投影,它可能:
- 只保留前 N 条结果;
- 截断单条文本;
- 对敏感列和日志字段做脱敏;
- 把大量事件聚合成模式;
- 只暴露粗粒度相关度,不暴露检索轨迹和原始分数。
这意味着它适合帮助模型推理,却不适合作为“后台真实返回”的完整证明。如果 EvidenceGuard 反过来验证 Agent 自己收到的 observation,就相当于用被审查对象提供的摘要证明其自身真实性。
Canonical Invocation 解决的正是这个独立性问题。它以 exact `runId + tool_call_id` 建立记录,并保存:
```text
tool_call_id
run_id
tool_name
request
raw_response
agent_result
invocation status
evidence status
error code
started_at / completed_at
```
EvidenceGuard 不信任 Draft 中的引用,也不从模型历史反推 Tool 结果,而是重新按当前 Run 构造 key,读取 canonical record 并验证状态和投影内容。
## 4. ToolBoundary 为什么必须是统一入口
RAG、日志和 MySQL 的业务执行方式不同,但以下控制规则完全相同:
- Run 必须仍然 active;
- envelope 的 runId 必须等于当前 Run;
- `tool_call_id` 必须合法且不能重复;
- Tool 必须已授权并声明只读;
- 调用前必须预占 Tool 预算和 request bytes;
- raw response 和 agent result 必须分别检查容量;
- canonical 状态只能按合法路径迁移;
- 对 Agent 只返回稳定错误码;
- durable audit 失败不能改变已经得到的 Tool 结果。
如果把这些逻辑复制到三个 Adapter,任何新增 Tool 都可能漏掉其中一项。因此统一由 `ToolBoundary` 编排一次调用,具体 Adapter 只负责 typed request、backend 和 projector 的连接。
```mermaid
sequenceDiagram
participant I as Tool Interceptor
participant B as ToolBoundary
participant C as Harness Core
participant S as Canonical Store
participant T as Backend
participant P as Projector
participant A as Audit Sink
I->>B: RunContext + ToolCallRequestEnvelope
B->>B: run / id / authorization / readonly / JSON preflight
B->>C: reserve Tool call + request bytes
B->>S: begin PROJECTING
B->>T: execute typed request
T-->>B: raw response
B->>C: reserve raw bytes
B->>P: project raw response
P-->>B: bounded agent_result + evidence_status
B->>C: reserve projection bytes
B->>S: mark READY
B-->>I: ToolBoundaryResult
B-->>A: best-effort metadata audit
```
错误发生时,已经创建的 canonical record 会尽力迁移到 ERROR;如果连 Store 都不可用,则向上只返回 `STORE_ERROR` 等稳定码,不把 Redis 或 backend 异常正文交给模型。
## 5. 两套状态为什么不能合并
Tool 调用同时有两个正交维度:
| 维度 | 状态 | 回答的问题 |
|---|---|---|
| Invocation lifecycle | `PROJECTING / READY / ERROR` | 这次调用是否完成并形成了可引用记录 |
| Evidence semantics | `EVIDENCE_FOUND / NO_EVIDENCE / ERROR` | 客观结果中是否存在候选证据 |
例如日志查询成功返回 0 条:
```text
InvocationStatus = READY
EvidenceStatus = NO_EVIDENCE
```
它不是 `ERROR`。这条负向观察可以支持“在指定时间、服务和查询条件下没有匹配日志”,但不能支持“故障不存在”。
合法组合被类型约束为:
| InvocationStatus | EvidenceStatus | 是否允许引用 |
|---|---|---:|
| `PROJECTING` | `null` | 否 |
| `READY` | `EVIDENCE_FOUND` | 是 |
| `READY` | `NO_EVIDENCE` | 是,但只能作为限定范围负向观察 |
| `ERROR` | `ERROR` | 否 |
把两者合成一个 `SUCCESS / FAILED` 会丢失最重要的信息:技术成功但业务范围内没有证据。
## 6. 三类 Tool 如何投影
### 6.1 RAG
`RagResultProjector` 从 backend 结果中提取有界 evidence block,稳定文档身份,限制 excerpt 数量和长度,并兼容 `relevanceLevel / relevance_level` 后统一为 `relevance_level`。
这里有一个刻意保留的区分:
```text
evidence 非空 -> EVIDENCE_FOUND
relevance_level=REFERENCE -> 相关度一般
```
`EVIDENCE_FOUND` 只说明存在候选内容,`REFERENCE` 也不自动表示 `NO_GAIN`。内容是否推进当前诊断假设,需要模型结合上下文判断。
### 6.2 Logs
`QueryLogsResultProjector` 不只是截取前几条日志,它还会:
- 脱敏 password、token、secret、API key;
- 脱敏 pod、host、PID、IP 和 SQL literal;
- 去掉堆栈尾部噪声;
- 对数字做模式归一化并聚合重复事件;
- 对事件做跨范围采样,而不是只保留开头;
- 保存实际时间、topic、query scope 和截断标记。
如果 backend 执行成功且日志数组为空,投影结果是 READY + NO_EVIDENCE。backend 明确返回失败,才是 Tool 执行或投影错误。
### 6.3 MySQL
MySQL 在投影之前还有独立的只读安全链:`MysqlSqlValidator` 根据逻辑数据源、schema、table 和 column allowlist 生成 `MysqlQueryPlan`,`JdbcMysqlReadOnlyExecutor` 只执行该 Plan。
`MysqlResultProjector` 再负责:
- 限制最大行数、单元格长度和总 bytes;
- 保持 number、boolean 和 null 类型;
- 对 password、token、secret、credential 等敏感列强制脱敏;
- 结果缩减时标记 `truncated=true`。
Validator 负责“能不能执行”,Projector 负责“模型能看到什么”,两者不能合并。
## 7. Control View 与 Model Observation 的字段边界
| 字段 | Harness | Agent | 原因 |
|---|---:|---:|---|
| `tool_call_id` | 是 | 是 | 引用和上一轮评价都需要 |
| 实际 scope | 是 | 是 | Harness 去重;模型理解负向观察边界 |
| 有界 evidence/events/rows | 是 | 是 | 模型推理所需事实 |
| `evidence_status` | 是 | 是 | 区分候选证据、空结果和错误 |
| `relevance_level` | 是 | 是 | 给模型粗粒度检索语义 |
| `truncated` | 是 | 是 | 防止模型误以为结果完整 |
| `returned_count` | 是 | 否 | Harness 统计,不必消耗模型上下文 |
| normalized scope / duplicate identity | 是 | 否 | 内部控制实现 |
| 连续 NO_GAIN、阈值和剩余预算 | 是 | 否 | 防止模型围绕限制博弈 |
| raw response / 检索轨迹 / 原始分数 | 是 | 否 | 敏感且体积不可控 |
只有 Harness 必须改变模型行为时,才注入 `STOP_REQUIRED`、错误码或已检查范围等有限控制信息。
## 8. 存储决策:为什么是 Redis canonical + MySQL metadata
### 8.1 不把完整 raw 长期写 MySQL
完整 Tool 结果可能包含日志、SQL 查询结果和内部知识内容。长期保存会扩大泄露半径,也会让 JPA audit 成为第二个事实源。MySQL 只保存运营和对账需要的字段,可以长时间保留而不复制正文。
### 8.2 Canonical 读取不续期
Redis record 在创建时设置 TTL,读取或状态更新不恢复初始 TTL。原因是:如果一次历史查询就能续期,敏感 raw 可能因审计访问而永久存在。
状态更新保留当前剩余 TTL,代价是 read-TTL-write 存在小的并发窗口,但它比无界续期更符合数据治理目标。
### 8.3 raw 超限为什么不截断
raw 是内部事实。如果静默截断后仍标记 READY,系统无法区分“backend 只返回这些内容”和“Harness 丢掉了内容”。因此 raw 或完整 record 超限时调用进入 ERROR。
Agent projection 可以截断,因为它本来就是面向消费的摘要,但必须通过 `truncated=true` 明示不完整。
## 9. 真实问题如何改变设计
| 真实问题 | 暴露的错误假设 | 最终修正 |
|---|---|---|
| Tool raw 直接进入 Agent | backend 输出天然适合模型消费 | 增加 Tool-specific projector 和白名单 observation |
| Trace、raw、ContextPack、重复正文同时存在 | 多保存几份可以提高可观测性 | canonical、model view、metadata audit 明确分层 |
| Mock 日志 0 命中返回 `success=false` | 没数据等于调用失败 | 技术执行状态与 NO_EVIDENCE 分离 |
| RAG 的 `REFERENCE` 在投影中丢失 | 只要 evidence 非空就够了 | 保留 relevance_level,但不提升为根因证据 |
| 生产 ObjectMapper 未注册 Java Time 模块,Redis 全部 STORE_ERROR | 单元测试序列化环境等同生产 | 使用生产装配验证 canonical record,并增加 live E2E |
| backend 日志打印 raw/rewritten query | 排障信息可以直接进入普通日志 | 普通日志和 durable audit 只保留安全摘要 |
## 10. 代价与边界
这项设计不是免费的:
1. 每增加一种 Tool,都要同时定义 typed request/result、Adapter、Projector 和 EvidenceGuard 读取规则。
2. Redis 在 TTL 内成为证据校验依赖;不可用时系统应 fail closed,而不是相信 Draft。
3. Agent 看到的是有损投影,Projector 设计不当可能丢掉模型真正需要的诊断信号。
4. durable audit 不能替代 canonical replay;TTL 到期后只能解释调用元数据,无法恢复完整正文。
但这些成本换来了明确的责任:Backend 决定原始事实,Projector 决定模型可见范围,Canonical Store 决定短期验真事实,Audit 决定长期允许保存什么。
## 11. 如何验证
| 验证内容 | 代表性测试或证据 |
|---|---|
| Run mismatch、未授权、非只读、重复 ID、预算和超限 | `ToolBoundaryTest` |
| PROJECTING / READY / ERROR 和 TTL 行为 | `CanonicalInvocationStoreTest` |
| RAG evidence identity、relevance 和 bytes | `RagResultProjectorTest` |
| 日志脱敏、模式聚合、空结果和采样 | `QueryLogsResultProjectorTest` |
| MySQL 行列/敏感字段/容量边界 | `MysqlResultProjectorTest` |
| typed Tool contract 不漂移 | 三类 `*ToolContractTest` |
| 生产 Redis serializer 与 ObjectMapper | single-react cleanup live E2E |
单元测试只能证明数据变换和状态机;生产 Redis serializer、实际 Tool Calling ID 和 backend 返回格式仍必须通过 live E2E 验证。
## 12. 与后续机制的关系
Tool 双视图只解决“什么是可验证的 Tool 事实、模型允许看到什么”,并不解决:
- 这些事实是否支持最终结论:见[Harness证据安全链-从引用真实到结论可发布.md](Harness证据安全链-从引用真实到结论可发布.md);
- Agent 是否应该继续查询:见[Harness信息增益停止-让无证据诊断正常收敛.md](Harness信息增益停止-让无证据诊断正常收敛.md)。