feat(agent): add verifier claim checks

This commit is contained in:
aruo
2026-07-08 02:33:02 +08:00
parent c5e496e715
commit 1b31e78be5
19 changed files with 1247 additions and 115 deletions
+1
View File
@@ -8,6 +8,7 @@
| 2026-07-07 | executor-evidence-output-contract | Chat质量门禁/证据归因 | Executor structured output, evidence bindings, Verifier structured claims, LOW_CONFID, hallucination | openspec/changes/archive/2026-07-07-executor-evidence-output-contract | archived | | 2026-07-07 | executor-evidence-output-contract | Chat质量门禁/证据归因 | Executor structured output, evidence bindings, Verifier structured claims, LOW_CONFID, hallucination | openspec/changes/archive/2026-07-07-executor-evidence-output-contract | archived |
| 2026-07-07 | executor-v2-output-contract | Chat质量门禁/证据归因 | executor_evidence_v2, user_facing_answer removal, diagnosis_summary removal, structured renderer | openspec/changes/archive/2026-07-07-executor-v2-output-contract | archived | | 2026-07-07 | executor-v2-output-contract | Chat质量门禁/证据归因 | executor_evidence_v2, user_facing_answer removal, diagnosis_summary removal, structured renderer | openspec/changes/archive/2026-07-07-executor-v2-output-contract | archived |
| 2026-07-07 | executor-gatekeeper-hook | Chat质量门禁/证据归因 | Gatekeeper, verifier payload, source_invocation_ids, tool_name match, self_evaluation | openspec/changes/archive/2026-07-07-executor-gatekeeper-hook | archived | | 2026-07-07 | executor-gatekeeper-hook | Chat质量门禁/证据归因 | Gatekeeper, verifier payload, source_invocation_ids, tool_name match, self_evaluation | openspec/changes/archive/2026-07-07-executor-gatekeeper-hook | archived |
| 2026-07-07 | executor-verifier-claim-checks | Chat质量门禁/证据归因 | Verifier claim_checks, facts_checked compatibility, effective verdict guardrail, malformed output downgrade | openspec/changes/archive/2026-07-07-executor-verifier-claim-checks | archived |
| 2026-07-06 | rag-eval-pipeline-closure | RAG/评测/回归闭环 | lookupResult fixture, LookupKnowledgeTool snapshot, evidenceBlocks, contextPack, retrievalTrace, rerankTrace, baseline diff, fallback case | devflow/projects/2026-07-06-rag-eval-pipeline-closure | archived | | 2026-07-06 | rag-eval-pipeline-closure | RAG/评测/回归闭环 | lookupResult fixture, LookupKnowledgeTool snapshot, evidenceBlocks, contextPack, retrievalTrace, rerankTrace, baseline diff, fallback case | devflow/projects/2026-07-06-rag-eval-pipeline-closure | archived |
| 2026-07-06 | modular-rag-pipeline | RAG/Agent工具/证据链 | modular RAG, lookup_knowledge, evidenceBlocks, contextPack, rerank, retrievalTrace, L0 hint, unfiltered retry | openspec/changes/archive/2026-07-06-modular-rag-pipeline | archived | | 2026-07-06 | modular-rag-pipeline | RAG/Agent工具/证据链 | modular RAG, lookup_knowledge, evidenceBlocks, contextPack, rerank, retrievalTrace, L0 hint, unfiltered retry | openspec/changes/archive/2026-07-06-modular-rag-pipeline | archived |
| 2026-07-05 | mvp-demo-interview-runbook | MVP Demo/Interview | Plan C, payment timeout, runbook, trace checklist, demo script | openspec/changes/archive/2026-07-05-mvp-demo-interview-runbook | archived | | 2026-07-05 | mvp-demo-interview-runbook | MVP Demo/Interview | Plan C, payment timeout, runbook, trace checklist, demo script | openspec/changes/archive/2026-07-05-mvp-demo-interview-runbook | archived |
@@ -0,0 +1,58 @@
# Acceptance
## Implementation Result
Implemented stage three of Executor Structured Output V2:
- Verifier prompt now validates claim derivability rather than scanning final natural-language output.
- Verifier output supports `claim_checks`.
- `ChatService` derives compatibility `facts_checked` from `claim_checks`.
- `ChatService` persists both `claim_checks` and `facts_checked`.
- Effective verdict guardrails prevent Gatekeeper failures and malformed structured output from remaining `PASS`.
- Verifier logging summarizes `claim_checks`.
## Static Verification
- Reviewed `git diff --stat` and changed files are scoped to stage three implementation, tests, OpenSpec/devflow, and the issue handoff document.
- `cmd /c openspec validate executor-verifier-claim-checks` passed.
- After OpenSpec archive, `cmd /c openspec validate --specs` passed.
## Script Verification
Passed:
```powershell
mvn "-Dtest=ExecutorGatekeeperServiceTest,VerifierInputHookTest,ChatServiceSequentialAgentTest" test
```
Coverage:
- Verifier payload and Gatekeeper hook behavior.
- Gatekeeper schema/invocation validation.
- `claim_checks` parsing and persistence.
- `claim_checks` to `facts_checked` compatibility mapping.
- all claim verification mapping classes.
- Gatekeeper failure downgrade from model `PASS`.
- malformed Executor output downgrade from model `PASS`.
The targeted Maven test command was re-run after OpenSpec archive and passed.
## Browser / Manual Verification
Not run. This stage changes backend prompt, parser, audit, and tests only.
## OpenSpec Archive Status
Archived:
```text
openspec/changes/archive/2026-07-07-executor-verifier-claim-checks
```
Archive follow-up: the generated canonical `chat-verifier-agent` spec was reviewed and amended to preserve pre-existing verifier input and Gatekeeper schema scenarios while adding the new claim-check scenarios.
## Remaining Risks
- Composer is not implemented in this stage; final PASS rendering still uses the temporary V2 renderer until stage four.
- Full eval fixture expansion is deferred to stage five.
- Existing Maven warnings about duplicate test dependency and Lombok builder defaults remain outside this stage.
@@ -0,0 +1,41 @@
# Executor Verifier Claim Checks
## Background
Stage one moved Chat Executor to `executor_evidence_v2`, and stage two added deterministic Gatekeeper checks before Verifier. After those stages, Verifier still primarily used the legacy `facts_checked` contract and could still treat `executor_final_answer` as a fact source.
That left two risks:
- Verifier could still extract extra confirmed facts from natural-language Executor output.
- Downstream audit and retry consumers could not distinguish V2 claim-level verification from legacy fact checks.
## Goal
Make Verifier V2 claim-oriented:
- verify `executor_structured_output.claims` as the primary target;
- emit `claim_checks` as the authoritative V2 result;
- keep `facts_checked` only as a compatibility projection;
- enforce code-side guardrails so Gatekeeper failures or malformed structured output cannot remain effective `PASS`.
## Scope
- Updated `chat-verifier-prompt.md` to frame verification as claim derivability.
- Extended `ChatService` to parse, normalize, map, and persist `claim_checks`.
- Added effective verdict guardrails for Gatekeeper failure and malformed/missing Executor structured output.
- Updated verifier logging summaries to count `claim_checks`.
- Updated sequential workflow tests to cover claim mapping and downgrade behavior.
## Non-Goals
- No Composer integration in this phase.
- No final-answer material filtering beyond existing templates and temporary V2 renderer.
- No Executor retry behavior change.
- No Gatekeeper rule expansion.
- No database schema migration.
## OpenSpec
- Active change before archive: `openspec/changes/executor-verifier-claim-checks`
- Capability: `chat-verifier-agent`
- Scale: standard
@@ -0,0 +1,57 @@
# Decisions
## Scope Decision
Stage three is limited to Verifier V2 claim checks. Composer is explicitly deferred to stage four.
Reason: Composer requires stable verifier output and allowed-material filtering; mixing it into this stage would make rollback and acceptance unclear.
## Contract Decision
`claim_checks` is the authoritative V2 verifier output.
`facts_checked` remains as a compatibility projection generated from `claim_checks` when present.
Reason: existing low-confidence rendering, retry context, trace output, and evaluation code still depend on `facts_checked`.
## Mapping Decision
Claim verification maps to legacy facts as follows:
| claim verification | legacy facts_checked verification |
|---|---|
| `direct_observation` | `direct_evidence` |
| `reasonable_inference` | `indirect_support` |
| `overstated` | `indirect_support` |
| `unsupported` | `no_evidence` |
| `external_unknown` | `no_evidence` |
| `contradicted` | `contradicted` |
## Guardrail Decision
Effective verdict is enforced in code:
- `gatekeeper_result.status=fail` cannot remain `PASS`.
- `evidence.invocation_ref` failure downgrades to `REJECT`.
- Other Gatekeeper failures downgrade at least to `LOW_CONFID`.
- missing/malformed Executor structured output cannot remain `PASS`.
Reason: prompt compliance is not deterministic enough for safety-critical evidence attribution.
## Apply Fix Record
Initial targeted Maven verification failed because older tests expected PASS to return Executor natural-language output or V1 `user_facing_answer`.
Classification: test drift from the committed OpenSpec, not a design blocker.
Resolution: update tests to use valid Executor V2 output for PASS paths and assert downgrade behavior for malformed or Gatekeeper-failed outputs.
## Interface Impact
L2 internal contract extension:
- Verifier output gains `claim_checks`.
- Existing `facts_checked` remains available.
- Persistence JSON gains `claim_checks` under existing `self_evaluation`.
No external API or database schema changes.
@@ -0,0 +1,35 @@
# Evidence
## Relevant History
- `executor-v2-output-contract`: Executor emits `executor_evidence_v2` and no longer emits final-expression fields.
- `executor-gatekeeper-hook`: Gatekeeper validates schema and invocation references before Verifier and persists `gatekeeper_result`.
- `chat-verifier-agent`: Existing Verifier used `facts_checked`, low-confidence rendering, retry context, and verifier audit.
## Code Evidence
- `src/main/resources/prompts/chat-verifier-prompt.md`: Verifier prompt now makes `executor_structured_output.claims` primary and treats `executor_final_answer` as debug/fallback only.
- `src/main/java/com/superbiz/agent/service/ChatService.java`: parses `claim_checks`, maps them to compatibility `facts_checked`, persists both, and applies effective verdict guardrails.
- `src/main/java/com/superbiz/agent/hook/AgentLoggingHook.java`: verifier thought summaries now include `claim_checks`.
- `src/test/java/com/superbiz/agent/service/ChatServiceSequentialAgentTest.java`: covers claim-check mapping, Gatekeeper downgrade, malformed output downgrade, and V2 PASS paths.
## Evidence-Driven Conclusions
- `facts_checked` cannot be removed yet because existing low-confidence templates, retry context, trace tooling, and eval paths still consume it.
- Prompt-only prevention is insufficient for Gatekeeper failures; `ChatService` must enforce effective verdict downgrades in code.
- No database schema migration is needed because `claim_checks` is persisted inside existing `diagnosis_session.self_evaluation`.
- Composer remains stage four and must not be mixed into this stage.
## Verification Evidence
Script verification passed:
```powershell
mvn "-Dtest=ExecutorGatekeeperServiceTest,VerifierInputHookTest,ChatServiceSequentialAgentTest" test
cmd /c openspec validate executor-verifier-claim-checks
```
Known existing warnings:
- Maven reports duplicate `spring-boot-starter-test` dependency in `pom.xml`.
- Existing Lombok `@Builder` default warnings remain.
+109 -13
View File
@@ -1,10 +1,10 @@
# Executor Structured Output V2 实施 Issue # Executor Structured Output V2 可执行设计与实施 Issue
**状态**:分阶段实施中 **状态**:阶段三实施中
**严重程度**:高 **严重程度**:高
**创建日期**:2026-07-07 **创建日期**:2026-07-07
**最后更新**:2026-07-08 **最后更新**:2026-07-08
**文档类型**:可执行 issue / 分阶段实施说明 **文档类型**:可执行设计 issue / 分阶段实施说明
**范围**:Chat Executor 输出结构、Gatekeeper、Verifier、Composer 数据契约调整 **范围**:Chat Executor 输出结构、Gatekeeper、Verifier、Composer 数据契约调整
**关联问题**: **关联问题**:
@@ -17,7 +17,25 @@
## 0. 给后续实现 Agent 的执行入口 ## 0. 给后续实现 Agent 的执行入口
这份文档不是单纯的数据结构草案,而是 `Executor Structured Output V2` 的分阶段实施 issue。后续 agent 接手时,应先理解问题背景和阶段边界,再进入具体字段定义。 这份文档是 `Executor Structured Output V2` 的可执行设计 issue,不是单纯的数据结构草案。后续 agent 接手时,先按本节确认背景、当前阶段、执行纪律和验收边界,再进入后面的字段契约。
### 0.0 当前接手快照
截至 2026-07-08,阶段状态如下:
| 阶段 | 状态 | 记录 |
|---|---|---|
| 阶段一:Executor V2 输出契约 | 已完成、已归档、已提交 | `050cbc8 feat(agent): add executor evidence v2 contract` |
| 阶段二:Gatekeeper 接入 VerifierInputHook | 已完成、已归档、已提交 | `c5e496e feat(agent): add executor gatekeeper hook` |
| 阶段三:Verifier V2 可推导性校验 | 实施中 | active change:`openspec/changes/executor-verifier-claim-checks` |
| 阶段四:Composer 输出最终答案 | 未开始 | 等阶段三归档并提交后再启动 |
| 阶段五:回归评测与审计闭环 | 未开始 | 等 Composer 链路稳定后补齐 |
接手时的第一动作:
1. 先运行 `git status --short`,确认是否已有阶段三未提交改动。
2. 如果 `openspec/changes/executor-verifier-claim-checks` 仍存在,继续阶段三,不要跳到 Composer。
3. 阶段三完成后,必须先验证、归档 OpenSpec、回填 devflow、单独 commit,再进入阶段四。
推荐阅读顺序: 推荐阅读顺序:
@@ -42,6 +60,13 @@
- 验收失败如果是代码问题,agent 自行修复;如果是设计决策不明确,停下来问用户。 - 验收失败如果是代码问题,agent 自行修复;如果是设计决策不明确,停下来问用户。
- 本 issue 不要求一次性完成所有阶段;后续 agent 应从当前 git/OpenSpec/devflow 状态继续推进。 - 本 issue 不要求一次性完成所有阶段;后续 agent 应从当前 git/OpenSpec/devflow 状态继续推进。
交付纪律:
- 每个阶段只解决该阶段的问题,不顺手做下一阶段。
- 每个阶段至少要有主成功路径和新增失败路径测试。
- 提交前必须确认 diff 只包含当前阶段内容。
- 不提交、不推送,除非用户明确要求;本 issue 默认只指导阶段实现和本地提交。
--- ---
## 0.1 背景 ## 0.1 背景
@@ -187,13 +212,25 @@ Executor raw JSON
| 阶段 | 名称 | 目标 | 状态 | 退出条件 | | 阶段 | 名称 | 目标 | 状态 | 退出条件 |
|---|---|---|---|---| |---|---|---|---|---|
| 1 | Executor V2 输出契约 | 移除 Executor 最终表达字段,只输出结构化诊断材料 | 已完成并归档 | `executor_evidence_v2` 可运行,最终用户不再看到 raw JSON | | 1 | Executor V2 输出契约 | 移除 Executor 最终表达字段,只输出结构化诊断材料 | 已完成并归档 | `executor_evidence_v2` 可运行,最终用户不再看到 raw JSON |
| 2 | Gatekeeper 接入 VerifierInputHook | 在 Verifier 前加入确定性拦截与审计 | 进行中 | `gatekeeper_result` 进入 Verifier payload 和 `self_evaluation` | | 2 | Gatekeeper 接入 VerifierInputHook | 在 Verifier 前加入确定性拦截与审计 | 已完成并归档 | `gatekeeper_result` 进入 Verifier payload 和 `self_evaluation` |
| 3 | Verifier V2 可推导性校验 | 从 `facts_checked` 转向 `claim_checks`,判断 claims 是否可由证据推出 | 未开始 | Verifier 不再从自然语言答案抽取额外事实 | | 3 | Verifier V2 可推导性校验 | 从 `facts_checked` 转向 `claim_checks`,判断 claims 是否可由证据推出 | 实施中 | Verifier 不再从自然语言答案抽取额外事实 |
| 4 | Composer 最终表达 | 由 Composer 根据 Verifier 允许材料生成最终用户答案 | 未开始 | PASS/LOW_CONFID/REJECT 都不读取 Executor `user_facing_answer` | | 4 | Composer 最终表达 | 由 Composer 根据 Verifier 允许材料生成最终用户答案 | 未开始 | PASS/LOW_CONFID/REJECT 都不读取 Executor `user_facing_answer` |
| 5 | 回归评测与审计闭环 | 用测试和 eval fixtures 证明幻觉拦截链路有效 | 未开始 | 伪造 ID、张冠李戴、hypothesis 写成事实等场景都有回归覆盖 | | 5 | 回归评测与审计闭环 | 用测试和 eval fixtures 证明幻觉拦截链路有效 | 未开始 | 伪造 ID、张冠李戴、hypothesis 写成事实等场景都有回归覆盖 |
更详细的实施拆解见 `21. 实施阶段`。 更详细的实施拆解见 `21. 实施阶段`。
阶段推进顺序必须串行:
```text
阶段一 archive + commit
-> 阶段二 archive + commit
-> 阶段三 archive + commit
-> 阶段四 archive + commit
-> 阶段五 archive + commit
```
不能在阶段三未归档提交时启动 Composer,也不能在 Composer 未稳定时把回归评测阶段提前做成大杂烩。
--- ---
## 0.6 范围与非目标 ## 0.6 范围与非目标
@@ -279,6 +316,22 @@ cmd /c openspec validate --specs
如阶段新增专门测试,应把测试类加入 Maven `-Dtest` 列表。 如阶段新增专门测试,应把测试类加入 Maven `-Dtest` 列表。
后续 agent 每个阶段的固定执行清单:
1. 确认当前阶段:读取本 issue、`git status --short`、`openspec status`。
2. 确认 OpenSpec:没有 active change 时先创建;已有当前阶段 active change 时继续使用。
3. 实现阶段内代码:只改该阶段要求的 prompt/service/hook/test,不夹带下一阶段。
4. 验证:运行该阶段建议测试和 `cmd /c openspec validate ...`。
5. 回填任务:更新 OpenSpec `tasks.md`、`decisions.md`,记录实际验证命令和结果。
6. 归档:创建或更新 `devflow/projects/<date>-<change-id>/`,然后 `cmd /c openspec archive <change-id> -y`。
7. 复验:归档后再次运行最小测试和 `cmd /c openspec validate --specs`。
8. 提交:检查 `git diff --cached --stat`,确认只包含当前阶段,再单独 commit。
若步骤 4 或 7 失败:
- 代码或测试问题:自行修复并重新验证。
- 阶段边界或协议设计问题:停止,向用户说明阻塞点,不进入下一阶段。
--- ---
## 0.10 推荐实现切片 ## 0.10 推荐实现切片
@@ -297,6 +350,27 @@ cmd /c openspec validate --specs
--- ---
## 0.11 为什么这样拆阶段
本 issue 的改造目标不是一次性重写 Chat 诊断链路,而是逐步切断“证据不足但自然语言先下结论”的通道。阶段拆分按风险来源分层:
| 阶段 | 切断的风险 | 为什么不能合并 |
|---|---|---|
| 阶段一 | Executor 提前输出最终诊断话术 | 先移除污染源,否则后面 Verifier/Composer 仍会被旧答案影响 |
| 阶段二 | 伪造 ID、工具名不匹配、非法 schema 等物理级幻觉 | 这类问题不需要 LLM 判断,先用代码低成本拦截 |
| 阶段三 | claim 与证据之间是否可推导 | 这是 Verifier 的语义职责,必须在 Gatekeeper 之后处理 |
| 阶段四 | 最终用户答案夹带未验证事实 | 只有 Verifier 输出稳定后,Composer 才知道哪些材料可用 |
| 阶段五 | 新链路是否真的防住历史幻觉场景 | 等链路完整后再做系统化回归,避免测试绑死临时实现 |
每个阶段的设计都必须满足两个条件:
- 可以独立验证,不依赖后续阶段已经完成。
- 失败时可以独立回滚,不破坏已经归档提交的前置阶段。
因此后续实现时不要把“顺手移除临时 renderer”“顺手扩展 Gatekeeper 规则”“顺手接 Composer”混进阶段三。阶段三只解决 Verifier `claim_checks` 和有效 verdict guardrail。
---
## 1. 设计目标 ## 1. 设计目标
当前 `executor_evidence_v1` 同时包含结构化诊断材料和自然语言表达字段: 当前 `executor_evidence_v1` 同时包含结构化诊断材料和自然语言表达字段:
@@ -1440,13 +1514,13 @@ Composer 输出严格 JSON。
### 阶段二:Gatekeeper 接入 VerifierInputHook ### 阶段二:Gatekeeper 接入 VerifierInputHook
状态:进行中。 状态:已完成并归档。
当前 OpenSpec change: 已完成记录:
```text - OpenSpec archive:`openspec/changes/archive/2026-07-07-executor-gatekeeper-hook`
openspec/changes/executor-gatekeeper-hook - devflow:`devflow/projects/2026-07-07-executor-gatekeeper-hook`
``` - commit:`c5e496e feat(agent): add executor gatekeeper hook`
目标:在 Verifier 之前用确定性规则拦截物理级幻觉。 目标:在 Verifier 之前用确定性规则拦截物理级幻觉。
@@ -1487,7 +1561,6 @@ openspec/changes/executor-gatekeeper-hook
```powershell ```powershell
mvn "-Dtest=ExecutorGatekeeperServiceTest,VerifierInputHookTest,ChatServiceSequentialAgentTest" test mvn "-Dtest=ExecutorGatekeeperServiceTest,VerifierInputHookTest,ChatServiceSequentialAgentTest" test
cmd /c openspec validate executor-gatekeeper-hook
cmd /c openspec validate --specs cmd /c openspec validate --specs
``` ```
@@ -1499,7 +1572,19 @@ cmd /c openspec validate --specs
### 阶段三:Verifier V2 可推导性校验 ### 阶段三:Verifier V2 可推导性校验
状态:未开始。 状态:实施中。
当前 OpenSpec change:
```text
openspec/changes/executor-verifier-claim-checks
```
接手说明:
- 如果该 active change 仍存在,继续完成阶段三,不要启动阶段四。
- 阶段三已有 OpenSpec proposal/design/spec/tasks,后续实现应先对照 `tasks.md` 逐项完成。
- 阶段三完成后必须 archive、回填 devflow、单独 commit,推荐提交信息:`feat(agent): add verifier claim checks`。
目标:Verifier 不再逐字扫描自然语言,而是校验 claim 是否能由证据合理推出。 目标:Verifier 不再逐字扫描自然语言,而是校验 claim 是否能由证据合理推出。
@@ -1532,11 +1617,22 @@ cmd /c openspec validate --specs
- `gatekeeper_result.status=fail` + Verifier 返回 PASS 时,代码侧应降级或测试 prompt 禁止该行为。 - `gatekeeper_result.status=fail` + Verifier 返回 PASS 时,代码侧应降级或测试 prompt 禁止该行为。
- `unsupported` / `overstated` 能进入低置信模板需要的 missing evidence 语义。 - `unsupported` / `overstated` 能进入低置信模板需要的 missing evidence 语义。
建议验证命令:
```powershell
mvn "-Dtest=ExecutorGatekeeperServiceTest,VerifierInputHookTest,ChatServiceSequentialAgentTest" test
cmd /c openspec validate executor-verifier-claim-checks
cmd /c openspec validate --specs
```
阶段退出条件: 阶段退出条件:
- Verifier prompt 已不再要求逐字扫描最终自然语言答案。 - Verifier prompt 已不再要求逐字扫描最终自然语言答案。
- 审计中同时可见 `claim_checks` 和兼容 `facts_checked`。 - 审计中同时可见 `claim_checks` 和兼容 `facts_checked`。
- 现有低置信重试链路不回归。 - 现有低置信重试链路不回归。
- OpenSpec change 已 archive。
- devflow 已回填 brief、evidence、decisions、acceptance。
- 本阶段代码和文档已单独 commit。
### 阶段四:Composer 输出最终答案 ### 阶段四:Composer 输出最终答案
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-07-07
@@ -0,0 +1,110 @@
# Decisions: executor-verifier-claim-checks
## sm-flow Progress
### Clarify
Entry summary: implement stage three of Executor Structured Output V2 by making Verifier V2 claim-oriented.
Slug: `executor-verifier-claim-checks`
Scale: standard. This changes internal verifier output/audit contracts and parsing logic, but does not change external APIs or database schema.
### Context
Relevant history:
- `executor-v2-output-contract`: Executor emits `executor_evidence_v2` and no longer emits final-expression fields.
- `executor-gatekeeper-hook`: Gatekeeper validates schema and invocation references before Verifier and persists `gatekeeper_result`.
- `chat-verifier-agent`: Current Verifier still primarily uses `facts_checked`.
Current code shape:
- `chat-verifier-prompt.md` still frames the task around `executor_final_answer` and `facts_checked`.
- `ChatService.parseVerifierDecision(...)` only parses `facts_checked`.
- `buildLowConfidenceOutput(...)` and `buildRetryContext(...)` consume `VerifierDecision.factsChecked()`.
- `persistVerifierEvaluation(...)` already persists parse status, structured output, trace summary, and gatekeeper result.
- `AgentLoggingHook` summarizes verifier output using `facts_checked`.
### Grill
Question pool:
| Question | Mode | Resolution |
|---|---|---|
| Should Verifier still scan `executor_final_answer` when structured output is valid? | evidence-driven | No. Stage three explicitly makes claims the primary target and raw output debug/fallback only. |
| Should `facts_checked` be removed now? | evidence-driven | No. It remains a compatibility projection for low-confidence templates, retry context, eval, and trace tooling. |
| Should Gatekeeper fail prevention be prompt-only? | evidence-driven | No. Stage two noted prompt compliance is not deterministic; stage three adds code-side effective verdict guard. |
| Does this require database migration? | evidence-driven | No. `claim_checks` is stored under existing JSON self_evaluation. |
| Does this introduce Composer? | evidence-driven | No. Composer is stage four. |
No user-interview questions are open for this stage.
### Specify
OpenSpec artifacts:
- `proposal.md`: why and scope.
- `design.md`: Verifier V2 output, compatibility mapping, verdict guardrails, risks.
- `specs/chat-verifier-agent/spec.md`: observable requirements for claim checks, compatibility facts, persistence, and guardrails.
- `tasks.md`: executable implementation and verification checklist.
### Audit
Architecture risk summary:
- This is an L2 internal verifier contract extension.
- Existing consumers continue using `facts_checked`, which is now generated from `claim_checks` when present.
- Gatekeeper PASS prevention becomes deterministic in code, reducing reliance on prompt compliance.
- No external API or database schema changes are introduced.
Cross-artifact alignment:
| Source | Target | Status |
|---|---|---|
| issue stage three | proposal | aligned |
| proposal scope / non-goals | design | aligned |
| design output contract and guardrails | specs | aligned |
| specs observable behavior | tasks | aligned |
Interface impact:
- Verifier output: L2 internal extension with `claim_checks`.
- Persistence JSON: L2 internal audit extension in existing `self_evaluation`.
- External HTTP/API behavior: unchanged.
### Commit
Commit gate result: passed.
- `proposal.md`, `design.md`, `specs/chat-verifier-agent/spec.md`, and `tasks.md` exist.
- `cmd /c openspec validate executor-verifier-claim-checks` passed.
- `cmd /c openspec status --change executor-verifier-claim-checks` reports 4/4 artifacts complete.
- No unresolved user-interview questions remain for this stage.
### Apply
Capability source: OpenSpec CLI + `openspec-apply-change` protocol, executed through the local shell tool. No separate semantic/LSP tools are available in this session, so implementation evidence used OpenSpec artifacts, `rg`/diff inspection, and targeted tests.
Implemented changes:
- Updated `chat-verifier-prompt.md` so `executor_structured_output.claims` is the primary verification target.
- Added `claim_checks` parsing, normalization, persistence, and compatibility mapping to legacy `facts_checked` in `ChatService`.
- Added code-side effective verdict guardrails:
- missing/malformed structured output cannot remain `PASS`;
- `gatekeeper_result.status=fail` cannot remain `PASS`;
- `evidence.invocation_ref` failures downgrade to `REJECT`;
- other Gatekeeper failures downgrade at least to `LOW_CONFID`.
- Updated verifier logging summaries to account for `claim_checks`.
- Updated sequential workflow tests to use valid Executor V2 output for PASS paths and to verify downgrade paths for Gatekeeper failure and malformed output.
Conflict / fix record:
- Initial targeted Maven verification failed because legacy tests still expected PASS to return Executor natural-language output or V1 `user_facing_answer`.
- Classification: code/test drift from the committed OpenSpec, not a design blocker.
- Resolution: updated tests to assert the stage-three contract: valid V2 structured output may PASS through the temporary renderer, while missing/malformed/V1-style output cannot produce effective PASS through natural-language fallback.
Verification:
- `mvn "-Dtest=ExecutorGatekeeperServiceTest,VerifierInputHookTest,ChatServiceSequentialAgentTest" test` passed.
- `cmd /c openspec validate executor-verifier-claim-checks` passed.
@@ -0,0 +1,140 @@
## Context
Current state after stage two:
```text
chat_planner
-> chat_executor
-> VerifierInputHook + Gatekeeper
-> chat_verifier
-> ChatService final rendering
```
Verifier receives explicit inputs:
- `original_query`
- `executor_final_answer`
- `executor_structured_output`
- `executor_output_parse_status`
- `tool_trace_summary`
- `gatekeeper_result`
- `retry_context`
However, Verifier output is still primarily:
```json
{
"verdict": "PASS",
"groundedness_score": 0.8,
"critical_fact_count": 1,
"facts_checked": [],
"rationale": "..."
}
```
Stage three introduces V2 output while preserving the old compatibility field.
## Verifier V2 Output
Verifier should output:
```json
{
"verdict": "LOW_CONFID",
"groundedness_score": 0.62,
"critical_fact_count": 1,
"claim_checks": [
{
"claim_id": "claim-1",
"claim_text": "payment-service CPU usage is high",
"claim_type": "symptom",
"verification": "direct_observation",
"detail": "query_metrics shows CPU=92%",
"evidence_refs": [
{
"trace_ref": "trace-1",
"tool_name": "query_metrics",
"source_invocation_ids": [394],
"note": "metrics summary contains CPU=92%"
}
]
}
],
"hypothesis_checks": [],
"facts_checked": [],
"rationale": "..."
}
```
`facts_checked` remains for compatibility. If Verifier does not emit it, `ChatService` must derive it from `claim_checks`.
## Claim Verification Set
`claim_checks[].verification` is limited to:
| Value | Meaning | Legacy mapping |
|---|---|---|
| `direct_observation` | Evidence directly observes the claim | `direct_evidence` |
| `reasonable_inference` | Evidence can reasonably support the claim, but not as direct observation | `indirect_support` |
| `overstated` | Evidence partially supports the claim, but the claim says too much | `indirect_support` |
| `unsupported` | Evidence is insufficient | `no_evidence` |
| `external_unknown` | Claim introduces evidence-external entity/value/root cause | `no_evidence` |
| `contradicted` | Claim conflicts with evidence | `contradicted` |
## Compatibility Mapping
`ChatService` must keep old downstream behavior alive by producing `facts_checked`.
Suggested mapping:
```text
facts_checked[].fact = "{claim_id}: {claim_text}"
facts_checked[].is_critical = claim_type in ["root_cause", "symptom", "impact", "risk"]
facts_checked[].verification = mapped legacy verification
facts_checked[].detail = claim_checks[].detail
facts_checked[].evidence_refs = claim_checks[].evidence_refs
```
If Verifier emits both `claim_checks` and `facts_checked`, `claim_checks` is authoritative. `facts_checked` may be replaced by the deterministic compatibility projection to avoid inconsistent audit data.
If Verifier emits only old `facts_checked`, ChatService keeps the old path.
## Verdict Guardrails
Gatekeeper fail:
- If `gatekeeper_result.status = fail`, effective verdict must not be `PASS`.
- If the model returns `PASS`, ChatService should downgrade the effective verdict to `LOW_CONFID` or `REJECT`.
- For this phase, `evidence.invocation_ref` failure should downgrade to `REJECT`; other Gatekeeper failures should downgrade to `LOW_CONFID`.
Malformed or missing structured output:
- If `executor_output_parse_status.status` is `missing` or `malformed`, Verifier should not use natural-language extraction to produce PASS.
- Effective verdict should be `LOW_CONFID`.
## Prompt Boundary
The prompt should say:
- Primary target is `executor_structured_output.claims`.
- Do not extract additional confirmed facts from `executor_final_answer` when structured output is valid.
- `executor_final_answer` is debug/fallback only.
- `claim_checks` is the primary output.
- `facts_checked` is compatibility output.
## Interface Impact
- L2 internal contract extension.
- No external API change.
- No database schema change.
- Audit JSON gains `claim_checks`.
## Risks / Mitigations
- Risk: old low-confidence templates rely on `facts_checked`.
- Mitigation: derive `facts_checked` from `claim_checks`.
- Risk: prompt-only Gatekeeper PASS prevention is insufficient.
- Mitigation: add code-side effective verdict guard.
- Risk: Agent logging only summarizes `facts_checked`.
- Mitigation: update logging to understand `claim_checks` while keeping old summary compatibility.
@@ -0,0 +1,47 @@
## Why
Stage one moved Executor to `executor_evidence_v2`, and stage two added deterministic Gatekeeper checks before Verifier. The Verifier still mainly operates through the legacy `facts_checked` contract and the prompt still allows fallback extraction from `executor_final_answer`.
That keeps two problems alive:
- Verifier can still treat natural-language Executor output as a fact source.
- Downstream code cannot distinguish claim-level verification results from legacy natural-language fact checks.
This phase makes Verifier V2 claim-oriented: Verifier evaluates `executor_structured_output.claims` for whether each claim can be reasonably derived from evidence, emits `claim_checks`, and keeps `facts_checked` only as a compatibility projection.
## What Changes
- Update `chat-verifier-prompt.md` so the primary verification target is `executor_structured_output.claims`.
- Add Verifier V2 output field `claim_checks`.
- Preserve compatibility by mapping `claim_checks` into legacy `facts_checked`.
- Parse and persist `claim_checks` in `ChatService`.
- Ensure `gatekeeper_result.status=fail` cannot result in an effective `PASS`.
- Ensure missing or malformed Executor structured output does not fall back to natural-language fact extraction for PASS.
## Capabilities
### New Capabilities
None.
### Modified Capabilities
- `chat-verifier-agent`: Verifier output now includes claim-level checks and uses claim derivability as the primary groundedness contract.
## Impact
- Affected prompt: `src/main/resources/prompts/chat-verifier-prompt.md`.
- Affected service: `ChatService.parseVerifierDecision(...)`, retry context generation, verifier persistence.
- Affected audit: `diagnosis_session.self_evaluation.verifier_evaluation` gains `claim_checks` and keeps `facts_checked`.
- Affected logging: verifier thought summaries may count `claim_checks`.
- Affected tests: `ChatServiceSequentialAgentTest` and focused verifier parsing tests.
- Database schema: no table or column change.
## Non-Goals
- No Composer in this phase.
- No final-answer material filtering in this phase beyond existing LOW_CONFID/REJECT templates and temporary V2 renderer.
- No Executor retry behavior change.
- No Gatekeeper rule expansion.
- No database schema migration.
@@ -0,0 +1,109 @@
## MODIFIED Requirements
### Requirement: Verifier SHALL fact-check Executor answers
The system SHALL have a Verifier Agent that reads structured Executor claims and the tool call history, then produces a structured verdict based on claim derivability.
#### Scenario: PASS verdict when all claims have evidence
- **WHEN** all critical claims in `executor_structured_output.claims` have direct observation or reasonable inference support in tool call results
- **AND** at least one critical claim has direct observation
- **AND** no critical claim is contradicted, unsupported, external unknown, or overstated
- **AND** `gatekeeper_result.status` is not `fail`
- **THEN** the Verifier MAY output verdict="PASS" with groundedness_score ≥ 0.5
#### Scenario: LOW_CONFID verdict with partial evidence
- **WHEN** no critical claim contradicts the tool results
- **AND** some critical claims are `unsupported`, `external_unknown`, or `overstated`
- **THEN** the Verifier SHALL output verdict="LOW_CONFID"
#### Scenario: LOW_CONFID verdict with only inference support
- **WHEN** no critical claim contradicts the tool results
- **AND** all critical claims are only `reasonable_inference`
- **THEN** the Verifier SHALL output verdict="LOW_CONFID"
#### Scenario: REJECT verdict when claims contradict evidence
- **WHEN** any critical claim in `executor_structured_output.claims` contradicts tool call results
- **OR** the claim fabricates a key entity, error code, or conclusion that does not exist in the tool evidence
- **THEN** the Verifier SHALL output verdict="REJECT"
#### Scenario: Structured Executor claims are verified first
- **WHEN** `executor_structured_output.claims` is present and valid
- **THEN** Verifier SHALL verify each structured claim against `tool_trace_summary` through `claim_checks`
- **AND** each claim's evidence bindings SHALL reference existing trace or invocation identifiers when those identifiers are available
- **AND** a claim with fabricated or missing evidence references SHALL NOT be classified as `direct_observation`
- **AND** Verifier SHALL NOT add extra confirmed facts from `executor_final_answer` that are absent from `executor_structured_output.claims`
#### Scenario: Malformed structured output cannot pass through natural language fallback
- **WHEN** Executor does not return parseable structured output
- **THEN** Verifier SHALL NOT produce an effective `PASS` by extracting facts from `executor_final_answer`
- **AND** the effective verdict SHALL be `LOW_CONFID`
### Requirement: Verifier SHALL output structured JSON
The Verifier SHALL output a JSON object with verdict, groundedness_score, claim_checks array, compatibility facts_checked array, and rationale.
#### Scenario: claim-level verifier output is accepted
- **WHEN** the Verifier checks Executor V2 structured output
- **THEN** the output SHALL contain `verdict`, `groundedness_score`, `critical_fact_count`, `claim_checks`, `facts_checked`, and `rationale`
- **AND** `claim_checks` SHALL be the primary V2 verification result
- **AND** `facts_checked` SHALL remain available for compatibility
### Requirement: facts_checked SHALL use a fixed classification set
The system SHALL continue to expose legacy `facts_checked` using its fixed verification classification set.
#### Scenario: claim checks are mapped to legacy facts
- **WHEN** Verifier output contains `claim_checks`
- **THEN** ChatService SHALL derive compatibility `facts_checked`
- **AND** `direct_observation` SHALL map to `direct_evidence`
- **AND** `reasonable_inference` and `overstated` SHALL map to `indirect_support`
- **AND** `unsupported` and `external_unknown` SHALL map to `no_evidence`
- **AND** `contradicted` SHALL map to `contradicted`
### Requirement: Verifier SHALL be observable
The Verifier's verdict SHALL be persisted for observability.
#### Scenario: claim checks written to self_evaluation
- **WHEN** the Verifier evaluation is persisted
- **THEN** `diagnosis_session.self_evaluation.verifier_evaluation` SHALL include `claim_checks`
- **AND** it SHALL continue to include compatibility `facts_checked`
- **AND** existing fields such as `verdict`, `groundedness_score`, `rationale`, `executor_output_parse_status`, `tool_trace_summary`, and `gatekeeper_result` SHALL be preserved
### Requirement: Verifier SHALL consume explicit verification inputs
The Verifier SHALL receive explicit verification inputs rather than inferring them only from raw conversation history.
#### Scenario: structured claims are the primary verification target
- **WHEN** `executor_output_parse_status.status` is `valid`
- **AND** `executor_structured_output.claims` is available
- **THEN** Verifier SHALL verify each claim through `claim_checks`
- **AND** Verifier SHALL NOT add extra confirmed facts from `executor_final_answer` that are absent from `executor_structured_output.claims`
#### Scenario: malformed structured output cannot pass through natural language fallback
- **WHEN** `executor_output_parse_status.status` is `missing` or `malformed`
- **THEN** Verifier SHALL NOT produce an effective `PASS` by extracting facts from `executor_final_answer`
- **AND** the effective verdict SHALL be `LOW_CONFID`
### Requirement: Executor Gatekeeper SHALL validate deterministic structured-output failures
The system SHALL run deterministic Gatekeeper checks after Executor output parsing and before Verifier model execution.
#### Scenario: gatekeeper fail prevents PASS
- **WHEN** `gatekeeper_result.status` is `fail`
- **AND** the Verifier model returns `verdict = "PASS"`
- **THEN** ChatService SHALL downgrade the effective verdict
- **AND** the effective verdict SHALL NOT be `PASS`
#### Scenario: invocation reference failure downgrades to reject
- **WHEN** `gatekeeper_result.failed_rules` contains `evidence.invocation_ref`
- **AND** the Verifier model returns `verdict = "PASS"`
- **THEN** ChatService SHALL set the effective verdict to `REJECT`
## ADDED Requirements
### Requirement: Verifier claim checks SHALL use a fixed derivability classification set
The Verifier SHALL classify each structured claim using a fixed derivability classification set.
#### Scenario: claim check verification values are constrained
- **WHEN** Verifier emits `claim_checks`
- **THEN** each item SHALL use one of `direct_observation`, `reasonable_inference`, `overstated`, `unsupported`, `external_unknown`, or `contradicted`
#### Scenario: claim check evidence references remain auditable
- **WHEN** Verifier emits `claim_checks`
- **THEN** each claim check SHALL include `claim_id`, `verification`, `detail`, and `evidence_refs`
- **AND** every evidence ref SHALL preserve available `trace_ref`, `tool_name`, and `source_invocation_ids`
@@ -0,0 +1,39 @@
## 1. Prompt Contract
- [x] 1.1 Update `chat-verifier-prompt.md` so `executor_structured_output.claims` is the primary verification target.
- [x] 1.2 Remove the valid-structured-output path that scans `executor_final_answer` for extra confirmed facts.
- [x] 1.3 Add `claim_checks` and optional `hypothesis_checks` to the output contract.
- [x] 1.4 Keep `facts_checked` as compatibility output.
- [x] 1.5 State that missing/malformed structured output cannot produce PASS through natural-language fallback.
## 2. Parser And Compatibility Mapping
- [x] 2.1 Extend `VerifierDecision` to store `claim_checks`.
- [x] 2.2 Parse `claim_checks` from verifier output.
- [x] 2.3 Derive compatibility `facts_checked` from `claim_checks` when present.
- [x] 2.4 Preserve old `facts_checked` parsing when `claim_checks` is absent.
- [x] 2.5 Persist `claim_checks` in verifier evaluation.
## 3. Effective Verdict Guardrails
- [x] 3.1 Add code-side guard so `gatekeeper_result.status=fail` cannot result in effective PASS.
- [x] 3.2 Downgrade `evidence.invocation_ref` failures to REJECT.
- [x] 3.3 Downgrade other Gatekeeper failures to at least LOW_CONFID.
- [x] 3.4 Ensure missing/malformed Executor structured output cannot produce effective PASS.
## 4. Compatibility Consumers
- [x] 4.1 Ensure low-confidence rendering still uses compatibility `facts_checked`.
- [x] 4.2 Ensure `buildRetryContext(...)` still receives evidence gaps from compatibility `facts_checked`.
- [x] 4.3 Update verifier logging summary to account for `claim_checks`.
- [x] 4.4 Preserve existing traceability and Gatekeeper audit fields.
## 5. Tests And Verification
- [x] 5.1 Add or update tests for parsing `claim_checks`.
- [x] 5.2 Add tests for `claim_checks` to `facts_checked` compatibility mapping.
- [x] 5.3 Add tests for each verification mapping class: `direct_observation`, `reasonable_inference`, `overstated`, `unsupported`, `external_unknown`, `contradicted`.
- [x] 5.4 Add tests proving Gatekeeper fail cannot remain PASS.
- [x] 5.5 Add tests proving malformed/missing structured output cannot remain PASS.
- [x] 5.6 Run targeted tests.
- [x] 5.7 Validate this OpenSpec change.
+77 -29
View File
@@ -4,51 +4,54 @@
TBD - created by archiving change chat-verifier-agent. Update Purpose after archive. TBD - created by archiving change chat-verifier-agent. Update Purpose after archive.
## Requirements ## Requirements
### Requirement: Verifier SHALL fact-check Executor answers ### Requirement: Verifier SHALL fact-check Executor answers
The system SHALL have a Verifier Agent that reads the Executor's answer and the tool call history, then produces a structured verdict. The system SHALL have a Verifier Agent that reads structured Executor claims and the tool call history, then produces a structured verdict based on claim derivability.
#### Scenario: PASS verdict when all claims have evidence #### Scenario: PASS verdict when all claims have evidence
- **WHEN** all critical facts in the Executor's answer have direct or indirect support in tool call results - **WHEN** all critical claims in `executor_structured_output.claims` have direct observation or reasonable inference support in tool call results
- **AND** at least one critical fact has direct evidence - **AND** at least one critical claim has direct observation
- **AND** no critical fact is contradicted - **AND** no critical claim is contradicted, unsupported, external unknown, or overstated
- **THEN** the Verifier SHALL output verdict="PASS" with groundedness_score ≥ 0.5 - **AND** `gatekeeper_result.status` is not `fail`
- **THEN** the Verifier MAY output verdict="PASS" with groundedness_score ≥ 0.5
#### Scenario: LOW_CONFID verdict with partial evidence #### Scenario: LOW_CONFID verdict with partial evidence
- **WHEN** no critical fact contradicts the tool results - **WHEN** no critical claim contradicts the tool results
- **AND** some critical facts have no supporting evidence - **AND** some critical claims are `unsupported`, `external_unknown`, or `overstated`
- **THEN** the Verifier SHALL output verdict="LOW_CONFID" - **THEN** the Verifier SHALL output verdict="LOW_CONFID"
#### Scenario: LOW_CONFID verdict with only indirect support #### Scenario: LOW_CONFID verdict with only inference support
- **WHEN** no critical fact contradicts the tool results - **WHEN** no critical claim contradicts the tool results
- **AND** all critical facts are only indirectly supported - **AND** all critical claims are only `reasonable_inference`
- **THEN** the Verifier SHALL output verdict="LOW_CONFID" - **THEN** the Verifier SHALL output verdict="LOW_CONFID"
#### Scenario: REJECT verdict when claims contradict evidence #### Scenario: REJECT verdict when claims contradict evidence
- **WHEN** any critical fact in the Executor's answer contradicts tool call results - **WHEN** any critical claim in `executor_structured_output.claims` contradicts tool call results
- **OR** the answer fabricates a key entity, error code, or conclusion that does not exist in the tool evidence - **OR** the claim fabricates a key entity, error code, or conclusion that does not exist in the tool evidence
- **THEN** the Verifier SHALL output verdict="REJECT" - **THEN** the Verifier SHALL output verdict="REJECT"
#### Scenario: Structured Executor claims are verified first #### Scenario: Structured Executor claims are verified first
- **WHEN** `executor_structured_output.claims` is present and valid - **WHEN** `executor_structured_output.claims` is present and valid
- **THEN** Verifier SHALL verify each structured claim against `tool_trace_summary` - **THEN** Verifier SHALL verify each structured claim against `tool_trace_summary` through `claim_checks`
- **AND** each claim's evidence bindings SHALL reference existing trace or invocation identifiers when those identifiers are available - **AND** each claim's evidence bindings SHALL reference existing trace or invocation identifiers when those identifiers are available
- **AND** a claim with fabricated or missing evidence references SHALL NOT be classified as `direct_evidence` - **AND** a claim with fabricated or missing evidence references SHALL NOT be classified as `direct_observation`
- **AND** Verifier SHALL NOT add extra confirmed facts from `executor_final_answer` that are absent from `executor_structured_output.claims`
#### Scenario: Extra confirmed-sounding answer facts are still checked #### Scenario: Malformed structured output cannot pass through natural language fallback
- **WHEN** `executor_structured_output.user_facing_answer` contains confirmed-sounding facts that are absent from `executor_structured_output.claims`
- **THEN** Verifier SHALL add those facts to `facts_checked`
- **AND** unsupported extra facts SHALL lower the verdict according to the existing verdict matrix
#### Scenario: Natural-language fallback remains available
- **WHEN** Executor does not return parseable structured output - **WHEN** Executor does not return parseable structured output
- **THEN** Verifier SHALL fall back to extracting facts from `executor_final_answer` - **THEN** Verifier SHALL NOT produce an effective `PASS` by extracting facts from `executor_final_answer`
- **AND** the final verdict SHALL still follow the existing groundedness and evidence classification rules - **AND** the effective verdict SHALL be `LOW_CONFID`
### Requirement: Verifier SHALL output structured JSON ### Requirement: Verifier SHALL output structured JSON
The Verifier SHALL output a JSON object with verdict, groundedness_score, facts_checked array, and rationale. The Verifier SHALL output a JSON object with verdict, groundedness_score, claim_checks array, compatibility facts_checked array, and rationale.
#### Scenario: claim-level verifier output is accepted
- **WHEN** the Verifier checks Executor V2 structured output
- **THEN** the output SHALL contain `verdict`, `groundedness_score`, `critical_fact_count`, `claim_checks`, `facts_checked`, and `rationale`
- **AND** `claim_checks` SHALL be the primary V2 verification result
- **AND** `facts_checked` SHALL remain available for compatibility
#### Scenario: Output format validation #### Scenario: Output format validation
- **WHEN** the Verifier completes its analysis - **WHEN** the Verifier completes its analysis
- **THEN** the output SHALL contain "verdict", "groundedness_score", "facts_checked", and "rationale" fields - **THEN** the output SHALL contain "verdict", "groundedness_score", "claim_checks", "facts_checked", and "rationale" fields
- **AND** groundedness_score SHALL be a float between 0.0 and 1.0 - **AND** groundedness_score SHALL be a float between 0.0 and 1.0
- **AND** verdict SHALL be one of "PASS", "LOW_CONFID", or "REJECT" - **AND** verdict SHALL be one of "PASS", "LOW_CONFID", or "REJECT"
@@ -57,14 +60,19 @@ The Verifier SHALL output a JSON object with verdict, groundedness_score, facts_
- **THEN** it SHALL output exactly one JSON object - **THEN** it SHALL output exactly one JSON object
- **AND** it SHALL NOT output Markdown, code fences, or explanatory text outside the JSON object - **AND** it SHALL NOT output Markdown, code fences, or explanatory text outside the JSON object
- **AND** the JSON object SHALL include `critical_fact_count` - **AND** the JSON object SHALL include `critical_fact_count`
- **AND** each `claim_checks` item SHALL include `claim_id`, `verification`, `detail`, and `evidence_refs`
- **AND** each `facts_checked` item SHALL include `fact`, `is_critical`, `verification`, and `detail` - **AND** each `facts_checked` item SHALL include `fact`, `is_critical`, `verification`, and `detail`
### Requirement: facts_checked SHALL use a fixed classification set ### Requirement: facts_checked SHALL use a fixed classification set
Each checked fact SHALL be labeled using a fixed evidence classification. The system SHALL continue to expose legacy `facts_checked` using its fixed verification classification set.
#### Scenario: fact classification values #### Scenario: claim checks are mapped to legacy facts
- **WHEN** the Verifier emits `facts_checked` - **WHEN** Verifier output contains `claim_checks`
- **THEN** each fact SHALL use one of `direct_evidence`, `indirect_support`, `no_evidence`, or `contradicted` - **THEN** ChatService SHALL derive compatibility `facts_checked`
- **AND** `direct_observation` SHALL map to `direct_evidence`
- **AND** `reasonable_inference` and `overstated` SHALL map to `indirect_support`
- **AND** `unsupported` and `external_unknown` SHALL map to `no_evidence`
- **AND** `contradicted` SHALL map to `contradicted`
### Requirement: groundedness_score SHALL be derived from fact classifications ### Requirement: groundedness_score SHALL be derived from fact classifications
The groundedness score SHALL be computed from critical fact classifications instead of being freely chosen by the model. The groundedness score SHALL be computed from critical fact classifications instead of being freely chosen by the model.
@@ -124,6 +132,12 @@ The system SHALL use fixed output protocols for LOW_CONFID and REJECT user-facin
### Requirement: Verifier SHALL be observable ### Requirement: Verifier SHALL be observable
The Verifier's verdict SHALL be persisted for observability. The Verifier's verdict SHALL be persisted for observability.
#### Scenario: claim checks written to self_evaluation
- **WHEN** the Verifier evaluation is persisted
- **THEN** `diagnosis_session.self_evaluation.verifier_evaluation` SHALL include `claim_checks`
- **AND** it SHALL continue to include compatibility `facts_checked`
- **AND** existing fields such as `verdict`, `groundedness_score`, `rationale`, `executor_output_parse_status`, `tool_trace_summary`, and `gatekeeper_result` SHALL be preserved
#### Scenario: verdict written to self_evaluation #### Scenario: verdict written to self_evaluation
- **WHEN** the Verifier produces a verdict - **WHEN** the Verifier produces a verdict
- **THEN** the ChatService SHALL write the verdict data under `diagnosis_session.self_evaluation.verifier_evaluation` - **THEN** the ChatService SHALL write the verdict data under `diagnosis_session.self_evaluation.verifier_evaluation`
@@ -166,7 +180,7 @@ The Verifier SHALL receive explicit verification inputs rather than inferring th
#### Scenario: Verifier remains isolated from intermediate reasoning #### Scenario: Verifier remains isolated from intermediate reasoning
- **WHEN** `executor_structured_output` is added to the verifier input - **WHEN** `executor_structured_output` is added to the verifier input
- **THEN** the input SHALL still exclude Planner reasoning and Executor intermediate reasoning - **THEN** the input SHALL still exclude Planner reasoning and Executor intermediate reasoning
- **AND** the input SHALL be limited to the original query, final Executor output, parsed Executor evidence contract, tool trace summary, and retry context - **AND** the input SHALL be limited to the original query, final Executor output, parsed Executor evidence contract, tool trace summary, gatekeeper result, and retry context
#### Scenario: tool trace summary derived from tool facts #### Scenario: tool trace summary derived from tool facts
- **WHEN** the system prepares verifier inputs - **WHEN** the system prepares verifier inputs
@@ -211,6 +225,17 @@ The Verifier SHALL receive explicit verification inputs rather than inferring th
- **AND** `gatekeeper_result.status` SHALL be one of `pass`, `warn`, or `fail` - **AND** `gatekeeper_result.status` SHALL be one of `pass`, `warn`, or `fail`
- **AND** `gatekeeper_result` SHALL include `failed_rules`, `warnings`, and `errors` - **AND** `gatekeeper_result` SHALL include `failed_rules`, `warnings`, and `errors`
#### Scenario: structured claims are the primary verification target
- **WHEN** `executor_output_parse_status.status` is `valid`
- **AND** `executor_structured_output.claims` is available
- **THEN** Verifier SHALL verify each claim through `claim_checks`
- **AND** Verifier SHALL NOT add extra confirmed facts from `executor_final_answer` that are absent from `executor_structured_output.claims`
#### Scenario: malformed structured output cannot pass through natural language fallback
- **WHEN** `executor_output_parse_status.status` is `missing` or `malformed`
- **THEN** Verifier SHALL NOT produce an effective `PASS` by extracting facts from `executor_final_answer`
- **AND** the effective verdict SHALL be `LOW_CONFID`
### Requirement: Verifier facts SHALL be auditable ### Requirement: Verifier facts SHALL be auditable
Verifier facts SHALL be linkable to the evidence summaries used during verification. Verifier facts SHALL be linkable to the evidence summaries used during verification.
@@ -355,3 +380,26 @@ The system SHALL run deterministic Gatekeeper checks after Executor output parsi
- **AND** each claim has evidence bindings pointing to current-session invocations with matching tool names - **AND** each claim has evidence bindings pointing to current-session invocations with matching tool names
- **THEN** `gatekeeper_result.status` SHALL be `pass` - **THEN** `gatekeeper_result.status` SHALL be `pass`
- **AND** `gatekeeper_result.failed_rules` SHALL be empty - **AND** `gatekeeper_result.failed_rules` SHALL be empty
#### Scenario: gatekeeper fail prevents PASS
- **WHEN** `gatekeeper_result.status` is `fail`
- **AND** the Verifier model returns `verdict = "PASS"`
- **THEN** ChatService SHALL downgrade the effective verdict
- **AND** the effective verdict SHALL NOT be `PASS`
#### Scenario: invocation reference failure downgrades to reject
- **WHEN** `gatekeeper_result.failed_rules` contains `evidence.invocation_ref`
- **AND** the Verifier model returns `verdict = "PASS"`
- **THEN** ChatService SHALL set the effective verdict to `REJECT`
### Requirement: Verifier claim checks SHALL use a fixed derivability classification set
The Verifier SHALL classify each structured claim using a fixed derivability classification set.
#### Scenario: claim check verification values are constrained
- **WHEN** Verifier emits `claim_checks`
- **THEN** each item SHALL use one of `direct_observation`, `reasonable_inference`, `overstated`, `unsupported`, `external_unknown`, or `contradicted`
#### Scenario: claim check evidence references remain auditable
- **WHEN** Verifier emits `claim_checks`
- **THEN** each claim check SHALL include `claim_id`, `verification`, `detail`, and `evidence_refs`
- **AND** every evidence ref SHALL preserve available `trace_ref`, `tool_name`, and `source_invocation_ids`
@@ -204,19 +204,22 @@ public class AgentLoggingHook extends MessagesModelHook {
private String summarizeVerifierThought(String verifierOutput) { private String summarizeVerifierThought(String verifierOutput) {
try { try {
JsonNode root = objectMapper.readTree(verifierOutput); JsonNode root = objectMapper.readTree(verifierOutput);
int claimCount = root.path("claim_checks").isArray() ? root.path("claim_checks").size() : 0;
int factCount = root.path("facts_checked").isArray() ? root.path("facts_checked").size() : 0; int factCount = root.path("facts_checked").isArray() ? root.path("facts_checked").size() : 0;
int tracedFactCount = 0; int tracedFactCount = 0;
if (root.path("facts_checked").isArray()) { JsonNode tracedNodes = root.path("claim_checks").isArray() ? root.path("claim_checks") : root.path("facts_checked");
for (JsonNode factNode : root.path("facts_checked")) { if (tracedNodes.isArray()) {
if (factNode.path("evidence_refs").isArray() && factNode.path("evidence_refs").size() > 0) { for (JsonNode node : tracedNodes) {
if (node.path("evidence_refs").isArray() && node.path("evidence_refs").size() > 0) {
tracedFactCount++; tracedFactCount++;
} }
} }
} }
return "verdict=%s, score=%s, critical_fact_count=%s, facts_checked=%d, traced_facts=%d".formatted( return "verdict=%s, score=%s, critical_fact_count=%s, claim_checks=%d, facts_checked=%d, traced_facts=%d".formatted(
root.path("verdict").asText("UNKNOWN"), root.path("verdict").asText("UNKNOWN"),
root.path("groundedness_score").asText("0.0"), root.path("groundedness_score").asText("0.0"),
root.path("critical_fact_count").asText("0"), root.path("critical_fact_count").asText("0"),
claimCount,
factCount, factCount,
tracedFactCount tracedFactCount
); );
@@ -650,12 +650,18 @@ public class ChatService {
try { try {
JsonNode root = objectMapper.readTree(sanitizeJsonPayload(verifierOutput)); JsonNode root = objectMapper.readTree(sanitizeJsonPayload(verifierOutput));
List<Map<String, Object>> factsChecked = parseFactsChecked(root.path("facts_checked")); List<Map<String, Object>> claimChecks = parseClaimChecks(root.path("claim_checks"));
List<Map<String, Object>> factsChecked = claimChecks.isEmpty()
? parseFactsChecked(root.path("facts_checked"))
: mapClaimChecksToFactsChecked(claimChecks);
String verdict = effectiveVerifierVerdict(root.path("verdict").asText("LOW_CONFID"));
int criticalFactCount = root.path("critical_fact_count").asInt(countCriticalFacts(factsChecked));
return new VerifierDecision( return new VerifierDecision(
root.path("verdict").asText("LOW_CONFID"), verdict,
root.path("groundedness_score").asDouble(0.0), root.path("groundedness_score").asDouble(0.0),
root.path("critical_fact_count").asInt(0), criticalFactCount,
claimChecks,
factsChecked, factsChecked,
root.path("rationale").asText(""), root.path("rationale").asText(""),
round round
@@ -678,6 +684,104 @@ public class ChatService {
return trimmed; return trimmed;
} }
private String effectiveVerifierVerdict(String modelVerdict) {
String verdict = normalizeVerdict(modelVerdict);
Map<String, Object> parseStatus = VerifierContextHolder.getExecutorOutputParseStatus();
String parseState = parseStatus == null ? "" : String.valueOf(parseStatus.getOrDefault("status", ""));
if (("missing".equals(parseState) || "malformed".equals(parseState)) && "PASS".equals(verdict)) {
return "LOW_CONFID";
}
Map<String, Object> gatekeeperResult = VerifierContextHolder.getGatekeeperResult();
if (gatekeeperResult == null || !"fail".equals(String.valueOf(gatekeeperResult.get("status")))) {
return verdict;
}
if (containsRule(gatekeeperResult.get("failed_rules"), ExecutorGatekeeperService.RULE_INVOCATION_REF)) {
return "REJECT";
}
return "PASS".equals(verdict) ? "LOW_CONFID" : verdict;
}
private String normalizeVerdict(String verdict) {
if ("PASS".equals(verdict) || "LOW_CONFID".equals(verdict) || "REJECT".equals(verdict)) {
return verdict;
}
return "LOW_CONFID";
}
private boolean containsRule(Object rulesValue, String ruleId) {
if (!(rulesValue instanceof List<?> rules)) {
return false;
}
return rules.stream().anyMatch(rule -> ruleId.equals(String.valueOf(rule)));
}
private int countCriticalFacts(List<Map<String, Object>> factsChecked) {
return (int) factsChecked.stream()
.filter(fact -> Boolean.TRUE.equals(fact.get("is_critical")))
.count();
}
private List<Map<String, Object>> parseClaimChecks(JsonNode claimChecksNode) {
List<Map<String, Object>> claimChecks = new ArrayList<>();
if (!claimChecksNode.isArray()) {
return claimChecks;
}
for (JsonNode claimNode : claimChecksNode) {
Map<String, Object> claimCheck = new LinkedHashMap<>();
claimCheck.put("claim_id", claimNode.path("claim_id").asText(""));
claimCheck.put("claim_text", claimNode.path("claim_text").asText(""));
claimCheck.put("claim_type", claimNode.path("claim_type").asText(""));
claimCheck.put("verification", normalizeClaimVerification(claimNode.path("verification").asText("unsupported")));
claimCheck.put("detail", claimNode.path("detail").asText(""));
claimCheck.put("evidence_refs", parseEvidenceRefs(claimNode.path("evidence_refs")));
claimChecks.add(claimCheck);
}
return claimChecks;
}
private String normalizeClaimVerification(String verification) {
return switch (verification) {
case "direct_observation", "reasonable_inference", "overstated", "unsupported",
"external_unknown", "contradicted" -> verification;
default -> "unsupported";
};
}
private List<Map<String, Object>> mapClaimChecksToFactsChecked(List<Map<String, Object>> claimChecks) {
List<Map<String, Object>> factsChecked = new ArrayList<>();
for (Map<String, Object> claimCheck : claimChecks) {
String claimId = String.valueOf(claimCheck.getOrDefault("claim_id", ""));
String claimText = String.valueOf(claimCheck.getOrDefault("claim_text", ""));
String claimType = String.valueOf(claimCheck.getOrDefault("claim_type", ""));
Map<String, Object> fact = new LinkedHashMap<>();
fact.put("fact", claimId.isBlank() ? claimText : claimId + ": " + claimText);
fact.put("is_critical", isCriticalClaimType(claimType));
fact.put("verification", mapClaimVerificationToFactVerification(
String.valueOf(claimCheck.getOrDefault("verification", "unsupported"))));
fact.put("detail", claimCheck.getOrDefault("detail", ""));
fact.put("evidence_refs", claimCheck.getOrDefault("evidence_refs", List.of()));
factsChecked.add(fact);
}
return factsChecked;
}
private boolean isCriticalClaimType(String claimType) {
return "root_cause".equals(claimType)
|| "symptom".equals(claimType)
|| "impact".equals(claimType)
|| "risk".equals(claimType);
}
private String mapClaimVerificationToFactVerification(String verification) {
return switch (verification) {
case "direct_observation" -> "direct_evidence";
case "reasonable_inference", "overstated" -> "indirect_support";
case "contradicted" -> "contradicted";
default -> "no_evidence";
};
}
private List<Map<String, Object>> parseFactsChecked(JsonNode factsNode) { private List<Map<String, Object>> parseFactsChecked(JsonNode factsNode) {
List<Map<String, Object>> factsChecked = new ArrayList<>(); List<Map<String, Object>> factsChecked = new ArrayList<>();
if (!factsNode.isArray()) { if (!factsNode.isArray()) {
@@ -723,7 +827,7 @@ public class ChatService {
} }
private VerifierDecision buildVerifierFallbackDecision(int round, String rationale) { private VerifierDecision buildVerifierFallbackDecision(int round, String rationale) {
return new VerifierDecision("LOW_CONFID", 0.0, 0, List.of(), rationale, round); return new VerifierDecision("LOW_CONFID", 0.0, 0, List.of(), List.of(), rationale, round);
} }
private String extractStateText(Optional<OverAllState> stateOptional, String key) { private String extractStateText(Optional<OverAllState> stateOptional, String key) {
@@ -748,6 +852,7 @@ public class ChatService {
verifierEvaluation.put("verdict", decision.verdict()); verifierEvaluation.put("verdict", decision.verdict());
verifierEvaluation.put("groundedness_score", decision.groundednessScore()); verifierEvaluation.put("groundedness_score", decision.groundednessScore());
verifierEvaluation.put("critical_fact_count", decision.criticalFactCount()); verifierEvaluation.put("critical_fact_count", decision.criticalFactCount());
verifierEvaluation.put("claim_checks", decision.claimChecks());
verifierEvaluation.put("facts_checked", decision.factsChecked()); verifierEvaluation.put("facts_checked", decision.factsChecked());
verifierEvaluation.put("rationale", decision.rationale()); verifierEvaluation.put("rationale", decision.rationale());
verifierEvaluation.put("round", round); verifierEvaluation.put("round", round);
@@ -1005,6 +1110,7 @@ public class ChatService {
String verdict, String verdict,
double groundednessScore, double groundednessScore,
int criticalFactCount, int criticalFactCount,
List<Map<String, Object>> claimChecks,
List<Map<String, Object>> factsChecked, List<Map<String, Object>> factsChecked,
String rationale, String rationale,
int round int round
@@ -1,4 +1,4 @@
你是质量闸 verifier。你的任务是对 `executor_final_answer` 做一次基于现有证据的事实校验。 你是质量闸 verifier。你的任务是对 Executor 的结构化 claims 做一次基于现有证据的可推导性校验。
边界约束: 边界约束:
- 不做新的检索 - 不做新的检索
@@ -9,7 +9,7 @@
## 输入字段 ## 输入字段
- `original_query`:用户原始问题 - `original_query`:用户原始问题
- `executor_final_answer`:本轮 Executor 最终答案 - `executor_final_answer`:Executor 原始输出,仅用于 debug/fallback;当结构化输出有效时,不得从这里抽取额外确认事实
- `executor_structured_output`:如果 Executor 输出了合法证据归因 JSON,这里会提供解析后的对象。结构包含 `claims`、`hypotheses`、`recommended_actions`、`missing_info`;兼容旧版时可能包含 `user_facing_answer` - `executor_structured_output`:如果 Executor 输出了合法证据归因 JSON,这里会提供解析后的对象。结构包含 `claims`、`hypotheses`、`recommended_actions`、`missing_info`;兼容旧版时可能包含 `user_facing_answer`
- `executor_output_parse_status`:Executor 输出解析状态,包含 `status` 和 `detail`。`status` 可能是 `valid` / `missing` / `malformed` - `executor_output_parse_status`:Executor 输出解析状态,包含 `status` 和 `detail`。`status` 可能是 `valid` / `missing` / `malformed`
- `tool_trace_summary`:基于真实工具调用整理出的证据索引。每一项都带有: - `tool_trace_summary`:基于真实工具调用整理出的证据索引。每一项都带有:
@@ -25,52 +25,47 @@
## 任务步骤 ## 任务步骤
### 步骤一:提取关键事实 ### 步骤一:确定校验对象
如果 `executor_output_parse_status.status="valid"` 且 `executor_structured_output.claims` 存在: 如果 `executor_output_parse_status.status="valid"` 且 `executor_structured_output.claims` 存在:
- 优先逐条校验 `executor_structured_output.claims` - 优先逐条校验 `executor_structured_output.claims`
- 每个 claim 至少形成一条 `facts_checked` - 每个 claim 至少形成一条 `claim_checks`
- 必须检查 claim 的 `evidence_bindings` 是否能对应到 `tool_trace_summary` 中真实存在的 trace、tool 或 source_invocation_ids - 必须检查 claim 的 `evidence_bindings` 是否能对应到 `tool_trace_summary` 中真实存在的 trace、tool 或 source_invocation_ids
- 如果 claim 声称 direct/indirect 支撑,但 evidence binding 不存在、无法定位、或 excerpt 与工具摘要不匹配,不得判为 `direct_evidence` - 不得从 `executor_final_answer` 中抽取不在 claims 里的额外确认事实
如果 `executor_structured_output.user_facing_answer` 存在,则必须扫描它: 如果 structured output 缺失或 malformed:
- 如果其中出现 confirmed-sounding facts(确认式事实、根因、指标值、错误码、服务名、修复结论) - 不得通过扫描 `executor_final_answer` 生成 `PASS`
- 且这些事实没有出现在 `executor_structured_output.claims` - 输出 `LOW_CONFID`
- 必须额外加入 `facts_checked` 并按工具证据校验 - `groundedness_score = 0.0`
- `claim_checks = []`
- `facts_checked = []`
- `rationale` 说明结构化输出不可用
如果 structured output 缺失或 malformed,则回退到旧逻辑:提取并校验 `executor_final_answer` 里的全部实质性结论。关键事实至少包括: ### 步骤二:逐条校验 claim
- 每一个根因结论 每条 claim check 必须输出:
- 每一个错误码、接口、组件归属或语义判断 - `claim_id`
- 每一个明确的修复建议、参数建议、排查步骤 - `claim_text`
- 每一个“证据来源陈述” - `claim_type`
覆盖要求:
- 不允许只抽取一个总括性事实替代整段答案
- 如果答案给出多个根因,必须逐条拆成多个 `fact`
- 如果答案给出多条修复建议,必须逐条拆成多个 `fact`
- 只有寒暄、流程衔接语、与结论无关的话,才可以不纳入 `facts_checked`
### 步骤二:逐条校验事实
每条事实必须输出:
- `fact`
- `is_critical`
- `verification` - `verification`
- `detail` - `detail`
- `evidence_refs` - `evidence_refs`
`verification` 只允许以下四个值: `claim_checks[*].verification` 只允许以下六个值:
- `direct_evidence` - `direct_observation`
- `indirect_support` - `reasonable_inference`
- `no_evidence` - `overstated`
- `unsupported`
- `external_unknown`
- `contradicted` - `contradicted`
结构化 claim 的校验规则: 结构化 claim 的校验规则:
- claim 有真实 evidence binding,且工具摘要直接包含该事实 → `direct_evidence` - claim 有真实 evidence binding,且工具摘要直接包含该事实 → `direct_observation`
- claim 有真实 evidence binding,但工具摘要只能支持方向或背景 → `indirect_support` - claim 有真实 evidence binding,工具摘要没有逐字说明但可以合理推出 → `reasonable_inference`
- claim 无法绑定真实 trace、invocation 或 excerpt → `no_evidence` - claim 有部分依据,但写成唯一根因、确认根因或说得过满 → `overstated`
- claim 与工具摘要冲突,或编造了不存在的关键实体、服务、错误码、指标值 → `contradicted` - claim 无法绑定真实 trace、invocation 或 excerpt → `unsupported`
- claim 引入证据外的新服务名、订单号、错误码、指标值、根因 → `external_unknown`
- claim 与工具摘要冲突 → `contradicted`
`hypotheses` 和 `missing_info` 默认不是 confirmed facts,不应因为它们承认缺证据而惩罚。 `hypotheses` 和 `missing_info` 默认不是 confirmed facts,不应因为它们承认缺证据而惩罚。
但如果 `user_facing_answer` 把 hypothesis 写成确认结论,必须按 confirmed fact 校验。
### 步骤三:补齐 evidence_refs ### 步骤三:补齐 evidence_refs
`evidence_refs` 必须是数组,数组元素必须引用 `tool_trace_summary` 中真实存在的证据项。每个元素包含: `evidence_refs` 必须是数组,数组元素必须引用 `tool_trace_summary` 中真实存在的证据项。每个元素包含:
@@ -94,24 +89,26 @@
- 若 `failed_rules` 包含 `evidence.invocation_ref`,倾向 `REJECT` - 若 `failed_rules` 包含 `evidence.invocation_ref`,倾向 `REJECT`
- 否则至少输出 `LOW_CONFID` - 否则至少输出 `LOW_CONFID`
1. 若任一关键事实(`is_critical=true`)为 `contradicted` 1. 若任一关键 claim 为 `contradicted`
- `verdict = "REJECT"` - `verdict = "REJECT"`
- `groundedness_score = 0.0` - `groundedness_score = 0.0`
2. 否则,若所有关键事实均为 `direct_evidence` 或 `indirect_support` 2. 否则,若所有关键 claims 均为 `direct_observation` 或 `reasonable_inference`
且至少一条关键事实为 `direct_evidence` 且至少一条关键 claim 为 `direct_observation`
- `verdict = "PASS"` - `verdict = "PASS"`
3. 否则,若不存在 `contradicted` 3. 否则,若不存在 `contradicted`
且存在关键事实为 `no_evidence` 且存在关键 claim 为 `unsupported` / `external_unknown` / `overstated`
或所有关键事实都只有 `indirect_support` 或所有关键 claim 都只有 `reasonable_inference`
- `verdict = "LOW_CONFID"` - `verdict = "LOW_CONFID"`
### 步骤五:计算 groundedness_score ### 步骤五:计算 groundedness_score
只统计 `is_critical=true` 的事实,映射如下: 只统计关键 claim,映射如下:
- `direct_evidence = 1.0` - `direct_observation = 1.0`
- `indirect_support = 0.6` - `reasonable_inference = 0.6`
- `no_evidence = 0.0` - `overstated = 0.3`
- `unsupported = 0.0`
- `external_unknown = 0.0`
- `contradicted = 0.0` - `contradicted = 0.0`
规则: 规则:
@@ -120,14 +117,28 @@
- 保留 2 位小数 - 保留 2 位小数
- 分数范围必须在 `[0.0, 1.0]` - 分数范围必须在 `[0.0, 1.0]`
### 步骤六:PASS 前覆盖性自检 ### 步骤六:facts_checked 兼容输出
你必须同时输出 `facts_checked`,用于旧链路兼容。
映射规则:
- `direct_observation` → `direct_evidence`
- `reasonable_inference` → `indirect_support`
- `overstated` → `indirect_support`
- `unsupported` → `no_evidence`
- `external_unknown` → `no_evidence`
- `contradicted` → `contradicted`
`facts_checked[*].fact` 使用 `{claim_id}: {claim_text}`。
### 步骤七:PASS 前覆盖性自检
在输出 `PASS` 前,必须再次检查: 在输出 `PASS` 前,必须再次检查:
- `facts_checked` 是否覆盖了 `executor_final_answer` 的全部实质性结论 - `claim_checks` 是否覆盖了 `executor_structured_output.claims` 中的全部 claims
- 是否遗漏了单独出现的根因、修复建议、参数建议、排查步骤 - 是否存在 `gatekeeper_result.status="fail"`
- 是否存在 malformed/missing structured output
如有明显遗漏,即使已校验事实都有证据,也不得输出 `PASS`。 如有明显遗漏,即使已校验事实都有证据,也不得输出 `PASS`。
### 步骤七:处理 retry_context ### 步骤八:处理 retry_context
若 `retry_context` 不为空: 若 `retry_context` 不为空:
- 优先检查上一轮缺失证据点是否已补足 - 优先检查上一轮缺失证据点是否已补足
- 不要扩展与缺口无关的新事实 - 不要扩展与缺口无关的新事实
@@ -141,9 +152,28 @@
"verdict": "PASS", "verdict": "PASS",
"groundedness_score": 0.8, "groundedness_score": 0.8,
"critical_fact_count": 2, "critical_fact_count": 2,
"claim_checks": [
{
"claim_id": "claim-1",
"claim_text": "ERR_TIMEOUT 表示请求超时",
"claim_type": "symptom",
"verification": "direct_observation",
"detail": "知识库文档明确给出该错误码定义",
"evidence_refs": [
{
"trace_ref": "trace-1",
"tool_name": "lookup_knowledge",
"topic_domain": "api",
"source_invocation_ids": [101, 104],
"note": "trace-1 的文档摘要直接给出错误码定义"
}
]
}
],
"hypothesis_checks": [],
"facts_checked": [ "facts_checked": [
{ {
"fact": "ERR_TIMEOUT 表示请求超时", "fact": "claim-1: ERR_TIMEOUT 表示请求超时",
"is_critical": true, "is_critical": true,
"verification": "direct_evidence", "verification": "direct_evidence",
"detail": "知识库文档明确给出该错误码定义", "detail": "知识库文档明确给出该错误码定义",
@@ -164,7 +194,9 @@
输出要求: 输出要求:
- `verdict` 只能是 `PASS` / `LOW_CONFID` / `REJECT` - `verdict` 只能是 `PASS` / `LOW_CONFID` / `REJECT`
- `groundedness_score` 必须是 JSON number - `groundedness_score` 必须是 JSON number
- `critical_fact_count` 必须等于 `facts_checked` 中 `is_critical=true` 的数量 - `critical_fact_count` 必须等于关键 claim 的数量;兼容期也应等于 `facts_checked` 中 `is_critical=true` 的数量
- `claim_checks` 可以为空数组,但字段不能缺失
- `facts_checked` 可以为空数组,但字段不能缺失 - `facts_checked` 可以为空数组,但字段不能缺失
- 每条 `claim_checks[*]` 都必须包含 `evidence_refs`
- 每条 `facts_checked[*]` 都必须包含 `evidence_refs` - 每条 `facts_checked[*]` 都必须包含 `evidence_refs`
- 不得输出 schema 之外的字段 - 不得输出 schema 之外的字段
@@ -55,7 +55,8 @@ class ChatServiceSequentialAgentTest {
"sequential-test-session" "sequential-test-session"
); );
assertEquals("EXECUTOR_FINAL_ANSWER", result.answer()); assertTrue(result.answer().contains("连接池 active 达到上限"));
assertFalse(result.answer().contains("\"answer_version\""));
assertEquals("sequential-test-session", result.sessionId()); assertEquals("sequential-test-session", result.sessionId());
assertEquals(List.of("chat_planner", "chat_executor", "chat_verifier"), chatModel.agentCalls); assertEquals(List.of("chat_planner", "chat_executor", "chat_verifier"), chatModel.agentCalls);
assertTrue(chatModel.sawVerifierPrompt); assertTrue(chatModel.sawVerifierPrompt);
@@ -225,7 +226,8 @@ class ChatServiceSequentialAgentTest {
"sequential-workflow-session" "sequential-workflow-session"
); );
assertEquals("EXECUTOR_FINAL_ANSWER", result.answer()); assertTrue(result.answer().contains("连接池 active 达到上限"));
assertFalse(result.answer().contains("\"answer_version\""));
assertEquals(List.of("chat_planner", "chat_executor", "chat_verifier"), chatModel.agentCalls); assertEquals(List.of("chat_planner", "chat_executor", "chat_verifier"), chatModel.agentCalls);
assertTrue(chatModel.sawVerifierPrompt); assertTrue(chatModel.sawVerifierPrompt);
} }
@@ -236,8 +238,7 @@ class ChatServiceSequentialAgentTest {
ScriptedChatModel chatModel = new ScriptedChatModel(); ScriptedChatModel chatModel = new ScriptedChatModel();
chatModel.executorOutput = """ chatModel.executorOutput = """
{ {
"answer_version": "executor_evidence_v1", "answer_version": "executor_evidence_v2",
"diagnosis_summary": "已确认连接池 active 达到上限。",
"claims": [ "claims": [
{ {
"claim_id": "claim-1", "claim_id": "claim-1",
@@ -257,8 +258,7 @@ class ChatServiceSequentialAgentTest {
], ],
"hypotheses": [], "hypotheses": [],
"recommended_actions": [], "recommended_actions": [],
"missing_info": [], "missing_info": []
"user_facing_answer": "已确认连接池 active 达到上限。"
} }
"""; """;
@@ -270,7 +270,8 @@ class ChatServiceSequentialAgentTest {
"sequential-structured-executor-session" "sequential-structured-executor-session"
); );
assertEquals("已确认连接池 active 达到上限。", result.answer()); assertTrue(result.answer().contains("连接池 active 达到上限"));
assertFalse(result.answer().contains("\"answer_version\""));
assertTrue(chatModel.verifierPromptText.contains("\"executor_structured_output\"")); assertTrue(chatModel.verifierPromptText.contains("\"executor_structured_output\""));
assertTrue(chatModel.verifierPromptText.contains("\"executor_output_parse_status\"")); assertTrue(chatModel.verifierPromptText.contains("\"executor_output_parse_status\""));
assertTrue(chatModel.verifierPromptText.contains("\"status\" : \"valid\"")); assertTrue(chatModel.verifierPromptText.contains("\"status\" : \"valid\""));
@@ -391,6 +392,160 @@ class ChatServiceSequentialAgentTest {
assertEquals("pass", gatekeeperResult.get("status")); assertEquals("pass", gatekeeperResult.get("status"));
} }
@Test
void executeChatComplexMapsClaimChecksToFactsCheckedAndPersistsBoth() throws Exception {
ChatService chatService = createChatService();
SelfEvaluationMergeService mergeService =
(SelfEvaluationMergeService) ReflectionTestUtils.getField(chatService, "selfEvaluationMergeService");
ToolInvocationRepository invocationRepository =
(ToolInvocationRepository) ReflectionTestUtils.getField(chatService, "toolInvocationRepository");
when(invocationRepository.findBySessionIdOrderByIdAsc("sequential-claim-check-session"))
.thenReturn(List.of(ToolInvocation.builder()
.id(101L)
.sessionId("sequential-claim-check-session")
.toolName("query_metrics")
.build()));
ScriptedChatModel chatModel = new ScriptedChatModel("""
{
"verdict": "LOW_CONFID",
"groundedness_score": 0.32,
"critical_fact_count": 6,
"claim_checks": [
{"claim_id":"claim-1","claim_text":"CPU 使用率 92%","claim_type":"symptom","verification":"direct_observation","detail":"direct","evidence_refs":[{"trace_ref":"trace-1","tool_name":"query_metrics","source_invocation_ids":[101],"note":"cpu"}]},
{"claim_id":"claim-2","claim_text":"CPU 过高可能导致超时","claim_type":"risk","verification":"reasonable_inference","detail":"inference","evidence_refs":[]},
{"claim_id":"claim-3","claim_text":"CPU 是唯一根因","claim_type":"root_cause","verification":"overstated","detail":"too strong","evidence_refs":[]},
{"claim_id":"claim-4","claim_text":"缺少线程池证据","claim_type":"symptom","verification":"unsupported","detail":"missing","evidence_refs":[]},
{"claim_id":"claim-5","claim_text":"出现证据外错误码 ERR_FAKE","claim_type":"symptom","verification":"external_unknown","detail":"external","evidence_refs":[]},
{"claim_id":"claim-6","claim_text":"证据显示 CPU 很低","claim_type":"symptom","verification":"contradicted","detail":"conflict","evidence_refs":[]}
],
"facts_checked": [],
"rationale": "claim checks drive compatibility"
}
""");
chatModel.executorOutput = validExecutorV2Output();
chatService.executeChatComplex(
chatModel,
new ToolCallback[0],
"请分析 MySQL 连接池耗尽",
List.of(),
"sequential-claim-check-session"
);
ArgumentCaptor<Map<String, Object>> captor = ArgumentCaptor.forClass(Map.class);
verify(mergeService).mergeVerifierEvaluation(isNull(), captor.capture());
Map<String, Object> verifierEvaluation = captor.getValue();
@SuppressWarnings("unchecked")
List<Map<String, Object>> claimChecks = (List<Map<String, Object>>) verifierEvaluation.get("claim_checks");
@SuppressWarnings("unchecked")
List<Map<String, Object>> factsChecked = (List<Map<String, Object>>) verifierEvaluation.get("facts_checked");
assertEquals(6, claimChecks.size());
assertEquals(6, factsChecked.size());
assertEquals("direct_evidence", factsChecked.get(0).get("verification"));
assertEquals("indirect_support", factsChecked.get(1).get("verification"));
assertEquals("indirect_support", factsChecked.get(2).get("verification"));
assertEquals("no_evidence", factsChecked.get(3).get("verification"));
assertEquals("no_evidence", factsChecked.get(4).get("verification"));
assertEquals("contradicted", factsChecked.get(5).get("verification"));
assertTrue(String.valueOf(factsChecked.get(0).get("fact")).startsWith("claim-1:"));
}
@Test
void executeChatComplexDowngradesPassToRejectWhenGatekeeperInvocationRefFails() throws Exception {
ChatService chatService = createChatService();
SelfEvaluationMergeService mergeService =
(SelfEvaluationMergeService) ReflectionTestUtils.getField(chatService, "selfEvaluationMergeService");
ToolInvocationRepository invocationRepository =
(ToolInvocationRepository) ReflectionTestUtils.getField(chatService, "toolInvocationRepository");
when(invocationRepository.findBySessionIdOrderByIdAsc("sequential-gatekeeper-fail-session"))
.thenReturn(List.of(ToolInvocation.builder()
.id(101L)
.sessionId("sequential-gatekeeper-fail-session")
.toolName("query_metrics")
.build()));
ScriptedChatModel chatModel = new ScriptedChatModel("""
{
"verdict": "PASS",
"groundedness_score": 1.0,
"critical_fact_count": 1,
"claim_checks": [
{"claim_id":"claim-1","claim_text":"连接池 active 达到上限","claim_type":"symptom","verification":"direct_observation","detail":"direct","evidence_refs":[]}
],
"facts_checked": [],
"rationale": "model tried pass"
}
""");
chatModel.executorOutput = """
{
"answer_version": "executor_evidence_v2",
"claims": [
{
"claim_id": "claim-1",
"claim_type": "symptom",
"claim_text": "连接池 active 达到上限",
"support_level": "direct",
"evidence_bindings": [
{
"source_type": "tool_trace",
"tool_name": "query_metrics",
"source_invocation_ids": [999],
"evidence_excerpt": "active=50 max=50"
}
]
}
],
"hypotheses": [],
"recommended_actions": [],
"missing_info": []
}
""";
ChatService.ChatResult result = chatService.executeChatComplex(
chatModel,
new ToolCallback[0],
"请分析 MySQL 连接池耗尽",
List.of(),
"sequential-gatekeeper-fail-session"
);
assertTrue(result.answer().startsWith("当前无法基于已获取证据生成可靠结论"));
ArgumentCaptor<Map<String, Object>> captor = ArgumentCaptor.forClass(Map.class);
verify(mergeService).mergeVerifierEvaluation(isNull(), captor.capture());
assertEquals("REJECT", captor.getValue().get("verdict"));
}
@Test
void executeChatComplexDowngradesPassToLowConfidenceWhenExecutorOutputMalformed() throws Exception {
ChatService chatService = createChatService();
SelfEvaluationMergeService mergeService =
(SelfEvaluationMergeService) ReflectionTestUtils.getField(chatService, "selfEvaluationMergeService");
ScriptedChatModel chatModel = new ScriptedChatModel("""
{
"verdict": "PASS",
"groundedness_score": 1.0,
"critical_fact_count": 0,
"claim_checks": [],
"facts_checked": [],
"rationale": "model tried pass"
}
""");
chatModel.executorOutput = "{ not-json";
ChatService.ChatResult result = chatService.executeChatComplex(
chatModel,
new ToolCallback[0],
"请分析 MySQL 连接池耗尽",
List.of(),
"sequential-malformed-pass-session"
);
assertTrue(result.answer().startsWith("以下结论基于当前已获取证据"));
ArgumentCaptor<Map<String, Object>> captor = ArgumentCaptor.forClass(Map.class);
verify(mergeService).mergeVerifierEvaluation(isNull(), captor.capture());
assertEquals("LOW_CONFID", captor.getValue().get("verdict"));
}
@Test @Test
void buildMethodToolsArrayIncludesLogsAndMetricsWhenAvailable() { void buildMethodToolsArrayIncludesLogsAndMetricsWhenAvailable() {
ChatService chatService = new ChatService(); ChatService chatService = new ChatService();
@@ -484,6 +639,10 @@ class ChatServiceSequentialAgentTest {
when(agentStepRepository.findBySessionIdOrderByStepIndex(anyString())).thenReturn(List.of()); when(agentStepRepository.findBySessionIdOrderByStepIndex(anyString())).thenReturn(List.of());
ToolInvocationRepository toolInvocationRepository = mock(ToolInvocationRepository.class); ToolInvocationRepository toolInvocationRepository = mock(ToolInvocationRepository.class);
when(toolInvocationRepository.countBySessionId(anyString())).thenReturn(0L); when(toolInvocationRepository.countBySessionId(anyString())).thenReturn(0L);
when(toolInvocationRepository.findBySessionIdOrderByIdAsc(anyString())).thenReturn(List.of(ToolInvocation.builder()
.id(101L)
.toolName("query_metrics")
.build()));
EvaluationService evaluationService = mock(EvaluationService.class); EvaluationService evaluationService = mock(EvaluationService.class);
RetrievedDocTracker retrievedDocTracker = mock(RetrievedDocTracker.class); RetrievedDocTracker retrievedDocTracker = mock(RetrievedDocTracker.class);
@@ -514,13 +673,65 @@ class ChatServiceSequentialAgentTest {
return chatService; return chatService;
} }
private String validExecutorV2Output() {
return """
{
"answer_version": "executor_evidence_v2",
"claims": [
{
"claim_id": "claim-1",
"claim_type": "symptom",
"claim_text": "连接池 active 达到上限",
"support_level": "direct",
"evidence_bindings": [
{
"source_type": "tool_trace",
"source_id": "trace-1",
"tool_name": "query_metrics",
"source_invocation_ids": [101],
"evidence_excerpt": "active=50 max=50"
}
]
}
],
"hypotheses": [],
"recommended_actions": [],
"missing_info": []
}
""";
}
private static final class ScriptedChatModel implements ChatModel { private static final class ScriptedChatModel implements ChatModel {
private final java.util.ArrayList<String> agentCalls = new java.util.ArrayList<>(); private final java.util.ArrayList<String> agentCalls = new java.util.ArrayList<>();
private String promptText = ""; private String promptText = "";
private String plannerPromptText = ""; private String plannerPromptText = "";
private String executorPromptText = ""; private String executorPromptText = "";
private String verifierPromptText = ""; private String verifierPromptText = "";
private String executorOutput = "EXECUTOR_FINAL_ANSWER"; private String executorOutput = """
{
"answer_version": "executor_evidence_v2",
"claims": [
{
"claim_id": "claim-1",
"claim_type": "symptom",
"claim_text": "连接池 active 达到上限",
"support_level": "direct",
"evidence_bindings": [
{
"source_type": "tool_trace",
"source_id": "trace-1",
"tool_name": "query_metrics",
"source_invocation_ids": [101],
"evidence_excerpt": "active=50 max=50"
}
]
}
],
"hypotheses": [],
"recommended_actions": [],
"missing_info": []
}
""";
private boolean sawVerifierPrompt; private boolean sawVerifierPrompt;
private final java.util.List<String> verifierOutputs; private final java.util.List<String> verifierOutputs;
private int verifierOutputIndex; private int verifierOutputIndex;
@@ -531,15 +742,10 @@ class ChatServiceSequentialAgentTest {
"verdict": "PASS", "verdict": "PASS",
"groundedness_score": 1.0, "groundedness_score": 1.0,
"critical_fact_count": 1, "critical_fact_count": 1,
"facts_checked": [ "claim_checks": [
{ {"claim_id":"claim-1","claim_text":"连接池 active 达到上限","claim_type":"symptom","verification":"direct_observation","detail":"covered by scripted verifier","evidence_refs":[]}
"fact": "executor answer generated",
"is_critical": true,
"verification": "direct_evidence",
"detail": "covered by scripted verifier",
"evidence_refs": []
}
], ],
"facts_checked": [],
"rationale": "scripted pass" "rationale": "scripted pass"
} }
"""); """);