Files
SuperBizAgent-java/mvp/engineering/harness/Harness设计演进-从多Agent编排到确定性控制边界.md

372 lines
18 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.
# Harness 设计演进:从多 Agent 编排到确定性控制边界
Harness 不是一开始就被完整设计出来的。它来自几轮真实重构:系统先拆出多个 Agent 角色,又增加 Gatekeeper 保证证据真实性,随后用 StateGraph 显式管理状态和分支,最终才发现一个更根本的问题:**外层系统正在重复实现 Agent 本身已经具备的 ReAct 生命周期。**
这篇文章不按提交逐条记流水账,而是追踪每一次设计变化背后的问题:当时为什么这样做、它解决了什么、为什么后来仍然不够,以及哪些思想最终保留了下来。
第一次阅读只看第 1、2、6、8 和 10 节即可。先建立演进主线,再理解最终边界和可复用经验。
## 1. 先看完整演进路线
```mermaid
flowchart LR
M["多 Agent 分工<br/>Planner / Executor / Verifier / Composer"]
G["Gatekeeper<br/>在模型审查前机械验真"]
S["StateGraph<br/>显式状态、条件边和终态"]
H["Single ReAct + Harness<br/>推理与控制分离"]
P["Progress Control<br/>从预算止损到正常收敛"]
M -->|"证据引用可能伪造"| G
G -->|"状态藏在 Service、Hook 和 ThreadLocal"| S
S -->|"显式了编排,但仍重复 ReAct"| H
H -->|"预算能止损,不能判断继续是否有价值"| P
```
这几次变化不是简单地“旧方案错、新方案对”。每一阶段都解决了当时最明显的问题,同时也让下一个更深层的问题暴露出来。
| 阶段 | 当时解决的核心问题 | 后来暴露的核心问题 |
|---|---|---|
| 多 Agent | 复杂诊断如何分工 | 一次 ReAct 被拆成多个模型角色和协议 |
| Gatekeeper | 如何阻止伪造证据引用 | 验真依赖旧输出结构、Trace 和隐式上下文 |
| StateGraph | 如何显式表达状态、重试和 Fallback | Graph 仍在编排多个重复的推理角色 |
| Single ReAct + Harness | 如何分离业务推理与确定性控制 | Agent 仍可能在证据不足时空转 |
| Progress Control | 如何让无证据诊断正常停止 | 阈值和信息增益仍需持续校准 |
## 2. 第一阶段:把复杂诊断拆成多个 Agent
项目早期采用过 Supervisor 和 Sequential 两类多 Agent 编排。Chat 诊断最终形成了一条固定链路:
```mermaid
flowchart LR
Q["用户问题"] --> P["Planner<br/>制定排查计划"]
P --> E["Executor<br/>调用 Tool 收集证据"]
E --> G["Gatekeeper<br/>检查证据引用"]
G --> V["Verifier<br/>判断 Claim 是否可信"]
V --> C["Composer<br/>组织最终回答"]
```
这个方案有合理动机:复杂诊断既要规划、执行,又要验证和表达,把职责拆开比让一个 Prompt 包办所有事情更容易理解。
它也确实建立了几项重要能力:
- Planner 不直接编造执行结果;
- Executor 专注 Tool 调用和微观事实;
- Verifier 不再负责重新检索;
- Composer 只能表达经过允许的 Claim;
- 每个角色都有自己的结构化输出和测试入口。
问题出在拆分粒度。Planner、Executor、Verifier 和 Composer 看似是不同业务岗位,实际上刚好覆盖了一次完整 ReAct:
```text
思考 -> Planner
行动观察 -> Executor + Tool
自我检查 -> Verifier
最终回答 -> Composer
```
Agent 框架本来已经支持“思考、调用 Tool、读取 Observation、继续推理、生成答案”。外层再把它拆成四个 Agent 后,系统必须额外维护:
- 四套 Prompt 和输出 Schema;
- Agent 间的 JSON 转换;
- 证据和上下文的重复搬运;
- PASS、LOW_CONFID、REJECT 与重试分支;
- 每个角色各自的 Token、timeout 和错误语义;
- Composer 是否严格遵守 Verifier 输出的新风险。
第一阶段真正留下的经验不是“多 Agent 一定不好”,而是:
> 只有当角色拥有不同数据权限、不同工具或真正独立的业务目标时,拆成多个 Agent 才可能值得。仅仅把一次 ReAct 的内部步骤外置成多个角色,会放大协议成本。
## 3. 第二阶段:Gatekeeper 把确定性验真从模型中拿出来
多 Agent 链路很快遇到另一个问题:Verifier 可以判断一段 evidence excerpt 看起来是否支持 Claim,却不能证明 `source_invocation_id`、`raw_path` 和 excerpt 真正对应某次 Tool 调用。
如果仍然让模型检查这些字段,就会出现“模型验证模型”的循环。于是系统在 Executor 和 Verifier 之间加入 `ExecutorGatekeeperService`:
```mermaid
flowchart LR
E["Executor 输出引用"] --> G["Gatekeeper<br/>查 Tool Invocation 和 raw path"]
G -->|"真实"| V["Verifier<br/>判断语义支持关系"]
G -->|"伪造或错配"| R["拒绝进入 Verifier"]
```
这是演进中一个非常重要、并且最终被保留的决策:
```text
代码能够机械证明的事实,不交给模型判断。
```
Gatekeeper 能拒绝伪造 invocation、错误 raw path 和不匹配的 evidence excerpt,使“引用真实”和“语义成立”第一次成为两个独立问题。
但旧 Gatekeeper 仍然耦合在多 Agent 协议上:
- 它读取 Executor 特定的 `executor_evidence_v2`;
- 引用协议包含 `source_invocation_id + raw_path + excerpt`;
- Verifier 输入依赖 Hook 组装;
- Tool Trace 和 Agent 上下文通过 ThreadLocal 等隐式状态关联;
- 它只能保护旧 Executor 到 Verifier 的这一段链路。
后来的 `EvidenceGuard` 不是凭空出现的。它继承了 Gatekeeper 的核心思想,但把真理源改为当前 Run 的 canonical Tool Invocation,并把验证对象改为最终 `DiagnosisDraft`。
## 4. 第三阶段:StateGraph 让隐式编排变得可见
随着重试、低置信分支、Fallback、Run Trace 和多个 Agent 输出不断增加,旧 `ChatService + SequentialAgent + Hook + ThreadLocal` 很难回答一个简单问题:**当前诊断到底处于哪个状态,下一步为什么走这条分支?**
2026-07-17 到 2026-07-20,项目完成了一轮 StateGraph 改造。它将节点、条件边、共享状态和终态显式化,并切换公开 Chat 诊断入口。
```mermaid
flowchart LR
P["Planner Node"] --> E["Executor Node"]
E --> G["Gatekeeper Node"]
G --> V["Verifier Node"]
V -->|"PASS"| C["Composer Node"]
V -->|"补证据"| RP["Evidence Retry Prepare"]
RP --> P
V -->|"拒绝"| F["Fallback Node"]
```
StateGraph 解决了几个真实问题:
- 分支不再隐藏在大段 Service `if/else` 中;
- Graph State 显式携带 Run 级数据;
- Node 和条件边可以独立测试;
- Fallback 和 retry 路径可以画出来并验证;
- 旧 `VerifierContextHolder`、部分 Hook 和 ThreadLocal 状态得以清理;
- `ChatService` 从直接拥有全部诊断细节转为调用 Graph Runtime。
因此,StateGraph 不是一次无效重构。它提高了旧多 Agent 架构的可见性和可测试性。
但它解决的是“怎样更清楚地编排这些角色”,没有重新质疑“这些角色是否都应该存在”。结果是:
- Planner、Executor、Verifier、Composer 仍然各自调用模型;
- Graph State 继续搬运多个角色的结构化上下文;
- Node Adapter、Result Mapper、条件边和业务协议形成第二套控制结构;
- 外层 Graph 决定何时计划、执行、补证据和回答,而框架内 Agent 也在做相似的 ReAct 控制;
- 状态机显式了复杂度,却没有消除复杂度。
这一阶段带来的关键认识是:
> 显式状态机能够治理复杂流程,但不能证明流程本身有必要。如果核心流程只是一个 Agent 的自然 ReAct,Graph 可能只是把重复实现变得更整齐。
## 5. 转折点:问题不是编排写得不好,而是重复实现了 ReAct
ISS-014 对旧架构做了一个更根本的判断:Planner、Executor、Verifier、Composer 并不是四个真正独立的业务主体,而是把一个完整 ReAct 生命周期拆到了 Agent 外部。
这使设计问题从:
```text
怎样把多 Agent 编排得更清楚?
```
转变为:
```text
哪些决定必须由模型做,哪些约束必须由代码拥有?
```
这个问题带来了新的职责划分:
| 决定 | 所有者 |
|---|---|
| 提出故障假设、选择 Tool、解释证据、写 Draft | Diagnosis Agent |
| Run 身份、deadline、预算、取消、retry、唯一终态 | Harness Core |
| Tool 权限、只读、bytes、canonical truth | ToolBoundary |
| 引用是否真实 | EvidenceGuard |
| 真实证据是否支持报告 | 隔离的 SemanticGuard |
| 发布原 Draft 还是 SafeFallback | Release |
这不是把所有能力重新塞回一个“大 Agent”。相反,它按**判断性质**而不是按“岗位名称”拆分:
- 非确定性的业务推理留给 Agent;
- 可以机械证明的控制规则交给代码;
- 必须使用模型的语义审查被隔离成单轮、无 Tool、无记忆的 Guard。
## 6. 第四阶段:一个 Diagnosis Agent,加一层 Harness
最终架构只保留一个拥有业务 Tool loop 的 Diagnosis Agent:
```mermaid
flowchart TB
U["用户问题"] --> APP["Chat Application"]
APP --> H["Harness Core<br/>Run、预算、取消、终态"]
H --> A["Diagnosis ReAct Agent<br/>假设、Tool、Observation、Draft"]
A --> TB["ToolBoundary<br/>执行与 canonical truth"]
TB --> A
A --> EG["EvidenceGuard<br/>确定性引用验真"]
EG --> SG["SemanticGuard<br/>隔离语义审查"]
SG --> REL["Release<br/>SUCCESS 或 SafeFallback"]
```
这里做了几项明确取舍。
### 不再保留业务 StateGraph
项目不再用外层 Graph 编排 Planner、Executor、Verifier 和 Composer。底层 ReactAgent 框架内部是否使用 Graph 属于框架实现细节,不再成为项目业务协议。
### 不自己重写 ReAct loop
Diagnosis Agent 使用框架原生 Tool Calling 和 ReAct。Harness 通过 Model/Tool Interceptor、Hook 和显式 RunContext 接入,不维护第二套 `while` 循环。
### 不让单 Agent 获得全部权力
Agent 合并的是业务推理职责,不是安全职责。它不能管理预算、读取 canonical raw、验证自己引用、调用 SemanticGuard 或决定最终发布。
### SemanticGuard 不是第二个业务 Agent
它只接收原始问题、Draft 的用户可见语义和 verified evidence,输出 `SUPPORTED / UNSUPPORTED`。它无 Tool、无记忆、不回调主 Agent,也不能改写报告。
### 迁移按边界而不是按页面完成
实施顺序先冻结 Contract,再建立 RunContext/Retry Core、Tool Boundary、各类投影、单 Diagnosis Agent、Evidence/Semantic Guard,最后切换 Application/SSE 并删除旧架构。这避免了“先切入口,再补安全边界”的过渡风险。
## 7. Harness 建成后,问题继续暴露
单 Agent + Harness 解决了外层重复编排,但真实运行又暴露了几类更细的问题。
### Tool 成功不等于有证据
旧代码常用一个 `success` 表达所有含义。后来拆为:
```text
InvocationStatus:Tool 调用是否完成
EvidenceStatus:当前 scope 是否返回候选证据
SemanticVerdict:证据是否支持报告
ReleaseOutcome:最终向用户发布什么
```
状态变多不是为了复杂,而是为了避免 `SUCCESS` 在四层中表达四种不同意思。
### Agent Observation 不能充当真理源
Agent 需要的是有界、清洗后的 Tool 结果;EvidenceGuard 需要的是独立、可回读的调用真相;长期 Audit 又不能复制全部敏感 raw。于是形成 canonical truth、Model Observation 和 metadata audit 三层数据责任。
### 引用真实不等于结论成立
Gatekeeper 思想被升级为 EvidenceGuard,但仅验引用仍不足以拦截“真实日志被过度解释”。因此保留隔离的 SemanticGuard,并由 Release 掌握唯一出口。
### 隐藏 retry 会破坏预算和审计
SDK、HTTP Client 和各模型组件各自重试会让 Token、延迟和 attempt 无法解释。最终只允许 Harness 根据稳定失败分类执行显式 retry;正常 ReAct 下一轮和重新调用 Tool 都不叫 retry。
## 8. 第五阶段:预算能止损,但不能让诊断正常完成
单 Agent 运行后又出现一个问题:当知识库未知、日志为空或查询条件不足时,预算只能限制最大调用次数,不能判断继续搜索是否还有价值。
模型可能不断改写查询,直到:
```text
BUDGET_EXHAUSTED
或 INTERNAL_FAILURE
```
用户最终只看到通用错误,却不知道系统已经检查了什么、为什么没有结论。
于是 Harness 增加 ProgressTracker 和信息增益停止协议:
```mermaid
flowchart LR
T["Tool Result"] --> I{"是否推进当前诊断?"}
I -->|"GAINED"| C["继续收集"]
I -->|"NO_GAIN"| N["连续无增益计数"]
N -->|"未达阈值"| C
N -->|"达到阈值"| S["SATURATED / STOP_REQUIRED"]
S --> P["ProgressSnapshot"]
P --> F["INSUFFICIENT_EVIDENCE Fallback"]
```
这次演进补上了资源控制与任务完成之间的差距:
- Budget 回答“还能不能继续消耗”;
- Information Gain 回答“继续查询是否推进诊断”;
- ProgressSnapshot 回答“没有结论时,哪些已检查事实仍可安全告诉用户”。
最重要的行为变化是:**证据不足成为合法完成,而不是只能撞到预算后失败。**
## 9. 哪些设计被放弃,哪些思想被保留
| 曾经的设计 | 最终处理 | 保留下来的思想 |
|---|---|---|
| Supervisor 调度多个诊断角色 | Chat 主链不再使用 | 复杂任务需要清晰职责边界 |
| Planner / Executor / Verifier / Composer | 合并业务推理到一个 Diagnosis Agent | 规划、执行、审查、表达仍需明确责任,只是不必都是 Agent |
| Executor Gatekeeper | 旧实现删除 | 确定性验真先于语义审查,演化为 EvidenceGuard |
| SequentialAgent | 删除 | 固定业务步骤必须可测试、可观测 |
| 业务 StateGraph | 删除 | 状态和终态必须显式,转化为 typed contracts、RunLifecycle 和 Release |
| ThreadLocal 上下文 | 删除 | exact Run 归属仍必须传播,改为显式 RunContext |
| PASS / LOW_CONFID / REJECT + 补证据循环 | 删除 | 不支持的结论不能发布,改为二元语义门禁和确定性 Fallback |
| Tool raw 直接参与上下文与审计 | 分层 | Tool 结果必须可追溯,但不同消费者使用不同视图 |
好的重构通常不是把过去全部推翻,而是把有效思想从不合适的实现形式中提取出来。
## 10. 这段演进真正说明了什么
Harness 最终形成,不是因为团队一开始就知道所有组件,而是逐步回答了四个问题:
1. **业务推理应该由谁负责?** 一个完整的 Diagnosis ReAct Agent。
2. **哪些约束不能依赖 Prompt?** 身份、预算、取消、权限、容量、验真和唯一发布。
3. **哪些模型判断必须隔离?** 证据是否支持用户可见报告的 SemanticGuard。
4. **证据不足怎样成为正常结果?** ProgressTracker、ProgressSnapshot 和 SafeFallback。
最终边界可以浓缩为:
```mermaid
flowchart LR
B["需要理解业务语义和提出假设"] --> A["交给 Diagnosis Agent"]
M["能够由代码机械证明"] --> H["交给 Harness"]
S["必须使用模型但不能拥有业务循环"] --> G["交给隔离 Guard"]
O["决定什么可以公开"] --> R["只交给 Release"]
```
这也是本项目对 Agent 系统最核心的工程判断:
> 不要围绕模型的“角色感”设计系统,而要围绕决策权、真理源和失败责任设计边界。
## 11. 这套演进的代价和未完成问题
当前方案不是没有代价:
- Harness 类型和状态较多,需要统一 Context 防止误读;
- canonical store 引入 Redis TTL、容量和访问控制成本;
- EvidenceGuard 与 SemanticGuard 增加发布延迟;
- 信息增益依赖模型对非空结果的二元评价,仍可能误判;
- `NO_GAIN` 阈值、Token 和 timeout 需要根据 Trace 持续校准;
- 当前 Mock 日志和未配置的业务 MySQL 数据源限制了真实诊断覆盖面。
但这些复杂度与旧多 Agent/Graph 的复杂度性质不同:旧复杂度主要用于搬运推理过程,当前复杂度主要用于保护身份、资源、事实和发布边界。前者会随角色数增长,后者围绕稳定的不变量增长。
## 12. 面试时如何讲这段演进
可以用下面这段话概括:
> 项目最初把复杂诊断拆成 Planner、Executor、Verifier 和 Composer,并通过 Gatekeeper 验证证据引用;为了治理 Service、Hook 和 ThreadLocal 中的隐式分支,又引入 StateGraph 显式管理状态和终态。Graph 提高了可测试性,但没有解决根问题:外层仍在重复框架已有的 ReAct 生命周期,并产生多套 Prompt、Schema、重试和上下文搬运。后来我们按决策性质重新划分责任,只保留一个拥有 Tool loop 的 Diagnosis Agent,把 Run、预算、取消、Tool 真相、证据验真和发布权放进确定性 Harness,语义审查则隔离成无 Tool、无记忆的单轮 Guard。最后又通过信息增益协议,让证据不足从预算失败变成可解释的正常 Fallback。
这段回答的重点不是“我们用了哪些框架”,而是展示:系统怎样从症状修补逐步走到责任边界重构。
## 13. 事实来源与延伸阅读
关键演进节点可由 Git 提交确认:
| 日期 | 代表提交 | 含义 |
|---|---|---|
| 2026-07-03 | `f01866c` | Chat 切换 Sequential Agent |
| 2026-07-08 | `c5e496e`、`1b31e78`、`6015bcb` | Gatekeeper、Verifier、Composer 完成 |
| 2026-07-17 | `581daff`、`99e490f` | StateGraph 设计冻结并切换 Chat |
| 2026-07-20 | `190013c` | StateGraph 清理与验收完成 |
| 2026-07-21 | `58c3910` 至 `f8809cb` | Single ReAct、Harness、Tool、Guard 和 Application 分阶段落地 |
| 2026-07-22 | `8ee7cc0` | 删除旧 Agent 架构 |
| 2026-07-27 | `d045218`、`38f781b` | 信息增益停止与协议修复完成 |
主要历史资料:
- `mvp/architecture/archive/2026-07-22-legacy/agent-orchestration.md`;
- `mvp/issues/archived/ISS-014-single-react-agent-harness-aci-ptk-refactor.md`;
- `devflow/projects/2026-07-21-single-react-design-freeze/decisions.md`;
- `openspec/changes/archive/2026-07-27-diagnosis-information-gain-stop-contract/`。
继续阅读:
- [Harness 入门](README.md)
- [支付超时诊断案例](案例-从一次支付超时诊断看Harness如何控制Agent.md)
- [Harness 主设计](Harness设计-非确定性Agent的确定性控制边界.md)
- [信息增益停止](Harness信息增益停止-让无证据诊断正常收敛.md)
- [组件渐进式导读](components/README.md)