feat(harness): add evidence and semantic guards
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
## Context
|
||||
|
||||
阶段 2-4 已提供显式 `RunContext`、类型化重试策略、canonical invocation store、三类有界 Tool projection 和内部 `DiagnosisAgentUseCase`。当前 `DiagnosisDraft` 仍只是未发布草稿:类型反序列化不保证 ID 唯一、报告内部引用闭合,也不证明 Tool Call 属于当前 Run 或 projection 可引用;即使物理证据真实,也还需要独立判断整份报告是否被证据支持。
|
||||
|
||||
本阶段只建立内部 release boundary。公开 `ChatController`、SSE、Run persistence 和旧 Planner/Executor/Verifier/Composer 链路由阶段 6A/6B/7 处理。接口影响为 L2,新增内部 Java use case 供后续阶段消费,不改变当前外部协议。
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
- 用确定性代码验证 Draft 结构、Analysis 引用和当前 Run canonical invocation。
|
||||
- 只从 `agent_result` 和必要的 canonical request 生成无 Tool ID、无 raw response 的 verified evidence snapshot。
|
||||
- 首次验真失败时允许一次保持用户可见语义不变的结构修复。
|
||||
- 使用同一 `ChatModel` 执行无 Tool、无记忆、无 ReAct 的隔离 SemanticGuard,并由 Harness 控制预算、超时、取消和技术重试。
|
||||
- 只发布通过两层门禁的原 Draft;所有其他可降级路径返回固定 `SafeFallback`。
|
||||
|
||||
**Non-Goals:**
|
||||
|
||||
- 不切换 `/api/chat`、`/api/chat_stream` 或 `/api/ai_ops`。
|
||||
- 不实现 SSE 事件、Run 持久化、PreviousTurn、Intent Router 或最终报告渲染。
|
||||
- 不删除或改造旧 Gatekeeper、Verifier、Composer 和多 Agent 生产链路。
|
||||
- 不访问 canonical `raw_response`,不新增 Redis 索引或历史证据召回。
|
||||
- 不执行 live model/Redis/MySQL E2E;最终 live 验收留到阶段 7。
|
||||
|
||||
## Decisions
|
||||
|
||||
### 1. EvidenceGuard 是 fail-closed 的纯确定性边界
|
||||
|
||||
`EvidenceGuard.validate(RunContext, DiagnosisDraft)` 分两步执行:先验证 Draft 顶层/嵌套结构、非空文本、Analysis ID 唯一性和 `based_on_analysis_ids` 闭合;再对每个 Analysis 的非空 Tool Call ID 使用 `ToolCallKeyFactory.create(runId, id)` 查询 `CanonicalInvocationStore`。
|
||||
|
||||
可引用记录必须同时满足:record `runId` 等于当前 Run、`status=READY`、`agent_result` 非空、`evidence_status=EVIDENCE_FOUND|NO_EVIDENCE`。`AnalysisKind.accepts` 固定 `NORMAL -> EVIDENCE_FOUND`、`NEGATIVE_OBSERVATION -> NO_EVIDENCE`。任何非法 key、缺失记录、跨 Run、错误/投影中记录、空 projection、状态不一致或解析错误都产生稳定 `EvidenceViolation`,不会抛出原始存储内容或进入 SemanticGuard。
|
||||
|
||||
替代方案是复用旧 `ExecutorGatekeeperService`;拒绝该方案,因为旧实现绑定 JPA `ToolInvocation`、Session/Run 双入口和 Map DTO,不能表达 Redis canonical lifecycle、`NO_EVIDENCE` kind 约束或新的 typed Draft。
|
||||
|
||||
### 2. verified snapshot 使用 Tool-specific 严格 projection adapter
|
||||
|
||||
Guard 根据冻结的 `AgentToolContracts` 严格解析 `RagToolResult`、`QueryLogsToolResult`、`MysqlToolResult`,并校验 projection 内 `tool_call_id`、`evidence_status` 与 canonical record 一致。未知 Tool 一律拒绝。
|
||||
|
||||
快照结构为 `analysis_id + analysis_text + analysis_kind + verified_evidence[]`。单条 evidence 只包含 `source_type/source/scope/timestamp/excerpt/values`:RAG 展开稳定 document metadata/excerpt;日志展开 pattern/event 或带精确 query/time window 的零结果;MySQL 从 canonical request 取得逻辑 data source/SQL/params scope,从 projection 取得 columns/有界 rows 或零结果。快照不包含 Tool Call ID、Redis key、raw response 或未被 Draft 引用的调用。
|
||||
|
||||
Snapshot 同时可确定性提取去重的 `SafeFallback.VerifiedSource`,因此 release policy 无需读取 Store。替代通用 JsonNode 透传;拒绝该方案,因为它会扩大模型输入面并削弱字段级测试。
|
||||
|
||||
### 3. Evidence repair 只能改引用结构,不能成为报告作者
|
||||
|
||||
首次 Guard 失败后,`EvidenceRepair` 使用直接单轮 `ChatModel.call(Prompt)`,输入为原始 query、完整 Draft 和稳定 violation code/target;没有 Tool callback、memory 或 ReAct Agent。调用由 `context.retryPolicies().evidenceRepair()` 执行,其 `maxAttempts=1`,因此不能形成隐式 retry loop。
|
||||
|
||||
修复输出经过严格 `DiagnosisDraft` 解析后,转换为移除所有标识/引用字段的 `SemanticDraftView` 并与原 Draft 比较。Conclusion/Analysis/Action/Recommendation/Limitations 的文本、kind、顺序和人工确认标志有任何变化都拒绝;只允许 `analysis_id`、`tool_call_ids` 和 `based_on_analysis_ids` 修正。修复后完整重跑 EvidenceGuard,第二次仍失败直接 `EVIDENCE_VALIDATION_FAILED`。
|
||||
|
||||
替代方案是允许模型删除无效 Analysis 或改写结论;拒绝该方案,因为 ISS-014 固定 Diagnosis Agent 是唯一报告作者,Harness/repair 不能改变用户报告语义。
|
||||
|
||||
### 4. SemanticGuard 使用直接 ChatModel 单轮调用
|
||||
|
||||
`SemanticGuardInput` 包含原始 query、`SemanticDraftView` 和 verified snapshot。View 保留完整用户可见 Draft 语义和 Analysis IDs,但删除所有 Tool Call IDs。每个 attempt 都构造只含一个 system message 和一个 user JSON message 的新 `Prompt`,直接调用系统注入的同一 `ChatModel`;不构建 `ReactAgent`,因此没有 Tool、memory、checkpoint、Graph 或 Agent 回调。
|
||||
|
||||
输出只允许精确 JSON object `{verdict, reason}`,字段集合固定,verdict 只允许 `SUPPORTED|UNSUPPORTED`,reason 必须非空。SemanticGuard 不返回 corrected report。`UNSUPPORTED` 正常返回给 release policy,不能进入 retry classifier。
|
||||
|
||||
### 5. 单轮模型执行由共享受控边界管理
|
||||
|
||||
`GuardModelCall` 在每次调用前执行 `DiagnosisHarnessCore.beforeModelCall`,在模型实际返回后从 `ChatResponseMetadata.Usage` 记录 input/output Token,并再次检查 Run active。调用提交到注入的 `ExecutorService`,等待时间为 `min(singleAttemptTimeout, totalTimeoutRemaining)`;attempt 超时或 Run cancellation 时执行 `Future.cancel(true)`。模型即使忽略中断而迟到返回,后台任务仍记录真实 Token,并在 `core.checkActive` 处阻止迟到结果被使用。
|
||||
|
||||
SemanticGuard 通过既有 `HarnessRetryExecutor` 和 `context.retryPolicies().semanticGuard()` 执行。只有 timeout、transport、parse error、schema invalid 可进行第二次 attempt;两次使用完全相同的序列化输入。Run cancelled/budget exhausted 不转换为 Fallback,而是继续抛给阶段 6A 生命周期边界;其他最终技术失败映射 `SEMANTIC_UNAVAILABLE`。
|
||||
|
||||
### 6. Release policy 不携带未验证语义
|
||||
|
||||
`DiagnosisReleaseUseCase.execute(context, query, draft)` 固定顺序为 `EvidenceGuard -> optional repair -> EvidenceGuard -> SemanticGuard -> decision`。成功结果保留与 Guard 通过的同一个 Draft 实例/修复实例,不总结、裁剪或重排。
|
||||
|
||||
- `SUPPORTED`:`ReleaseOutcome.SUCCESS`,返回完整 Draft 和 verified snapshot。
|
||||
- `UNSUPPORTED`:`ReleaseOutcome.FALLBACK` + `SEMANTIC_UNSUPPORTED`。
|
||||
- SemanticGuard 最终技术失败:`ReleaseOutcome.FALLBACK` + `SEMANTIC_UNAVAILABLE`。
|
||||
- 首次修复调用失败、语义被改写或第二次 Guard 失败:`ReleaseOutcome.FALLBACK` + `EVIDENCE_VALIDATION_FAILED`。
|
||||
|
||||
`SafeFallbackFactory` 使用固定模板。Evidence failure 的 `verified_sources=[]`;Semantic fallback 只从 snapshot 提取来源。所有 Fallback 的 `conclusion=null`,且不接收 Draft 文本、SemanticGuard reason、供应商错误或异常作为模板参数。
|
||||
Fallback release result 不保留完整 snapshot,因为 snapshot 含 Analysis text;已验真来源只以 `SafeFallback.verified_sources` 的安全子集返回,防止误序列化内部 result 泄漏草稿。
|
||||
|
||||
## Module and Ownership Audit
|
||||
|
||||
```text
|
||||
query + DiagnosisDraft + RunContext
|
||||
-> DiagnosisReleaseUseCase
|
||||
-> EvidenceGuard -> CanonicalInvocationStore + Tool projection parsers
|
||||
-> optional EvidenceRepair -> GuardModelCall -> shared ChatModel
|
||||
-> SemanticGuardInput(snapshot + ID-free Draft)
|
||||
-> SemanticGuard -> HarnessRetryExecutor -> GuardModelCall -> shared ChatModel
|
||||
-> original Draft OR SafeFallbackFactory
|
||||
```
|
||||
|
||||
- `RunContext` owns deadline、cancellation、budget、retry policy 和 lifecycle;本阶段不创建第二套 Run 状态。
|
||||
- canonical store owns Tool 调用真实性;EvidenceGuard 只读并生成隔离 snapshot,不持有 Redis client。
|
||||
- Diagnosis Agent owns report semantics;repair 只修标识结构,SemanticGuard 只审查,release 只选择 Draft 或模板。
|
||||
- 最大耦合风险是阻塞式 `ChatModel` 的取消能力;受控 Future 可发出 interrupt,但 provider 是否立即终止取决于 SDK,故迟到结果必须由 Core active check 丢弃。
|
||||
- 无新 ADR:模块边界均来自 ISS-014 已冻结设计,且公开消费者尚未切换。
|
||||
|
||||
## Interface Impact
|
||||
|
||||
- Level: L2 internal interface。
|
||||
- 新增内部 records/interfaces/use cases;不修改现有 Java 方法签名。
|
||||
- 当前消费者仅 focused tests;阶段 6A 将装配真实 `DiagnosisAgentUseCase -> DiagnosisReleaseUseCase`。
|
||||
- HTTP/SSE、Controller DTO、JPA/Flyway、Redis key schema、旧 Chat/AiOps 行为均不变,无迁移或回滚数据操作。
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [Projection 字段遗漏导致误判] -> 三类显式 adapter、ID/status 双重校验和 contract fixture tests。
|
||||
- [Repair 改写报告] -> `SemanticDraftView` 确定性相等检查,任何可见差异直接 Evidence fallback。
|
||||
- [模型忽略 interrupt] -> Future cancel、迟到 Token 记录、Core active check;不允许迟到结果进入 release。
|
||||
- [业务 `UNSUPPORTED` 被重试] -> verdict 作为正常返回值,classifier 只处理异常。
|
||||
- [Fallback 泄漏草稿或内部 reason] -> 固定 factory 不接受这些参数,并对序列化结果做负向断言。
|
||||
- [输入快照过大] -> repair/SemanticGuard 独立 UTF-8 input/output limits,并占用 Run byte/model/token budget。
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. 新增内部 guard/release 包和聚焦测试,不连接公开入口。
|
||||
2. 阶段 6A 将内部 Chat application use case 装配到这些接口。
|
||||
3. 阶段 6B 在新 release boundary 通过后原子切换公开 SSE。
|
||||
4. 如阶段 5 回滚,只需移除新增内部包;阶段 4 和旧生产链路不受影响。
|
||||
|
||||
## Open Questions
|
||||
|
||||
- None。单次/总超时与字节预算由构造配置提供可调默认值,具体生产数值按阶段 7 Trace 校准,不构成本阶段方向问题。
|
||||
Reference in New Issue
Block a user