# Harness 设计演进:从多 Agent 编排到确定性控制边界 Harness 不是一开始就被完整设计出来的。它来自几轮真实重构:系统先拆出多个 Agent 角色,又增加 Gatekeeper 保证证据真实性,随后用 StateGraph 显式管理状态和分支,最终才发现一个更根本的问题:**外层系统正在重复实现 Agent 本身已经具备的 ReAct 生命周期。** 这篇文章不按提交逐条记流水账,而是追踪每一次设计变化背后的问题:当时为什么这样做、它解决了什么、为什么后来仍然不够,以及哪些思想最终保留了下来。 第一次阅读只看第 1、2、6、8 和 10 节即可。先建立演进主线,再理解最终边界和可复用经验。 ## 1. 先看完整演进路线 ```mermaid flowchart LR M["多 Agent 分工
Planner / Executor / Verifier / Composer"] G["Gatekeeper
在模型审查前机械验真"] S["StateGraph
显式状态、条件边和终态"] H["Single ReAct + Harness
推理与控制分离"] P["Progress Control
从预算止损到正常收敛"] 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
制定排查计划"] P --> E["Executor
调用 Tool 收集证据"] E --> G["Gatekeeper
检查证据引用"] G --> V["Verifier
判断 Claim 是否可信"] V --> C["Composer
组织最终回答"] ``` 这个方案有合理动机:复杂诊断既要规划、执行,又要验证和表达,把职责拆开比让一个 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
查 Tool Invocation 和 raw path"] G -->|"真实"| V["Verifier
判断语义支持关系"] 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
Run、预算、取消、终态"] H --> A["Diagnosis ReAct Agent
假设、Tool、Observation、Draft"] A --> TB["ToolBoundary
执行与 canonical truth"] TB --> A A --> EG["EvidenceGuard
确定性引用验真"] EG --> SG["SemanticGuard
隔离语义审查"] SG --> REL["Release
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)