Files
SuperBizAgent-java/mvp/engineering/harness/案例-从一次支付超时诊断看Harness如何控制Agent.md
T

14 KiB
Raw Blame History

从一次支付超时诊断看 Harness 如何控制 Agent

这篇文章不从组件清单开始,而是跟随一次真实请求,看 Agent 如何完成诊断,以及 Harness 在每个关键节点控制了什么。

先说最终结果:这次请求正常执行了两轮 Agent、调用了两个 Tool,两个 Tool 都返回了候选证据,但最终没有发布诊断结论,而是安全地返回了 Fallback。

这不是一次“什么都没做成”的失败。恰恰相反,它展示了 Harness 最重要的价值:即使 Agent 已经写出答案,只要证据无法完成验真,答案就不能越过发布出口。

第一次阅读只看第 1、2、8、9 和 13 节即可:先知道问题和主流程,再看为什么被挡下、用户最终收到什么。其余章节用于展开每一步的组件设计。

1. 这次诊断从什么问题开始

用户请求是:

支付接口最近出现超时。在给出结论前必须实际调用 lookup_knowledge 和 query_logs 各一次,日志范围使用 APPLICATION 并查询 payment-service error slow database;随后结束诊断,证据不足时明确说明缺口。

这是一个很典型的 AIOps 问题。用户看到的是“支付超时”,但可能原因很多:

  • 数据库连接池耗尽;
  • 下游服务响应过慢;
  • Redis 或网络超时;
  • JVM、线程池或 CPU 资源异常;
  • 只是历史知识中的相似案例,并非当前生产故障。

如果只有 Agent,它可以查询资料、阅读日志并写出一个听起来合理的解释。但系统还必须回答:查询是否属于本次请求、结果能否被引用、引用是否支持结论,以及证据不足时应该怎样结束。

2. 先看完整故事

flowchart LR
    Q["用户报告支付超时"] --> R["创建 Run<br/>建立身份、预算和取消"]
    R --> I["Router 判定为 DIAGNOSIS"]
    I --> A1["Agent 第 1 轮<br/>选择知识库和日志 Tool"]
    A1 --> T["ToolBoundary<br/>执行、投影并保存调用真相"]
    T --> A2["Agent 第 2 轮<br/>根据观察结果生成 Draft"]
    A2 --> E["EvidenceGuard<br/>验证引用真实性"]
    E -->|"本次未通过"| F["SafeFallback<br/>不发布未经验证的根因"]
    F --> U["SSE content + done FALLBACK"]

沿着这条线,可以把双方职责简单分开:

阶段 Agent 在做什么 Harness 在控制什么
请求开始 尚未参与 创建 Run,固定身份、deadline、预算和取消
意图路由 尚未诊断 限制 Router 输入、调用次数和重试
选择 Tool 提出查询计划 检查 Run、进展协议、重复 scope 和 Tool 权限
Tool 返回 阅读有界观察 保存 canonical truth,限制 bytes,只投影必要字段
生成 Draft 写出候选报告 限制输出结构和大小,Draft 尚不可发布
验证发布 不再拥有决定权 验引用、验语义,决定报告或 Fallback
请求结束 执行结束 固化终态、持久化结果、记录 Trace 和 Token

下面逐步展开。

3. 第一步:Harness 先创建 Run

请求进入 ChatApplicationUseCase 后,系统不会立即调用 Agent,而是先创建本次执行的 RunContext。

flowchart TB
    S["sessionId<br/>多轮对话容器"] --> R["runId<br/>本次独立执行"]
    R --> D["deadline"]
    R --> B["RunBudget"]
    R --> C["RunCancellation"]
    R --> L["RunLifecycle"]
    R --> P["ProgressTracker"]

为什么不能只使用 sessionId?因为同一个会话可以连续提出多个问题。Tool Call、Agent Step、Evidence 和最终结果都必须属于某一个精确 Run,否则上一轮日志可能被下一轮报告误引用。

为什么要在 Agent 之前创建预算和取消能力?因为 Router、Agent、Tool 和 SemanticGuard 都会消耗时间或资源。只有共享同一个 RunContext,系统才能统一回答“还能不能继续执行”。

这一阶段最重要的不是创建了几个对象,而是确定了一条规则:

后续任何模型调用、Tool 调用和发布动作,都必须证明自己仍属于这个 active Run。

4. 第二步:Router 只决定走哪条路

用户问题先经过 Intent Router。它只判断请求属于:

SYSTEM_CHAT
KNOWLEDGE_QUERY
DIAGNOSIS

本次结果是 DIAGNOSIS,于是 ChatApplicationUseCase 将同一个 RunContext 交给 DiagnosisChatExecutor。

Router 不读取完整历史,不调用业务 Tool,也不尝试回答支付超时的根因。这样设计是为了避免“路由”在不知不觉中变成一个简化版诊断 Agent。

即便只是路由,Harness 仍然控制它的输入大小、timeout、Token、显式 retry 和 Trace。因为一次隐藏的模型重试,同样会造成成本和延迟无法解释。

