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

327 lines
14 KiB
Markdown
Raw 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 如何控制 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<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`。
```mermaid
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。它只判断请求属于:
```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<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 中可以看到:
```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<br/>检查证据是否支持结论"]
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)。