# 从一次支付超时诊断看 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)。