Files
SuperBizAgent-java/mvp/engineering/harness/components/01-运行控制-让一次请求有边界.md

122 lines
6.3 KiB
Markdown
Raw Permalink 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.
# 01 运行控制:让一次请求有边界
这一篇只回答一个问题:**一次诊断请求由谁负责到底?**
## 1. 先看一个具体问题
用户发起支付超时诊断。请求开始后,Router 调了一次模型,Diagnosis Agent 又调用多轮模型和 Tool,SemanticGuard 最后还要调用一次模型。与此同时,客户端可能断开,某次调用可能超时,两个线程也可能同时报告成功和失败。
如果没有统一的运行控制,每个组件都会有自己的理解:
- Controller 认为连接断开了,后台 Agent 却还在继续查询;
- Agent 认为还可以调用 Tool,Run 的总预算其实已经耗尽;
- 超时线程先写入失败,迟到的模型结果又把它覆盖为成功;
- SDK 在内部偷偷重试,系统无法解释多出来的延迟和 Token;
- 线上只看到最终失败,却不知道请求在哪一步、因为什么停止。
所以运行控制不是一个计时器,而是由几个职责不同的组件共同完成。
## 2. 四组组件怎样协作
```mermaid
sequenceDiagram
participant APP as Application
participant CORE as Core / RunContext
participant WORK as Agent、Tool、Guard
participant RETRY as Retry
participant AUDIT as Audit
APP->>CORE: 创建 RunContext
CORE-->>APP: runId、deadline、budget、cancel、lifecycle
APP->>WORK: 显式传入 RunContext
WORK->>CORE: 每次消耗前检查 active 和预算
WORK->>RETRY: 仅允许策略声明的技术重试
RETRY->>AUDIT: 记录每个 attempt
WORK->>AUDIT: 记录模型、Tool 和阶段元数据
APP->>CORE: 尝试写入最终终态
CORE-->>APP: first-terminal-wins
APP->>AUDIT: 记录公开结果和预算对账
```
第一次阅读可以把它们理解为:
- **Application 是负责人**:组织一次请求从创建到公开结果。
- **Core 是执行规则**:统一管理身份、deadline、预算、取消和唯一终态。
- **Retry 是重做规则**:明确什么失败允许再试一次。
- **Audit 是运行账本**:记录发生过什么,但不保存敏感正文。
## 3. Application:负责人,而不是推理者
Application 负责建立一次请求的应用边界:读取安全历史、创建 Run、判断 intent、调用对应执行分支、持久化结果,并把安全内容交给 SSE。
设计它的原因,是这些步骤必须由一个地方协调。如果散落在 Controller、Agent 和 Guard 中,会出现多个结果出口和不同的失败语义。
它不负责判断支付超时的根因,也不负责自己拼一份诊断 Fallback。它只负责把正确的执行组件按顺序接起来,并确保结果经过统一出口。
主要代码入口:`ChatApplicationUseCase`、`DiagnosisChatExecutor`、`KnowledgeQueryExecutor`、`SystemChatExecutor`、`ChatRunStore`。
## 4. Core:一次 Run 的共同规则
Core 创建 `RunContext`。这个上下文显式携带:
```text
这是谁的请求:sessionId + runId
最晚执行到何时:deadline
还能消耗多少:RunBudget
是否要求停止:RunCancellation
最终如何结束:RunLifecycle
模型消耗如何对账:ModelCallLedger
信息收集是否仍有价值:DiagnosisProgressTracker
```
这里最重要的决策是**显式传递 RunContext**,而不是使用 ThreadLocal 或让每个组件自己查询全局状态。原因是模型和 Tool 可能跨线程运行;隐式上下文很容易丢失、串线,也很难在测试中证明归属关系。
`RunLifecycle` 使用 first-terminal-wins:第一个成功写入的终态不可被迟到结果覆盖。它解决的是并发一致性,不是公开结果的业务含义。
主要代码入口:`DiagnosisHarnessCore`、`RunContext`、`RunBudget`、`RunCancellation`、`RunLifecycle`。
## 5. Retry:不能让“再试一次”藏起来
重试会增加成本和延迟,也可能重复副作用,因此不能由 SDK、HTTP Client 和各组件各自决定。当前策略是:
- Router 和 SemanticGuard 的特定技术失败最多尝试两次;
- Diagnosis Agent、业务 Tool 和 EvidenceRepair 不做隐藏自动重试;
- 取消、预算耗尽和协议错误不能被重试吞掉。
这里没有设计通用重试 DSL。首版只需要不可变的 `RetryPolicy`、稳定的失败分类和统一的 `HarnessRetryExecutor`,复杂配置系统反而会掩盖真实执行路径。
## 6. Audit:留下解释,而不是留下全部内容
线上排障需要知道:调用了哪个组件、用了多少 Token、Tool 是否完成、为什么停止、为什么发布 Fallback。但这不等于长期保存 Prompt、SQL、日志原文、Tool raw response 和模型 reasoning。
因此 Audit 长期保留的是有界元数据,完整 Tool 真相只在 canonical store 中短期存在。这个决策同时满足可回放和最小暴露原则。
主要代码入口:`DiagnosisTraceRecorder`、`TraceAuditEvents`、`ToolInvocationAuditSink`、`HarnessAgentAuditHook`、`ModelCallAuditor`。
## 7. 为什么没有合并成一个 RunManager
把上述职责都放进一个 `RunManager` 看起来更简单,但它会同时知道 HTTP、路由、预算、模型、Tool、数据库和 SSE,最后变成新的业务工作流引擎。
当前拆分依据不是“代码越细越好”,而是决策权不同:
| 组件 | 它拥有的决定 | 它不能决定 |
|---|---|---|
| Application | 请求走哪条应用分支、何时持久化和返回 | 业务根因是否成立 |
| Core | 是否仍可执行、资源是否允许、哪个终态生效 | 返回正常报告还是 Fallback |
| Retry | 某类技术失败是否允许下一 attempt | 修改业务结果 |
| Audit | 记录哪些安全元数据 | 影响执行结果 |
## 8. 先记住这些
第一次阅读只需记住:
1. Application 对一次请求负责,Core 对执行不变量负责。
2. RunContext 必须显式传播,所有模型和 Tool 共用同一预算与取消信号。
3. 第一个终态获胜,迟到结果不能翻案。
4. Retry 必须显式、分类、可审计。
5. Audit 记录控制事实,不长期复制敏感正文。
下一篇:[02 Agent 接入与收敛](02-Agent接入与收敛-让推理可以工作也可以停止.md)。
需要查完整状态转换时,阅读[生命周期与状态](../Harness生命周期与状态.md);需要查全部类型时,阅读[组件全景](../Harness组件全景-职责-设计原因与边界.md)。