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

18 KiB
Raw Blame History

Harness 设计演进:从多 Agent 编排到确定性控制边界

Harness 不是一开始就被完整设计出来的。它来自几轮真实重构:系统先拆出多个 Agent 角色,又增加 Gatekeeper 保证证据真实性,随后用 StateGraph 显式管理状态和分支,最终才发现一个更根本的问题:外层系统正在重复实现 Agent 本身已经具备的 ReAct 生命周期。

这篇文章不按提交逐条记流水账,而是追踪每一次设计变化背后的问题:当时为什么这样做、它解决了什么、为什么后来仍然不够,以及哪些思想最终保留了下来。

第一次阅读只看第 1、2、6、8 和 10 节即可。先建立演进主线,再理解最终边界和可复用经验。

1. 先看完整演进路线

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 诊断最终形成了一条固定链路:

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:

思考    -> 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:

flowchart LR
    E["Executor 输出引用"] --> G["Gatekeeper<br/>查 Tool Invocation 和 raw path"]
    G -->|"真实"| V["Verifier<br/>判断语义支持关系"]
    G -->|"伪造或错配"| R["拒绝进入 Verifier"]

这是演进中一个非常重要、并且最终被保留的决策:

代码能够机械证明的事实,不交给模型判断。

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 诊断入口。

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 外部。

这使设计问题从:

怎样把多 Agent 编排得更清楚?

转变为:

哪些决定必须由模型做,哪些约束必须由代码拥有?

这个问题带来了新的职责划分:

决定 所有者
提出故障假设、选择 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:

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 表达所有含义。后来拆为:

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 运行后又出现一个问题:当知识库未知、日志为空或查询条件不足时,预算只能限制最大调用次数,不能判断继续搜索是否还有价值。

模型可能不断改写查询,直到:

BUDGET_EXHAUSTED
或 INTERNAL_FAILURE

用户最终只看到通用错误,却不知道系统已经检查了什么、为什么没有结论。

于是 Harness 增加 ProgressTracker 和信息增益停止协议:

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。

最终边界可以浓缩为:

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/。

继续阅读: