feat(agent): add executor gatekeeper hook

This commit is contained in:
aruo
2026-07-08 02:01:49 +08:00
parent 050cbc8fee
commit c5e496e715
23 changed files with 1343 additions and 30 deletions
+247 -28
View File
@@ -1,9 +1,10 @@
# Executor Structured Output V2 实施设计
# Executor Structured Output V2 实施 Issue
**状态**:待规划
**状态**:分阶段实施中
**严重程度**:高
**日期**:2026-07-07
**文档类型**:实施设计文档
**创建日期**:2026-07-07
**最后更新**:2026-07-08
**文档类型**:可执行 issue / 分阶段实施说明
**范围**:Chat Executor 输出结构、Gatekeeper、Verifier、Composer 数据契约调整
**关联问题**:
@@ -14,13 +15,15 @@
---
## 0. 给实现 Agent 的阅读入口
## 0. 给后续实现 Agent 的执行入口
这份文档用于指导 `Chat` 复杂诊断链路的一次增量改造。实现时不要先从字段表开始,而应按以下顺序阅读:
这份文档不是单纯的数据结构草案,而是 `Executor Structured Output V2` 的分阶段实施 issue。后续 agent 接手时,应先理解问题背景和阶段边界,再进入具体字段定义。
1. 先读 `0.1 - 0.7`,理解为什么改、当前链路是什么、目标链路是什么、哪些地方不能改。
2. 再读 `20 - 25`,理解代码影响面、实施阶段、回滚策略和完成标准。
3. 最后按需查阅 `2 - 19` 的详细数据契约、Gatekeeper 规则、Verifier/Composer 输入输出。
推荐阅读顺序:
1. 先读 `0.1 - 0.10`:理解为什么要改、当前问题在哪里、目标链路是什么、哪些事情明确不做,以及如何按 OpenSpec/devflow 分阶段落地。
2. 再读 `20 - 25`:理解代码影响面、阶段拆分、每阶段验收、回滚策略和最终完成标准。
3. 最后按需查阅 `2 - 19`:这些是实现时使用的详细数据契约、Gatekeeper 规则、Verifier/Composer 输入输出。
一句话目标:
@@ -31,18 +34,36 @@
最终用户答案交给 Composer 生成。
```
执行约束:
- 必须按阶段实施,不允许把五个阶段揉成一个大改动。
- 每个阶段都要先有 OpenSpec change,再实现、验证、归档到 devflow,并单独提交。
- 阶段未归档、未提交前,不进入下一阶段。
- 验收失败如果是代码问题,agent 自行修复;如果是设计决策不明确,停下来问用户。
- 本 issue 不要求一次性完成所有阶段;后续 agent 应从当前 git/OpenSpec/devflow 状态继续推进。
---
## 0.1 背景
当前 Chat 复杂诊断链路中,Executor 的输出既包含结构化证据归因,也包含最终面向用户的自然语言答案:
近期 Chat 复杂诊断链路中,多条诊断会话被 Verifier 判为 `LOW_CONFID`。这些会话并不是没有调用工具;Executor 通常已经调用了 `lookup_knowledge`、`query_metrics`、`query_logs` 等 evidence tools。
真正的问题是:Executor 在综合输出阶段把三类内容混在一起写成“当前事故结论”:
1. 本轮工具真实返回的事实。
2. runbook、skill 或知识库里的通用模式。
3. 模型基于经验补全的推断。
当前 Executor 输出中还包含最终面向用户的自然语言字段:
```text
diagnosis_summary
user_facing_answer
```
这导致一个核心问题:Executor 在证据还不充分时,容易提前把“可能方向”写成“确认结论”。后续 Verifier 虽然会校验,但它面对的是自然语言答案和结构化字段混在一起的输出,容易出现以下风险:
这会诱导 Executor 提前进入“诊断报告 / 用户表达”模式,在证据不充分时把“可能方向”写成“确认结论”。后续 Verifier 虽然会拦截,但它面对的是自然语言答案和结构化字段混在一起的输出,校验边界不稳定。
典型风险包括:
- Executor 编造不存在的 `source_invocation_ids`。
- Executor 使用真实 invocation id,但 `evidence_excerpt` 与真实工具输出不一致。
@@ -50,11 +71,38 @@ user_facing_answer
- Executor 在 `user_facing_answer` 里夹带 claims 中没有的根因、错误码、指标值或修复建议。
- Verifier 被迫从自然语言里逐字抽事实,校验边界不稳定。
因此,本次改造不是为了增加 Agent 数量,而是为了收紧职责边界和证据链路。
因此,本次改造不是为了“多加几个 Agent 显得完整”,而是为了收紧证据归因链路:Executor 只产出可校验材料,Verifier 只判断材料是否被证据支撑,最终表达交给 Composer。
---
## 0.2 当前实现
## 0.2 当前问题定义
本 issue 要解决的是:
```text
Executor 证据归因幻觉
```
更具体地说,Executor 已经调用了工具,但在最终输出时没有严格区分:
| 类型 | 定义 | 应进入哪里 |
|---|---|---|
| direct evidence | 本轮工具直接观测到的事实 | `claims` |
| reasonable inference | 能由证据合理推出但不是逐字出现的判断 | `claims`,但需要标注 `indirect`,并由 Verifier 判断是否说重 |
| hypothesis | 值得排查但尚未被当前证据确认的方向 | `hypotheses` |
| missing evidence | 还缺少哪些证据才能确认 | `missing_info` |
| user expression | 面向用户的自然语言答案 | Composer 输出,不由 Executor 输出 |
如果不拆开这些边界,Verifier 会长期承担两个不同任务:
1. 从自然语言中抽事实。
2. 判断事实是否有证据支撑。
这会让 Verifier 的职责过重,也让最终答案容易夹带未经校验的内容。
---
## 0.3 当前实现
当前代码中的复杂 Chat 链路是三 Agent 顺序执行:
@@ -90,7 +138,7 @@ Verifier 既要校验 structured output,又要扫描自然语言答案
---
## 0.3 目标设计
## 0.4 目标设计
目标链路仍保持单条顺序链路,不新增 Controller,不改 Planner:
@@ -124,23 +172,42 @@ Executor raw JSON
-> Composer 生成 user_facing_answer
```
核心原则:
- 单智能体优先:不为了抽象而新增 Controller;只有已经明确的职责边界才拆 Agent。
- Planner 不改:本 issue 的问题不在计划拆解,而在 Executor 输出和 Verifier 校验边界。
- Gatekeeper 不做诊断:只做确定性校验,拦截伪造 ID、字段退化、明显张冠李戴。
- Verifier 不再逐字扫描最终答案:主校验对象是结构化 `claims`。
- Composer 不补事实:只把 Verifier 允许的材料组织成中文答案。
---
## 0.4 分阶段实施总览
## 0.5 分阶段实施总览
| 阶段 | 目标 | 主要改动 | 验收重点 |
|---|---|---|---|
| 阶段一 | Executor V2 输出契约 | 改 `chat-executor-prompt.md`,移除 `diagnosis_summary` / `user_facing_answer` | Executor 只输出结构化诊断材料 |
| 阶段二 | Gatekeeper 接入 | 在 `VerifierInputHook` 中加入 Gatekeeper,写入 payload 和审计 | 伪造 id、工具名不匹配、schema 退化可被拦截 |
| 阶段三 | Verifier V2 | 改 `chat-verifier-prompt.md`,从 `facts_checked` 转向 `claim_checks` | Verifier 判断可推导性,不再逐字抽自然语言事实 |
| 阶段四 | Composer | 新增 Composer prompt/调用,由 ChatService 过滤输入 | 最终答案只使用 Verifier 允许的材料 |
| 阶段五 | 回归与审计 | 扩测试和 eval fixtures | 证明幻觉拦截、生效路径、审计回溯都可验证 |
| 阶段 | 名称 | 目标 | 状态 | 退出条件 |
|---|---|---|---|---|
| 1 | Executor V2 输出契约 | 移除 Executor 最终表达字段,只输出结构化诊断材料 | 已完成并归档 | `executor_evidence_v2` 可运行,最终用户不再看到 raw JSON |
| 2 | Gatekeeper 接入 VerifierInputHook | 在 Verifier 前加入确定性拦截与审计 | 进行中 | `gatekeeper_result` 进入 Verifier payload 和 `self_evaluation` |
| 3 | Verifier V2 可推导性校验 | 从 `facts_checked` 转向 `claim_checks`,判断 claims 是否可由证据推出 | 未开始 | Verifier 不再从自然语言答案抽取额外事实 |
| 4 | Composer 最终表达 | 由 Composer 根据 Verifier 允许材料生成最终用户答案 | 未开始 | PASS/LOW_CONFID/REJECT 都不读取 Executor `user_facing_answer` |
| 5 | 回归评测与审计闭环 | 用测试和 eval fixtures 证明幻觉拦截链路有效 | 未开始 | 伪造 ID、张冠李戴、hypothesis 写成事实等场景都有回归覆盖 |
更详细的实施拆解见 `21. 实施阶段`。
---
## 0.5 非目标
## 0.6 范围与非目标
### In scope
- 调整 Executor 输出契约,从 `executor_evidence_v1` 演进到 `executor_evidence_v2`。
- 在 `VerifierInputHook` 中接入 Gatekeeper。
- 将 Gatekeeper 结果写入 Verifier payload 和 `diagnosis_session.self_evaluation`。
- 将 Verifier 主输出从 `facts_checked` 迁移到 `claim_checks`,兼容期保留旧字段。
- 新增 Composer 表达层,最终用户答案只来自 Verifier 允许材料。
- 补齐关键回归测试和 eval fixtures。
### Out of scope
本 issue 第一版明确不做以下事情:
@@ -154,7 +221,7 @@ Executor raw JSON
---
## 0.6 关键设计决策
## 0.7 关键设计决策
| 决策 | 结论 | 原因 |
|---|---|---|
@@ -168,7 +235,7 @@ Executor raw JSON
---
## 0.7 实现约束
## 0.8 实现约束
实现时必须遵守:
@@ -181,7 +248,40 @@ Executor raw JSON
---
## 0.8 推荐实现切片
## 0.9 OpenSpec / devflow 落地要求
这个 issue 后续按 OpenSpec-first 的方式实施。每个阶段都必须留下可审计痕迹:
```text
OpenSpec proposal/design/spec/tasks
-> 实现代码
-> 最小验证
-> archive 到 openspec/changes/archive
-> 回填 devflow/projects
-> git commit
```
阶段交付物:
| 交付物 | 要求 |
|---|---|
| OpenSpec change | 每个阶段一个独立 change,不能复用上个阶段的 active change |
| tests | 至少覆盖该阶段的新增失败路径和主成功路径 |
| devflow | 记录 brief、decisions、evidence、acceptance |
| commit | 每个阶段单独提交,提交前确认 diff 只包含本阶段内容 |
推荐验证命令:
```powershell
mvn "-Dtest=VerifierInputHookTest,ChatServiceSequentialAgentTest" test
cmd /c openspec validate --specs
```
如阶段新增专门测试,应把测试类加入 Maven `-Dtest` 列表。
---
## 0.10 推荐实现切片
建议不要把所有改动塞进一个大 PR。推荐拆成以下可独立验收的切片:
@@ -1305,8 +1405,18 @@ Composer 输出严格 JSON。
## 21. 实施阶段
每个阶段都必须作为独立 OpenSpec change 落地。后续 agent 不应直接从本节复制代码实现,而应把本节转成该阶段的 `proposal.md`、`design.md`、`spec.md` 和 `tasks.md`。
### 阶段一:Executor V2 输出契约
状态:已完成并归档。
已完成记录:
- OpenSpec archive:`openspec/changes/archive/2026-07-07-executor-v2-output-contract`
- devflow:`devflow/projects/2026-07-07-executor-v2-output-contract`
- commit:`050cbc8 feat(agent): add executor evidence v2 contract`
目标:先让 Executor 不再输出最终诊断话术,只输出结构化诊断材料。
改动范围:
@@ -1323,17 +1433,40 @@ Composer 输出严格 JSON。
- `claims[].evidence_bindings` 仍要求非空。
- 现有流程即使尚未接入 Composer,也不会把 Executor raw JSON 直接当最终答案泄露给用户。
已知阶段性债务:
- `ChatService` 仍有临时 V2 renderer,用于 Composer 上线前避免 raw JSON 外泄。
- Verifier 仍以 `facts_checked` 为主,`claim_checks` 等待阶段三。
### 阶段二:Gatekeeper 接入 VerifierInputHook
状态:进行中。
当前 OpenSpec change:
```text
openspec/changes/executor-gatekeeper-hook
```
目标:在 Verifier 之前用确定性规则拦截物理级幻觉。
改动范围:
- 新增 Gatekeeper 规则接口和校验服务。
- 新增规则索引与元数据配置。
- 新增 Gatekeeper 校验服务,第一版先覆盖必要规则。
- 在 `VerifierInputHook` 中调用 Gatekeeper。
- 将 `gatekeeper_result` 写入 Verifier payload。
- 将 `gatekeeper_result` 持久化到 `self_evaluation.verifier_evaluation`。
- 更新 `chat-verifier-prompt.md`,明确 Gatekeeper fail 时不得 PASS。
第一版规则范围:
| 规则 | 必须实现 | 说明 |
|---|---:|---|
| `schema.executor_v2` | 是 | 校验 V2 基础字段、移除字段、数组字段 |
| `evidence.invocation_ref` | 是 | 校验 invocation id 存在于当前 session 且工具名匹配 |
| `evidence.excerpt_similarity` | 否 | 可作为后续阶段或 warn-only 增强 |
| `claim.hallucination_phrases` | 否 | 可作为后续配置化规则增强 |
| `claim.evidence_utilization` | 否 | 第一版不建议硬启用,避免中文分词误杀 |
验收标准:
@@ -1342,9 +1475,32 @@ Composer 输出严格 JSON。
- `tool_name` 与真实 invocation 不匹配时,Gatekeeper fail。
- 缺省 `hypotheses`、`recommended_actions`、`missing_info` 时,传给 Verifier 前会 normalize 为 `[]`。
- 旧的 `VerifierInputHook` 结构校验不再和 Gatekeeper 重复维护。
- `diagnosis_session.self_evaluation.verifier_evaluation.gatekeeper_result` 可回溯到本轮校验结果。
建议测试:
- `ExecutorGatekeeperServiceTest`
- `VerifierInputHookTest`
- `ChatServiceSequentialAgentTest`
建议验证命令:
```powershell
mvn "-Dtest=ExecutorGatekeeperServiceTest,VerifierInputHookTest,ChatServiceSequentialAgentTest" test
cmd /c openspec validate executor-gatekeeper-hook
cmd /c openspec validate --specs
```
阶段退出条件:
- OpenSpec change 已 archive。
- devflow 已回填 brief、evidence、decisions、acceptance。
- 本阶段代码和文档已单独 commit。
### 阶段三:Verifier V2 可推导性校验
状态:未开始。
目标:Verifier 不再逐字扫描自然语言,而是校验 claim 是否能由证据合理推出。
改动范围:
@@ -1355,6 +1511,13 @@ Composer 输出严格 JSON。
- 兼容期继续输出或由代码生成 `facts_checked`。
- `parseVerifierDecision(...)` 能处理 `claim_checks` 和旧 `facts_checked`。
核心设计:
- Verifier 主校验对象是 `executor_structured_output.claims`。
- `executor_raw_output` / `executor_final_answer` 只作为 debug/fallback 字段,不作为新增事实来源。
- Verifier 不要求 claim 与证据逐字一致,而是判断是否可以合理推出。
- Gatekeeper fail 时,Verifier 不允许输出 `PASS`。
验收标准:
- `direct_observation`、`reasonable_inference`、`overstated`、`unsupported`、`external_unknown`、`contradicted` 均有测试覆盖。
@@ -1362,8 +1525,23 @@ Composer 输出严格 JSON。
- malformed/missing Executor 输出不回退自然语言抽事实,直接进入 `LOW_CONFID`。
- `facts_checked` 兼容字段能继续支撑现有低置信模板、retry_context 和评测用例。
建议测试重点:
- `parseVerifierDecision(...)` 能解析 `claim_checks`。
- `facts_checked` 能从 `claim_checks` 兼容映射。
- `gatekeeper_result.status=fail` + Verifier 返回 PASS 时,代码侧应降级或测试 prompt 禁止该行为。
- `unsupported` / `overstated` 能进入低置信模板需要的 missing evidence 语义。
阶段退出条件:
- Verifier prompt 已不再要求逐字扫描最终自然语言答案。
- 审计中同时可见 `claim_checks` 和兼容 `facts_checked`。
- 现有低置信重试链路不回归。
### 阶段四:Composer 输出最终答案
状态:未开始。
目标:把最终用户表达从 Executor 中移出,由 Composer 基于 Verifier 允许的材料生成。
改动范围:
@@ -1373,6 +1551,14 @@ Composer 输出严格 JSON。
- Composer 只接收 `allowed_claims`、`allowed_hypotheses`、`missing_info`、`recommended_actions`、`rationale`。
- PASS/LOW_CONFID/REJECT 均不再读取 Executor 的 `user_facing_answer`。
核心设计:
- Composer 是表达层,不是诊断层。
- Composer 不接收 raw tool output。
- Composer 不接收未经筛选的完整 Executor output。
- ChatService 负责根据 `claim_checks` 过滤出 `allowed_claims`。
- `overstated` claim 不得作为确认事实输出,可降级为 hypothesis 或 missing_info。
验收标准:
- Composer 输出严格 JSON,包含 `answer_summary`、`recommended_actions`、`user_facing_answer`。
@@ -1380,9 +1566,26 @@ Composer 输出严格 JSON。
- `REJECT` 时 `allowed_hypotheses=[]`,最终答案不出现根因结论。
- `LOW_CONFID` 时必须区分已确认信息和可能方向。
- `PASS` 时只有存在 root cause 类型 allowed claim,才允许表达“根因已确认”。
- `ChatService` 不再依赖临时 V2 renderer 生成 PASS 用户答案。
建议测试重点:
- Composer 输入不包含 raw tool output。
- 未通过 Verifier 的 claim 不进入最终答案。
- `LOW_CONFID` 能输出已确认信息、缺口、下一步建议。
- `REJECT` 不输出 root cause 结论。
- Composer malformed 输出时有降级策略,不向用户泄露 raw JSON。
阶段退出条件:
- 最终用户答案唯一来源是 Composer 或固定降级模板。
- Executor 的 `user_facing_answer` 不再参与任何最终答案路径。
- Stage 1 的临时 renderer 被移除或明确只作为关闭 Composer 时的安全降级,不恢复 Executor 表达。
### 阶段五:回归评测与审计闭环
状态:未开始。
目标:确认新链路真的降低证据归因幻觉,而不是只改变字段名。
改动范围:
@@ -1400,6 +1603,22 @@ Composer 输出严格 JSON。
- 最终答案中不再出现未通过 Verifier 的 claim。
- 审计链路能从 final answer 回溯到 Composer 输入、Verifier 判定、Gatekeeper 结果、tool invocation。
建议 fixture 场景:
| 场景 | 预期 |
|---|---|
| 伪造不存在的 invocation id | Gatekeeper fail,最终不可 PASS |
| 引用真实 id 但 tool_name 不匹配 | Gatekeeper fail,倾向 REJECT |
| excerpt 与真实工具输出明显不符 | Gatekeeper fail 或 warn,最终不可 PASS |
| hypothesis 写成 root_cause claim | Verifier 判 `overstated` 或 `unsupported` |
| Composer 输入不含某错误码但输出包含该错误码 | 测试失败 |
阶段退出条件:
- 关键回归测试已进入 CI 可运行测试集合。
- eval fixture 覆盖证据归因幻觉的主路径。
- 文档、OpenSpec、devflow 与实际代码行为一致。
---
## 22. 兼容与回滚策略