5. 第三步:Agent 决定查什么,Harness 决定能不能查

Diagnosis Agent 第 1 轮读取用户问题和服务端注册的 Tool schema,随后发出两个 Tool Call:

lookup_knowledge
query_logs

这里仍然由 Agent 负责业务判断:它认为需要先查排障知识,再查看应用日志。Harness 不会用固定工作流替它安排“先 RAG、再日志”。

但 Agent 发出 Tool Call 不等于后端会立即执行。调用先经过 HarnessToolInterceptor:

flowchart LR
    TC["Agent Tool Call"] --> A{"Run 仍 active?"}
    A --> P{"上一轮进展协议正确?"}
    P --> D{"scope 是否重复或已饱和?"}
    D --> B{"ToolBoundary 权限与预算允许?"}
    B -->|"全部通过"| X["执行 Tool backend"]
    A -->|"否"| STOP["拒绝调用"]
    P -->|"否"| STOP
    D -->|"否"| STOP
    B -->|"否"| STOP

这就是 Harness 与工作流引擎的区别:

  • 工作流引擎决定下一步必须调用什么;
  • Harness 不替 Agent 选下一步,只检查这一步是否满足执行条件。

6. 第四步:Tool 返回的不是一份数据,而是三种视图

两个 Tool 都通过 ToolBoundary。它统一完成 Run 归属、Tool 授权、只读约束、调用预算、结果 bytes、canonical 状态和长期审计检查。

后端原始结果不会直接返回给 Agent,而是形成三种用途不同的视图:

flowchart TB
    RAW["Tool Raw Result"] --> C["Canonical Invocation<br/>当前 Run 的短期完整真相"]
    C --> CV["Control View<br/>状态、证据数量、scope"]
    C --> MO["Model Observation<br/>Agent 可见的有界内容"]
    C --> EG["EvidenceGuard<br/>独立验真来源"]
    C -.-> AU["Durable Audit<br/>长期只保存元数据"]

为什么要分开?

  • Agent 需要的是能继续推理的少量事实,不需要 Redis key、预算阈值和全部 raw;
  • EvidenceGuard 需要独立于 Agent 上下文读取真实调用记录;
  • 线上审计需要状态、耗时和 bytes,但不应该永久复制日志正文。

本次真实 Run 中:

Tool InvocationStatus EvidenceStatus 说明
lookup_knowledge READY EVIDENCE_FOUND 找到了候选排障知识
query_logs READY EVIDENCE_FOUND 返回了候选日志内容,但数据源明确为 Mock

这两个结果只能证明“查询成功并返回候选内容”,不能证明“已经找到支付超时的生产根因”。尤其 Mock 日志不能被包装成生产环境事实。

7. 第五步:Agent 写出 Draft,但 Draft 还不是报告

Tool Observation 回到 ReAct 上下文后,Agent 进入第 2 轮模型调用并生成结构化 DiagnosisDraft。

真实 Trace 中可以看到:

Agent Step 0:has_text=false,发出 lookup_knowledge 和 query_logs
Agent Step 1:has_text=true,不再调用 Tool

到这一步,Agent 的职责已经完成:它收集了观察结果,并根据这些内容写出候选结论、分析和限制。

但 Harness 把它称为 Draft,而不是 Report。原因是模型输出还没有证明:

  • 引用的 tool_call_id 属于当前 Run;
  • 引用对应的调用已经 READY;
  • 引用内容确实存在于 canonical result;
  • 真实证据足以支持用户可见结论。

如果此时直接通过 SSE 返回,前面所有 canonical store 和 Guard 设计都失去了意义。

8. 第六步:EvidenceGuard 挡住了这次发布

Release Pipeline 首先调用 EvidenceGuard。它不调用模型,而是通过代码检查 Draft 的结构、引用闭包、Run 归属、Tool 状态和 evidence 内容。

本次结果没有通过 EvidenceGuard。公开 SSE 没有泄露具体内部违规字段,只给出了稳定结果:

FallbackType = EVIDENCE_VALIDATION_FAILED
message = 当前证据无法完成真实性校验,无法确认根因
verified_sources = []

这意味着系统不能把 Agent Draft 中的引用提升为已验真证据。既然第一层确定性验证都没有通过,后续 SemanticGuard 就没有可信输入,也不应继续讨论结论是否“语义上合理”。

flowchart LR
    D["DiagnosisDraft"] --> E{"EvidenceGuard"}
    E -->|"通过"| S["SemanticGuard<br/>检查证据是否支持结论"]
    E -->|"本次未通过"| F["EVIDENCE_VALIDATION_FAILED"]
    S -->|"SUPPORTED"| OK["发布原 Draft"]
    S -->|"UNSUPPORTED"| SF["语义不支持 Fallback"]

这里最值得注意的是:两个 Tool 都成功了,Agent 也成功输出了 Draft,但 Release 仍然拒绝发布。

Tool READY
    不等于 EvidenceGuard 通过
EvidenceGuard 通过
    不等于 SemanticGuard SUPPORTED
SemanticGuard SUPPORTED
    才可能发布正常诊断报告

