# 从一次支付超时诊断看 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. 先看完整故事 ```mermaid flowchart LR Q["用户报告支付超时"] --> R["创建 Run
建立身份、预算和取消"] R --> I["Router 判定为 DIAGNOSIS"] I --> A1["Agent 第 1 轮
选择知识库和日志 Tool"] A1 --> T["ToolBoundary
执行、投影并保存调用真相"] T --> A2["Agent 第 2 轮
根据观察结果生成 Draft"] A2 --> E["EvidenceGuard
验证引用真实性"] E -->|"本次未通过"| F["SafeFallback
不发布未经验证的根因"] 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`。 ```mermaid flowchart TB S["sessionId
多轮对话容器"] --> R["runId
本次独立执行"] 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。它只判断请求属于: ```text 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: ```text lookup_knowledge query_logs ``` 这里仍然由 Agent 负责业务判断:它认为需要先查排障知识,再查看应用日志。Harness 不会用固定工作流替它安排“先 RAG、再日志”。 但 Agent 发出 Tool Call 不等于后端会立即执行。调用先经过 `HarnessToolInterceptor`: ```mermaid 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,而是形成三种用途不同的视图: ```mermaid flowchart TB RAW["Tool Raw Result"] --> C["Canonical Invocation
当前 Run 的短期完整真相"] C --> CV["Control View
状态、证据数量、scope"] C --> MO["Model Observation
Agent 可见的有界内容"] C --> EG["EvidenceGuard
独立验真来源"] C -.-> AU["Durable Audit
长期只保存元数据"] ``` 为什么要分开? - 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 中可以看到: ```text 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 没有泄露具体内部违规字段,只给出了稳定结果: ```text FallbackType = EVIDENCE_VALIDATION_FAILED message = 当前证据无法完成真实性校验,无法确认根因 verified_sources = [] ``` 这意味着系统不能把 Agent Draft 中的引用提升为已验真证据。既然第一层确定性验证都没有通过,后续 SemanticGuard 就没有可信输入,也不应继续讨论结论是否“语义上合理”。 ```mermaid flowchart LR D["DiagnosisDraft"] --> E{"EvidenceGuard"} E -->|"通过"| S["SemanticGuard
检查证据是否支持结论"] E -->|"本次未通过"| F["EVIDENCE_VALIDATION_FAILED"] S -->|"SUPPORTED"| OK["发布原 Draft"] S -->|"UNSUPPORTED"| SF["语义不支持 Fallback"] ``` 这里最值得注意的是:两个 Tool 都成功了,Agent 也成功输出了 Draft,但 Release 仍然拒绝发布。 ```text Tool READY 不等于 EvidenceGuard 通过 EvidenceGuard 通过 不等于 SemanticGuard SUPPORTED SemanticGuard SUPPORTED 才可能发布正常诊断报告 ``` ## 9. 第七步:Fallback 是安全结果,不是技术失败 EvidenceGuard 未通过后,`SafeFallbackFactory` 由代码构造公开内容。它没有让另一个模型“重新写得保守一点”,也没有复用 Draft 中未经验证的根因。 用户最终收到: ```json { "content_type": "SAFE_FALLBACK", "fallback": { "type": "EVIDENCE_VALIDATION_FAILED", "conclusion": null, "message": "当前证据无法完成真实性校验,无法确认根因", "verified_sources": [], "limitations": ["证据引用校验未通过"], "next_steps": ["重新收集当前诊断范围内的证据后再发起诊断"] } } ``` SSE 顺序完整结束: ```text metadata -> status ROUTING -> status DIAGNOSIS_RUNNING -> status SAFETY_VALIDATING -> content SAFE_FALLBACK -> done FALLBACK ``` 数据库记录则是: ```text 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`。 继续理解具体机制: - [Harness 入门](README.md) - [组件渐进式导读](components/README.md) - [Tool 双视图](Harness-Tool双视图-从原始结果到可验证证据.md) - [证据安全链](Harness证据安全链-从引用真实到结论可发布.md) - [生命周期与状态](Harness生命周期与状态.md) 需要查看另一条真实 `SUCCESS` 路径及详细 Token、RAG 和 Trace 数据,阅读[一次诊断全流程 E2E 导读](../diagnosis/一次诊断全流程-E2E导读.md)。