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

14 KiB
Raw Permalink Blame History

Harness Tool 双视图:从原始结果到可验证证据

更新日期:2026-07-29 主题:ToolBoundary、Canonical Invocation、Harness Control View 与 Agent Observation 术语与状态:CONTEXT.md · Harness生命周期与状态.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 内容视图,不参与推理或证据验真。

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 建立记录,并保存:

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 的连接。

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 条:

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。

这里有一个刻意保留的区分:

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 事实、模型允许看到什么”,并不解决: