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

6.3 KiB
Raw Permalink Blame History

01 运行控制:让一次请求有边界

这一篇只回答一个问题:一次诊断请求由谁负责到底?

1. 先看一个具体问题

用户发起支付超时诊断。请求开始后,Router 调了一次模型,Diagnosis Agent 又调用多轮模型和 Tool,SemanticGuard 最后还要调用一次模型。与此同时,客户端可能断开,某次调用可能超时,两个线程也可能同时报告成功和失败。

如果没有统一的运行控制,每个组件都会有自己的理解:

  • Controller 认为连接断开了,后台 Agent 却还在继续查询;
  • Agent 认为还可以调用 Tool,Run 的总预算其实已经耗尽;
  • 超时线程先写入失败,迟到的模型结果又把它覆盖为成功;
  • SDK 在内部偷偷重试,系统无法解释多出来的延迟和 Token;
  • 线上只看到最终失败,却不知道请求在哪一步、因为什么停止。

所以运行控制不是一个计时器,而是由几个职责不同的组件共同完成。

2. 四组组件怎样协作

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。这个上下文显式携带:

这是谁的请求: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 接入与收敛。

需要查完整状态转换时,阅读生命周期与状态;需要查全部类型时,阅读组件全景。