docs(mvp): add remaining harness audit guides and engineering index
This commit is contained in:
@@ -0,0 +1,371 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user