9. 第七步:Fallback 是安全结果,不是技术失败

EvidenceGuard 未通过后,SafeFallbackFactory 由代码构造公开内容。它没有让另一个模型“重新写得保守一点”,也没有复用 Draft 中未经验证的根因。

用户最终收到:

{
  "content_type": "SAFE_FALLBACK",
  "fallback": {
    "type": "EVIDENCE_VALIDATION_FAILED",
    "conclusion": null,
    "message": "当前证据无法完成真实性校验,无法确认根因",
    "verified_sources": [],
    "limitations": ["证据引用校验未通过"],
    "next_steps": ["重新收集当前诊断范围内的证据后再发起诊断"]
  }
}

SSE 顺序完整结束:

metadata
-> status ROUTING
-> status DIAGNOSIS_RUNNING
-> status SAFETY_VALIDATING
-> content SAFE_FALLBACK
-> done FALLBACK

数据库记录则是:

diagnosis_run.status = SUCCESS
diagnosis_run.release_outcome = FALLBACK

两者并不矛盾:status=SUCCESS 表示请求被系统正常、安全地处理完毕;release_outcome=FALLBACK 表示没有正常诊断结论可以发布。

10. 这次 Run 最终留下了什么

项目 真实结果
sessionId mvp-demo-payment-timeout-stage7-20260722-1741
runId 363f481c-33b8-42e7-8699-428a6ec61806
总耗时 35,996 ms
总 Token 11,764
Agent Step 2
Tool Invocation 2
Tool 结果 两次均 READY / EVIDENCE_FOUND
最终内容 SAFE_FALLBACK
ReleaseOutcome FALLBACK
FallbackType EVIDENCE_VALIDATION_FAILED

长期审计保留的是 Run、Agent Step、Tool 状态、耗时和 Token 等有界信息。模型 Thought 保持为空,完整 Tool raw 也不会因为排障方便就永久进入普通 Trace。

因此这次 Run 虽然没有根因结论,却仍然可以回答:谁执行了什么、用了多少资源、在哪一层停止、为什么没有发布以及用户最终收到了什么。

11. 如果验证通过,后续会发生什么

本次真实路径在 EvidenceGuard 结束。为了理解完整设计,可以继续看通过分支:

  1. EvidenceGuard 生成 VerifiedEvidenceSnapshot;
  2. SemanticGuard 只接收用户问题、Draft 语义和已验真证据;
  3. SemanticGuard 输出 SUPPORTED / UNSUPPORTED,不能改写报告;
  4. 只有 SUPPORTED 才发布 Agent 原 Draft;
  5. UNSUPPORTED 或 Guard 不可用时发布对应 SafeFallback。

这条分支不是本次支付超时 Run 的真实结果,因此这里只说明当前代码协议,不把它写成已经发生的事实。

12. 这次案例体现了哪些设计决策

决策一:Agent 拥有推理权,不拥有发布权

Agent 可以选择 Tool、解释 Observation 和撰写 Draft,但不能决定 Draft 是否直接交给用户。否则模型既是作者又是最终审批者。

决策二:Tool 成功与结论成立必须分层

READY + EVIDENCE_FOUND 只描述一次 Tool 调用。EvidenceGuard 和 SemanticGuard 分别处理引用真实性与结论支持度,避免一个含糊的 SUCCESS 贯穿全链路。

决策三:系统保留事实,模型只拿观察

Canonical Invocation 为当前 Run 保存完整真相;Agent Observation 只服务推理。Guard 不依赖模型看到的裁剪版本进行自证。

决策四:证据不足也应正常结束

不是所有诊断都能找到根因。Fallback 把“不能安全下结论”转换成稳定产品行为,而不是抛出技术异常或让模型猜一个答案。

决策五:审计记录控制事实,不复制思维过程

系统需要可回放,但不需要永久存储 Chain of Thought、完整 Prompt 和所有 raw payload。可观测性本身也必须有数据边界。

13. 用一句话复述这次案例

用户提出支付超时问题后,Harness 为请求建立独立 Run,允许 Diagnosis Agent 自主调用知识库和日志 Tool,并将完整调用事实保存在系统边界内;Agent 根据有界 Observation 写出 Draft 后,EvidenceGuard 发现引用无法完成真实性校验,Release 因此拒绝发布根因,转而返回确定性的 SafeFallback。整个请求正常结束、过程可回放,但未经验证的结论没有离开系统。

理解这句话,就理解了 Harness 的核心:它不保证 Agent 每次都找到答案,但保证系统只对能够证明的答案负责。

14. 事实来源与延伸阅读

本文案例数据来自:

  • mvp/demo/requests/payment-timeout-chat.json;
  • mvp/demo/output/stage7-20260722-1741/chat-sse.txt;
  • mvp/demo/output/stage7-20260722-1741/trace-response.json;
  • devflow/projects/2026-07-22-single-react-cleanup-e2e/evidence.md。

继续理解具体机制:

需要查看另一条真实 SUCCESS 路径及详细 Token、RAG 和 Trace 数据,阅读一次诊断全流程 E2E 导读。