diff --git a/mvp/engineering/harness/Harness application+audit 学习笔记-从 Run 编排到可回放审计.md b/mvp/engineering/harness/Harness application+audit 学习笔记-从 Run 编排到可回放审计.md new file mode 100644 index 0000000..dc7b546 --- /dev/null +++ b/mvp/engineering/harness/Harness application+audit 学习笔记-从 Run 编排到可回放审计.md @@ -0,0 +1,166 @@ +# Harness application + audit 学习笔记:从 Run 编排到可回放审计 + +**更新日期**:2026-08-06 +**主题**:application 域(Run 全生命周期编排 + 安全落库)+ audit 域(可观测账本)——重点讲清 audit 与 trace 的设计与区别 +**配套**:[证据安全链笔记](Harness%20证据安全链学习笔记-从收敛控制到唯一发布点.md)、[执行控制笔记](Harness执行控制笔记-终态检查与取消广播.md) + +## 1. 一句话定位 + +```text +application = Run 应用所有者:创建 Run / 路由意图 / 执行分支 / 持久化 / SSE 输出 +audit = 可观测账本:Trace 时序回放 + Token 对账 + 各明细审计表(metadata-only) +``` + +## 2. application 域:ChatApplicationUseCase 六步编排 + +```mermaid +flowchart TD + A["Controller → execute(request, observer)"] --> B["① 读会话上下文
RoutingHistory + PreviousTurn"] + B --> C["② core.startRun
创建 Run 边界"] + C --> D["③ persistStart + observer.onStarted
(SSE metadata + 取消句柄 CoreRunControl)"] + D --> E["④ router.route
意图路由(单次模型调用)"] + E --> F["⑤ executePath 按 intent 分叉
SYSTEM_CHAT / KNOWLEDGE_QUERY / DIAGNOSIS"] + F --> G["⑥ completePath + persistFinish + 返回"] + G -.异常.-> H["统一失败出口
terminalOutcome + safeFailure"] +``` + +### 2.1 关键设计点 + +| 设计 | 代码事实 | 意义 | +|---|---|---| +| 取消句柄 | `observer.onStarted(new CoreRunControl(core, context))` → `core.cancel(context, CLIENT_DISCONNECTED)` | 客户端断连 → 取消广播 → 强杀 in-flight 调用 | +| 取消有原因 | `RunCancellationReason.CLIENT_DISCONNECTED` | 区分断连/用户取消,可审计 | +| sessionId 白名单 | `SAFE_ID = [A-Za-z0-9][A-Za-z0-9._-]{0,63}` | 信任边界校验 | +| 多轮记忆有界 | RoutingHistory(intent+query) + PreviousTurn(PublishedResult 有界化) | 上一轮只传安全摘要,无 tool ids / raw evidence | +| 预算终态特殊处理 | `handledBudgetTermination()` 时不二次 completeSuccess | 不破坏 first-terminal-wins | +| 统一失败出口 | terminalOutcome → CANCELLED/FAILED + ChatFailureCode(文案安全) | 不暴露内部堆栈 | +| 路由输出契约 | `OUTPUT_FIELDS = {intent}`,值必须枚举名 | 防模型夹带 | + +### 2.2 持久化(JpaChatRunStore + PublishedResultPolicy) + +- start / markIntent / finish(@Transactional),finish 写 outcome + safeContentJson + publishedResult; +- PublishedResultPolicy:**只有 SUCCESS 且 draft 有结论才构造 PublishedResult**;sanitize 全套有界(query 2000/结论 2000/scope 1000/limitations 10×500/文档 10); +- sourceDocuments 只收 RAG 类型证据的 document_id(去重)——MySQL/日志证据不进发布文档列表; +- PreviousTurn = sanitize 后的有界摘要(多轮记忆来源)。 + +## 3. audit 域:可观测账本的层次 + +```mermaid +flowchart LR + subgraph 写入侧["写入侧(不阻断主流程)"] + H1["HarnessAgentAuditHook
每模型步 → agent_step + agent_reasoning_audit"] + H2["ToolInvocationAuditSink
工具 → tool_invocation"] + H3["ModelCallAuditor
Token → ledger + core + agent_step 回写"] + H4["JpaDiagnosisTraceRecorder
事件 → diagnosis_trace_event"] + H5["JpaChatRunStore
Run → diagnosis_run"] + end + + subgraph 读取侧["读取侧(回放)"] + S["DiagnosisTraceService"] + S --> R["DiagnosisTraceResponse
timeline + steps + toolInvocations + run + summary"] + end + + H1 --> DB[(MySQL 各表)] + H2 --> DB + H3 --> DB + H4 --> DB + H5 --> DB + DB --> S +``` + +### 3.1 各落库点(谁写哪张表) + +| 落库点 | 表 | 内容 | +|---|---|---| +| HarnessAgentAuditHook | agent_step | 每模型步摘要(stepIndex/耗时/token/工具计划) | +| HarnessAgentAuditHook | agent_reasoning_audit | 推理 + assistant_text 正文(受限) | +| ToolInvocationAuditSink | tool_invocation | 工具入参/输出预览/检索明细 | +| JpaDiagnosisTraceRecorder | diagnosis_trace_event | 全链路事件时序线 | +| ModelCallAuditor | agent_step.token_count | Token 回写(仅 DIAGNOSIS_AGENT) | +| JpaChatRunStore | diagnosis_run / chat_session | Run 生命周期 + 发布契约 | + +### 3.2 audit 的三个核心边界(fail-safe / metadata-only / 对账) + +1. **审计不阻断主流程**:三个落库点全部 try-catch + log.warn——审计挂了不能让 Run 跟着挂; +2. **metadata-only 分层**:正文只允许出现在 agent_reasoning_audit(受限)和 tool_invocation(入参/输出预览),其余全部摘要化; +3. **Token 三写闭环**:ledger 分账 → core.recordTokens(Run 预算)→ agent_step.token_count 回写——审计与预算同源可对账。 + +## 4. audit vs trace:设计与区别(重点) + +### 4.1 核心区别:包含关系,不是并列 + +```text +audit = 域(可观测账本的总集合,17 个文件) + ├─ ★ trace = 域内的事件回放子体系(诊断时序线) + ├─ ModelCallLedger / Auditor(Token 记账) + ├─ HarnessAgentAuditHook(模型步审计) + ├─ ToolInvocationAuditSink(工具审计) + ├─ RagLookupAuditEnricher(RAG 检索审计) + └─ RunConclusionExtractor(结论提取) +``` + +**常见误解修正**:trace 不是「agent 执行记录」,而是**全链路七阶段时序线**(RUN/ROUTING/AGENT/TOOL/EVIDENCE/SEMANTIC/RELEASE);audit 也不只是「tool 审计」,tool 审计只是其中一小块。 + +### 4.2 一张表讲清区别 + +| 维度 | trace(diagnosis_trace_event) | audit(各明细账本) | +|---|---|---| +| 本质 | 时序事件流(按 sequence_no 排序) | 实体化明细记录 | +| 回答 | 发生了什么、按什么顺序 | 每个细节落在哪本账上 | +| 粒度 | 每帧只带摘要 + 关联键(step_id 等) | 完整字段(入参/输出/token/耗时) | +| 结构 | 一张表、一个序列 | 多张表(agent_step/tool_invocation/...) | +| 排序保证 | sequence_no 单调(ConcurrentHashMap 分配) | step_index / id / createdAt 排序 | +| 典型查询 | `findByRunIdOrderBySequenceNoAscIdAsc` | `findByRunIdOrderByStepIndex` 等 | +| 与明细的关系 | 靠 step_id / run_id 互链,不重复存储 | 承载被关联的实体数据 | + +### 4.3 真实数据对照(run 1b584a01) + +```text +trace(15 帧时序线): + RUN_STARTED → ROUTING_DECISION → AGENT_MODEL_STEP×2 → TOOL_INVOCATION + → EVIDENCE_GUARD_INITIAL → SEMANTIC_GUARD_DECISION → RELEASE_DECISION → RUN_FINISHED + +audit 各账本(同 Run): + agent_step 2 行(token 2461/4415,耗时 1418/8542ms) + agent_reasoning_audit 2 行(step1 = 完整 Draft JSON) + tool_invocation 1 行(lookup_knowledge,step_id=979) + diagnosis_run outcome=SUCCESS + published_result 完整 JSON +``` + +**关联示例**:trace 第 7 帧 TOOL_INVOCATION 的 details 里 `step_id=979` = agent_step.id=979 = tool_invocation.step_id——时序帧与明细账本通过 id 互链。 + +## 5. 可回放机制(DiagnosisTraceService) + +```text +GET /api/diagnosis/{sessionId}/trace?runId=xxx + → DiagnosisTraceResponse 七块:runId / chatSession / session / run / + steps[] / toolInvocations[] / timeline[] / summary + +GET /api/diagnosis/{sessionId}/trace/reasoning?runId=xxx(受限:runId 必填) + → agent_reasoning_audit(reasoning + assistantText) +``` + +三级回放深度:**时间线(timeline)→ 明细(steps/toolInvocations)→ 推理(reasoning,按需受限读取)**。 + +回放能成立的四个保证: +1. sequence_no 单调(索引 idx_trace_event_run_sequence); +2. 事件与明细靠 step_id 互链; +3. summary 的 persisted vs returned 双计数对账(发现落库不完整); +4. 双查询入口兼容新旧会话(buildLegacyTraceResponse)。 + +## 6. 面试话术(30 秒) + +> "application 域是 Run 的应用所有者:一次请求六步编排——建 Run 边界、意图路由(单次模型调用、输出契约严格为 {intent} 枚举)、按意图分叉执行、路径完成后写终态并持久化发布契约,异常统一走失败出口映射成安全的 ChatFailureCode;取消能力通过 SSE 句柄暴露给客户端(断连即 core.cancel)。**audit 域是可观测账本,trace 是它内部的事件回放子体系**:trace 用一张 diagnosis_trace_event 表按 sequence_no 记录全链路七阶段的事件时序线(每帧只带摘要和关联键),audit 的明细账本(agent_step / tool_invocation / agent_reasoning_audit / diagnosis_run)承载完整字段,两层通过 step_id/run_id 互链不重复存储。三个边界:审计不阻断主流程(fail-safe)、metadata-only(正文只在受限审计表)、Token 三写闭环(ledger 分账 → Run 预算 → 明细回写)。回放由 DiagnosisTraceService 按 runId 聚合排序,三级深度:时间线 → 明细 → 推理。" + +## 7. 代码位置索引 + +| 组件 | 文件 | +|---|---| +| ChatApplicationUseCase / ChatFailureCode / ChatApplicationStatus | `src/main/java/com/superbiz/agent/harness/application/` | +| DiagnosisChatExecutor / 其他 executor | `.../application/executor/` | +| IntentRouter / IntentRouterPrompt | `.../application/routing/` | +| JpaChatRunStore / PublishedResultPolicy / RoutingHistory | `.../application/persistence/` | +| Trace 体系(Recorder/Event/Type/Status/AuditEvents) | `src/main/java/com/superbiz/agent/harness/audit/`(前半) | +| ModelCallLedger / ModelCallAuditor / HarnessAgentAuditHook / RunConclusionExtractor | `.../audit/`(后半) | +| DiagnosisTraceService / DiagnosisTraceController | `src/main/java/com/superbiz/agent/service/` + `controller/` | +| V014 建表(diagnosis_trace_event) | `src/main/resources/db/migration/V014__create_diagnosis_trace_event.sql` | diff --git a/mvp/engineering/harness/Harness contract 状态流学习笔记-11个状态枚举的正交全景.md b/mvp/engineering/harness/Harness contract 状态流学习笔记-11个状态枚举的正交全景.md new file mode 100644 index 0000000..015ae51 --- /dev/null +++ b/mvp/engineering/harness/Harness contract 状态流学习笔记-11个状态枚举的正交全景.md @@ -0,0 +1,105 @@ +# Harness contract 状态流学习笔记:11 个状态枚举的正交全景 + +**更新日期**:2026-08-06 +**主题**:contract 域状态枚举全景——五层状态 / 正交维度 / 纵向映射链 / 真实数据案例 / 面试讲法 +**配套**:[证据安全链笔记](Harness%20证据安全链学习笔记-从收敛控制到唯一发布点.md)、[application+audit 笔记](Harness%20application+audit%20学习笔记-从%20Run%20编排到可回放审计.md) + +## 1. 一句话定位 + +**contract 状态流 = 11 个状态枚举按五层正交组织,每层回答一个独立问题;层间通过显式映射链串联——从技术终态到用户结局,从工具调用到发布裁决,全部类型化,杜绝字符串漂移。** + +## 2. 五层状态全景 + +| 层 | 枚举 | 值 | 回答的问题 | +|---|---|---|---| +| ① 技术层(core) | `RunState` | RUNNING / SUCCESS / FAILED / CANCELLED / TIMED_OUT / BUDGET_EXHAUSTED | Run 技术上是否还允许执行 | +| ② 证据层(tool/guard) | `InvocationStatus` | PROJECTING / READY / ERROR | 调用生命周期走到哪 | +| | `EvidenceStatus` | EVIDENCE_FOUND / NO_EVIDENCE / ERROR | 这次调用有没有拿到可引用证据 | +| | `AnalysisKind` | NORMAL / NEGATIVE_OBSERVATION | 分析条目是正向还是负向观察 | +| ③ 收集层(progress) | `DiagnosisStopReason` | INFORMATION_SATURATED / BUDGET_LIMIT_REACHED / PROTOCOL_VIOLATED | 证据收集为何受控停止 | +| | `SemanticVerdict` | SUPPORTED / UNSUPPORTED | 结论是否被证据支持 | +| ④ 发布层(release) | `ReleaseOutcome` | SUCCESS / FALLBACK / FAILED / CANCELLED | 用户侧内容结局 | +| | `FallbackType` | EVIDENCE_VALIDATION_FAILED / SEMANTIC_UNSUPPORTED / SEMANTIC_UNAVAILABLE / BUDGET_EXHAUSTED / INSUFFICIENT_EVIDENCE / MISSING_REQUIRED_CONTEXT | 为什么降级 | +| ⑤ 协议层 + 失败层 | `ChatApplicationStatus` | ROUTING / SYSTEM_RESPONDING / KNOWLEDGE_SEARCHING / KNOWLEDGE_ANSWERING / DIAGNOSIS_RUNNING / SAFETY_VALIDATING | 当前走到哪一阶段(进行中) | +| | `SseOutcome` | SUCCESS / FALLBACK / FAILED | done 事件粗粒度结局(**未接线**) | +| | `ChatFailureCode` | ROUTING_UNAVAILABLE / SYSTEM_CHAT_UNAVAILABLE / KNOWLEDGE_UNAVAILABLE / DIAGNOSIS_UNAVAILABLE / RUN_PERSISTENCE_FAILED / RUN_CANCELLED / INTERNAL_FAILURE | 失败时给客户端的粗粒度原因 | + +## 3. 正交维度(四个独立轴) + +```text +轴 1:RunState(技术终态)⊥ ReleaseOutcome(用户结局) + 一个 Run 技术停了,用户看到的可能是降级(有安全进展)或失败(没进展) +轴 2:InvocationStatus(调用生命周期)⊥ EvidenceStatus(证据语义) + 注释原话:一个是「投影中/就绪/错误」,一个是「有没有拿到可引用证据」 + NO_EVIDENCE 仍可能是 success 的工具执行(查了但空) +轴 3:ChatApplicationStatus(进度,进行中)⊥ 结局(终态) + 进度回答「走到哪」,结局回答「最终给什么」 +轴 4:IntentType(路由)——每次请求一个,决定走哪条分支 +``` + +## 4. 纵向映射链(代码事实) + +```text +RunState.BUDGET_EXHAUSTED ──hasObservedFacts()==true──▶ FALLBACK(INSUFFICIENT_EVIDENCE) + └──无 facts──▶ FAILED(fail closed) +RunState.CANCELLED ──▶ ReleaseOutcome.CANCELLED + ChatFailureCode.RUN_CANCELLED +内部失败 ──▶ RunState.FAILED + ReleaseOutcome.FAILED + INTERNAL_FAILURE +SemanticVerdict.SUPPORTED(guard 全过)──▶ ReleaseOutcome.SUCCESS(唯一出口) +FallbackType 任意值 ──▶ ReleaseOutcome.FALLBACK + +证据层内部约束: + AnalysisKind.NORMAL.accepts(EVIDENCE_FOUND) + AnalysisKind.NEGATIVE_OBSERVATION.accepts(NO_EVIDENCE) + InvocationStatus.READY 是 EvidenceGuard 可引用前提(isReferencableBy) + EvidenceStatus.ERROR 不是证据,不能支持分析/结论(prompt 原话) +``` + +## 5. 真实数据案例(run 1b584a01 的状态流转) + +| 阶段 | 状态值(真实 trace 佐证) | +|---|---| +| 路由 | IntentType=DIAGNOSIS(ROUTING_DECISION 事件 details) | +| Agent 执行 | RunState=RUNNING,ChatApplicationStatus: ROUTING → DIAGNOSIS_RUNNING → SAFETY_VALIDATING | +| 工具调用 | InvocationStatus: PROJECTING → READY;EvidenceStatus=EVIDENCE_FOUND | +| 分析 | AnalysisKind=NORMAL × 3(accepts EVIDENCE_FOUND) | +| 验真 | EVIDENCE_GUARD_INITIAL=PASSED(violations=0) | +| 语义裁决 | SEMANTIC_GUARD_DECISION=SUPPORTED | +| 发布 | RELEASE_DECISION=SUCCESS(= ReleaseOutcome.SUCCESS) | +| 终态 | RunState=SUCCESS | + +## 6. 面试怎么讲这个状态流设计 + +### 6.1 叙事模板(① 动机 → ② 决策 → ③ 实现 → ④ 边界 → ⑤ 话术) + +**① 动机**:Agent 系统里最容易被搞混的就是「状态」。技术停了(预算耗尽)不等于用户看到失败;查了没查到不等于系统出错;进行中不等于终态。如果所有状态塞进一个枚举,语义就糊了。 + +**② 决策**:**分层 + 正交 + 显式映射**——每个枚举只回答一个问题(技术/证据/收集/发布/协议各管各的),层间不隐式耦合,用明确的映射链串联。 + +**③ 实现**:11 个枚举五层,两个最典型的正交轴 + 一条纵向映射链(如上);全部类型化(enum/record),杜绝字符串漂移。 + +**④ 边界**:SSE 取消场景连接可能已断、发不出 done,所以协议层只有三态;`SseOutcome` 目前未接线(实现直接复用 ReleaseOutcome);`FallbackType.BUDGET_EXHAUSTED` 是命名债务(实际发布 INSUFFICIENT_EVIDENCE)。 + +**⑤ 话术(30 秒)**: + +> "Harness 的状态设计核心是**分层正交**:技术终态(RunState)和用户结局(ReleaseOutcome)是两个正交轴——预算耗尽且有安全进展时用户看到 FALLBACK 降级,没进展才是 FAILED,这样同一个技术终态可以诚实映射到不同用户结局;工具调用也是两个正交轴(InvocationStatus 生命周期 vs EvidenceStatus 证据语义),查了但空结果(NO_EVIDENCE)仍是成功执行。层间是显式映射链:BUDGET_EXHAUSTED+有事实→FALLBACK,CANCELLED→CANCELLED+RUN_CANCELLED,guard 全过→SUCCESS 唯一出口。协议层(SSE)复用 ReleaseOutcome 三态并拒绝 CANCELLED(取消时连接已断),SseOutcome 是预留未启用的抽象。" + +### 6.2 高频追问应对 + +| 追问 | 答 | +|---|---| +| RunState 和 ReleaseOutcome 有什么区别? | 技术 vs 用户:RunState 回答 Run 是否还允许执行;ReleaseOutcome 回答用户侧内容形态。BUDGET_EXHAUSTED 可以有 FALLBACK 或 FAILED 两种用户结局 | +| 为什么预算耗尽既可能 FALLBACK 又可能 FAILED? | `progress.hasObservedFacts()` 安全阀——有已验真事实才能发布降级,没有就 fail closed | +| NO_EVIDENCE 算失败吗? | 不算。NO_EVIDENCE 是成功执行但范围内无证据(查了但空),常记为信息无增益;ERROR 才是工具侧失败 | +| CANCELLED 为什么不在 SSE done 里? | 取消时客户端连接可能已断,发不出 done;所以协议层只有 SUCCESS/FALLBACK/FAILED 三态 | +| 这些状态为什么不直接用一个枚举? | 塞一个枚举语义就糊了——技术、证据、发布、协议回答的是不同问题,正交分层后各层可独立演进,映射显式可审计 | +| InvocationStatus 和 EvidenceStatus 会不会重复? | 分工明确:生命周期(投影中/就绪/错误)vs 证据语义(有没有证据);READY+NO_EVIDENCE 是完全合法的组合 | + +## 7. 代码位置索引 + +| 组件 | 文件 | +|---|---| +| 全部状态枚举与数据契约 | `src/main/java/com/superbiz/agent/harness/contract/` | +| RunState | `.../harness/core/RunState.java` | +| DiagnosisStopReason | `.../harness/progress/DiagnosisStopReason.java` | +| ChatApplicationStatus / ChatFailureCode | `.../harness/application/` | +| SSE 会话(done 事件拒绝 CANCELLED) | `.../controller/sse/ChatSseEvent.java` | diff --git a/mvp/engineering/harness/Harness 证据安全链学习笔记-从收敛控制到唯一发布点.md b/mvp/engineering/harness/Harness 证据安全链学习笔记-从收敛控制到唯一发布点.md new file mode 100644 index 0000000..8909e6e --- /dev/null +++ b/mvp/engineering/harness/Harness 证据安全链学习笔记-从收敛控制到唯一发布点.md @@ -0,0 +1,164 @@ +# Harness 证据安全链学习笔记:从收敛控制到唯一发布点 + +**更新日期**:2026-08-06 +**主题**:progress → guard → release 三域联动——双通道验证架构 + 状态流转全景 + 关键字段来源与使用 +**配套**:[progress 代码学习笔记](Harness%20progress%20代码学习笔记-从拦截器五道门到唯一发布点.md)、[Retry 重试机制](Retry重试机制-显式可计量的attempt循环.md)、[执行控制笔记](Harness执行控制笔记-终态检查与取消广播.md) + +## 1. 一句话定位 + +**证据安全链 = progress(执行期收敛控制)→ guard(验证)→ release(唯一发布点)**: +任何对外发布的内容,必须能追溯到 canonical 账本的已验证事实;任何无法证明的内容,只能以有界、诚实的 SafeFallback 降级形态出现。 + +## 2. 主链路图 + +```mermaid +flowchart LR + subgraph 执行期["Agent 执行期(core 控资源 + progress 控收敛)"] + TC["HarnessToolInterceptor
工具调用完成"] + TC -->|"完整记录"| CS["Canonical Store
(唯一真相源, TTL 2h)"] + TC -->|"identity"| TR["Progress Tracker
(toolCallId+toolName+scope)"] + end + + subgraph 停止["Agent 结束(所有出口触发投影)"] + TR -->|"回读验真"| PP["DiagnosisProgressProjector"] + PP --> PS["ProgressSnapshot
(observedFacts+limitations+stopReason)"] + end + + subgraph 裁决["Release 裁决(唯一发布点)"] + EX["DiagnosisAgentExecution
(draft / stopped)"] + EX --> RC["releaseConclusion
(有结论)"] + RC -->|"tool_call_ids"| EG["EvidenceGuard
机械验引用"] + EG -->|"失败"| ER["EvidenceRepair
只修引用→复查"] + EG -->|"通过"| VES["VerifiedEvidenceSnapshot"] + VES --> SG["SemanticGuard
判支持度"] + SG -->|"SUPPORTED"| SUCCESS["SUCCESS
(唯一出口)"] + SG -->|"UNSUPPORTED/不可用"| FB["SafeFallback 降级"] + EG -->|"仍失败"| FB + EX -->|"stopped / 无结论"| FB + PS -->|"降级原料"| FB + end + + CS -.->|"回读"| EG + CS -.->|"回读"| PP +``` + +## 3. 双通道验证架构(核心) + +两条并行的「账本背书」通道,合起来覆盖所有结局: + +| | progress 快照 | guard 快照 | +|---|---|---| +| 类 | `DiagnosisProgressSnapshot` | `VerifiedEvidenceSnapshot` | +| 组装时机 | agent 结束那一刻(所有出口) | release 验引用通过后 | +| 原料 | Tracker 的调用 identity | draft 里模型写的 tool_call_ids | +| 回读者 | `DiagnosisProgressProjector` | `EvidenceGuard` | +| 需要 draft | **不需要** | **必须** | +| 服务谁 | 受控停止/无结论/非法 draft 降级 | 有结论 draft 证据链 + SemanticGuard | +| 共同点 | 都从 canonical 回读、三重校验、去重有界、绝不输出 raw | 同左 | + +**设计意义**:agent 没给出合法 draft(预算打断、输出烂)时,靠 progress 快照降级;给出合法 draft 时,靠 guard 快照支撑。**任何情况下对外发布都有据可依。** + +## 4. 状态流转全景(技术终态 vs 用户终态) + +### 4.1 六层状态(从内到外) + +| 层 | 类型 | 值 | 回答的问题 | +|---|---|---|---| +| core 生命周期 | `RunState` | RUNNING / SUCCESS / FAILED / CANCELLED / TIMED_OUT / BUDGET_EXHAUSTED | Run 技术上是否还允许继续执行 | +| progress 收集停止 | `DiagnosisStopReason` | INFORMATION_SATURATED / BUDGET_LIMIT_REACHED / PROGRESS_PROTOCOL_VIOLATED | 证据收集为何受控停止 | +| release 发布裁决 | `ReleaseOutcome` | SUCCESS / FALLBACK / FAILED / CANCELLED | 用户侧内容结局是什么 | +| release 降级细分 | `FallbackType` | EVIDENCE_VALIDATION_FAILED / SEMANTIC_UNSUPPORTED / SEMANTIC_UNAVAILABLE / BUDGET_EXHAUSTED / INSUFFICIENT_EVIDENCE / MISSING_REQUIRED_CONTEXT | FALLBACK 为什么降级 | +| SSE 进度 | `ChatApplicationStatus` | ROUTING / DIAGNOSIS_RUNNING / SAFETY_VALIDATING ... | 当前走到哪一阶段(不是结局) | +| 对外失败码 | `ChatFailureCode` | RUN_CANCELLED / INTERNAL_FAILURE / ... | 失败时给客户端的粗粒度原因 | + +### 4.2 关键映射规则(代码事实) + +```text +RunState.BUDGET_EXHAUSTED + progress.hasObservedFacts()==true + → ReleaseOutcome.FALLBACK(FallbackType.INSUFFICIENT_EVIDENCE) +RunState.BUDGET_EXHAUSTED + 无 facts + → FAILED(fail closed:没有安全内容可发布) +RunState.CANCELLED → ReleaseOutcome.CANCELLED + ChatFailureCode.RUN_CANCELLED +内部失败(completeFailure)→ RunState.FAILED + ReleaseOutcome.FAILED + INTERNAL_FAILURE +guard 全过 → RunState.SUCCESS + ReleaseOutcome.SUCCESS(唯一出口) +预算耗尽且子路径已产出 FALLBACK → 不二次 completeSuccess +``` + +### 4.3 两个正交维度的区分(高频面试点) + +- **RunState** 回答「Run 技术上是否还在跑、因何技术终态停下」; +- **ReleaseOutcome** 回答「用户侧内容形态:正常报告 / 安全降级 / 失败 / 取消」; +- 常见组合:`RunState=BUDGET_EXHAUSTED` 且有安全进展 → `ReleaseOutcome=FALLBACK`;无进展 → `FAILED`。 + +### 4.4 终态异常透传 + +`DiagnosisReleaseUseCase.propagateTerminal`:`RunAbortedException` / `BudgetExceededException` / `RetryFailure.CANCELLED|BUDGET_EXHAUSTED` **原样上抛**,不吞、不伪装成业务 FALLBACK——取消和预算耗尽是 Run 的技术终态事实,用户侧必须知道「被取消了」而不是「诊断结论是不足」。 + +## 5. 关键字段的来源与使用 + +### 5.1 CanonicalToolInvocation(唯一真相源,执行时落库) + +| 字段 | 来源 | 使用 | +|---|---|---| +| tool_call_id + run_id | ToolBoundary 执行时生成 | key = runId + toolCallId(两个投影器都按它回读) | +| request / raw_response | 工具请求与原始返回 | 审计/验真,不外发 | +| agent_result | Projector 投影后的有界结果 | Agent 可见的唯一形态;EvidenceGuard 重读它 | +| status | PROJECTING → READY / ERROR(单向迁移) | isReferencableBy 要求 READY | +| evidence_status | FOUND / NO_EVIDENCE / ERROR | kind.accepts() 匹配、投影一致性校验 | +| error_code | 仅 ERROR 携带 | 稳定错误码 | +| started_at / completed_at | 生命周期时间戳 | TTL / 审计 | + +### 5.2 DiagnosisProgressSnapshot(progress 快照,agent 结束时组装) + +| 字段 | 来源 | 使用 | +|---|---|---| +| verifiedSources | Projector 回读 canonical,去重 | 降级时展示「查过哪些来源」 | +| observedFacts | 同上(≤12 条,摘要 320 字,空查询也算) | 「查了查到什么」;`hasObservedFacts()` 安全阀 | +| limitations | 无法验真/截断的诚实说明 | 降级时展示限制 | +| stopReason | Tracker 状态 | release 检查白名单后决定降级 | + +### 5.3 VerifiedEvidenceSnapshot(guard 快照,验真通过后组装) + +| 字段 | 来源 | 使用 | +|---|---|---| +| analyses[] | EvidenceGuard 重读 canonical agent_result 重建 | SemanticGuard.review 输入 | +| verifiedSources() | 方法去重(sourceType+source+scope) | SUCCESS 的 published_result.source_documents、FALLBACK 的来源 | + +### 5.4 SafeFallback(降级载荷,release 降级出口构造) + +| 字段 | 来源 | 使用 | +|---|---|---| +| type | SafeFallbackFactory 按场景 | 用户/审计区分降级原因 | +| conclusion | 恒 null | 降级绝不发布根因结论 | +| verified_sources / observed_facts | progress 或 guard 快照投影 | 保留已验证事实供用户继续排查 | +| limitations / next_steps | 工厂按场景拼装 | 诚实说明 + 下一步 | +| failure_stage | DIAGNOSIS_INPUT / DIAGNOSIS_COLLECTION / EVIDENCE_VALIDATION / SEMANTIC_VALIDATION | 定位失败阶段 | +| validation_issues | EvidenceViolation 映射 | EVIDENCE_VALIDATION_FAILED 时的违规明细 | + +## 6. 设计要点(贯穿全链的规律) + +1. **fail closed 贯穿每一层**:guard 验引用默认拒绝、读投影严格反序列化、release 无 facts 抛异常、SafeFallback 无事实拒绝构造——「不确定 → 默认拒绝,没查到永不伪装成结果」。 +2. **负向证据被完整建模**:progress 空查询投影成事实、NEGATIVE_OBSERVATION 配 NO_EVIDENCE、降级诚实声明「查到了但不够」。 +3. **canonical 唯一真相源**:链上不存在第二份工具真相;两个投影器共用同一套三重校验。 +4. **全链路可审计回放**:EVIDENCE_GUARD_INITIAL/RECHECK、SEMANTIC_ATTEMPT/DECISION、EVIDENCE_REPAIR_ATTEMPT、RELEASE_DECISION 全落 trace。 +5. **语义不变性**:EvidenceRepair 只修引用不修结论(prompt 锁死 + hasSameUserVisibleSemantics 校验)。 +6. **命名债务**:`FallbackType.BUDGET_EXHAUSTED` 枚举保留,但预算停实际发布 `INSUFFICIENT_EVIDENCE`(注释自认)。 +7. **重复验证**:同一 tool_call_id 被多条 analysis 引用会重复验 N 次(引用级独立校验的代价,账本查询便宜可接受)。 + +## 7. 面试话术(30 秒) + +> "Harness 的证据安全链是 progress → guard → release 三段联动。**执行期**:progress 管信息增益收敛(预算归 core),工具调用实时落 canonical 账本、Tracker 只记 identity;**验证期**:agent 结束后 release 编排——有结论的 draft 先进 EvidenceGuard 机械验引用(每个 tool_call_id 从账本回读、READY 且 kind 匹配,失败则 EvidenceRepair 只修引用再验),通过后 SemanticGuard 判结论是否被证据支持;**发布期**:SUPPORTED 是唯一 SUCCESS 出口,其余全部经 SafeFallbackFactory 构造有界诚实的降级。关键设计是**双通道**:没有合法 draft 时靠 progress 快照(observedFacts)降级,有 draft 时靠 guard 快照支撑——任何情况对外发布都有据可依;以及**状态正交**:RunState 回答技术终态(预算耗尽/取消),ReleaseOutcome 回答用户结局(降级/失败),取消和预算耗尽经 propagateTerminal 原样上抛,绝不伪装成业务降级。" + +## 8. 代码位置索引 + +| 组件 | 文件 | +|---|---| +| EvidenceGuard / EvidenceViolationCode | `src/main/java/com/superbiz/agent/harness/guard/evidence/` | +| SemanticGuard / GuardModelCall | `src/main/java/com/superbiz/agent/harness/guard/semantic/` | +| DiagnosisReleaseUseCase / EvidenceRepair / SafeFallbackFactory | `src/main/java/com/superbiz/agent/harness/release/` | +| DiagnosisProgressProjector / Tracker / Snapshot | `src/main/java/com/superbiz/agent/harness/progress/` | +| CanonicalToolInvocation / Store | `src/main/java/com/superbiz/agent/harness/tool/store/` | +| RunState | `src/main/java/com/superbiz/agent/harness/core/RunState.java` | +| DiagnosisStopReason | `src/main/java/com/superbiz/agent/harness/progress/DiagnosisStopReason.java` | +| ReleaseOutcome / FallbackType / SafeFallback | `src/main/java/com/superbiz/agent/harness/contract/` | +| ChatApplicationStatus / ChatFailureCode | `src/main/java/com/superbiz/agent/harness/application/` | diff --git a/mvp/engineering/harness/Harness 面试复习笔记-五步复习与白板图沉淀.md b/mvp/engineering/harness/Harness 面试复习笔记-五步复习与白板图沉淀.md new file mode 100644 index 0000000..5e7445b --- /dev/null +++ b/mvp/engineering/harness/Harness 面试复习笔记-五步复习与白板图沉淀.md @@ -0,0 +1,307 @@ +# Harness 面试复习笔记:五步复习与白板图沉淀(详细版) + +**更新日期**:2026-08-06 +**主题**:面试总复习成果固化——30 秒电梯陈述 / 三张白板图 / 九域五段式面试讲法 / 六个易错点 / 追问应对大全 / 支付超时案例 +**配套**:[面试速查](Harness面试速查-一张图讲清设计.md)、[设计演进](Harness设计演进-从多Agent编排到确定性控制边界.md)、各域学习笔记 + +## 1. 五步复习路径 + +```text +① 30 秒电梯陈述 + 一张图 +② 默画三张白板图(主链路 / 职责迁移 / 数据三层) +③ 六个易错点 +④ 2 分钟真实案例(支付超时) +⑤ 每域面试话术背诵(九域五段式) +``` + +## 2. 30 秒电梯陈述(详细版) + +### 2.1 一句话版本 + +> "Harness 是包围非确定性 Agent 的确定性控制边界。Diagnosis Agent 负责提出假设、选择 Tool、解释观察并生成 Draft;Harness 负责一次 Run 的身份、deadline、预算、取消、Tool 权限和事实保管,发布前再验证引用真实性与结论支持度。它不保证 Agent 每次都找到根因,但保证执行过程有边界、失败能够收敛,并且只有可验证的内容能够发布。" + +### 2.2 逐句展开(面试官追问「具体怎么做」时用) + +```text +「确定性控制边界」展开为三层边界: + 运行边界:RunContext(身份/截止/预算/取消)+ 唯一终态(first-terminal-wins) + 事实边界:ToolBoundary(权限/只读/容量)+ canonical 真相 + 有界观察 + 发布边界:EvidenceGuard 验引用 → SemanticGuard 判支持度 → Release 唯一出口 +``` + +### 2.3 三个重点(背的时候盯住) + +```text +Agent 负责业务推理(出草稿,不是出报告) +Harness 负责确定性约束(真相与观察分离) +Release 决定什么可以公开(引用真实与结论支持是两个独立门禁) +``` + +### 2.4 不要一开始列 10 个职责域 + +先给一句定义 + 三句话,面试官追问「具体怎么做」再沿三张白板图展开。 + +## 3. 三张白板图(详细版) + +### 3.1 图一:主链路(含分支,不是单轮) + +```text +chat 接口 + → 创建 RunContext(core.startRun:runId/deadline/budget/cancel/lifecycle) + → 意图识别(IntentRouter,单次模型调用,输出契约恰好 {intent} 枚举) + → 诊断 Agent(ReAct 多轮循环) + ├─ agent 决策 → ToolBoundary + │ ├─ preflight(run 匹配/授权/只读/JSON/key)→ 失败不落库 + │ ├─ 预算门禁(beforeToolCall + bytes 三笔预留) + │ ├─ canonical 状态机(begin PROJECTING → READY/ERROR) + │ ├─ 执行 + Projector 投影(严格校验 + 脱敏 + 截断) + │ └─ 有界观察返回 Agent(模型永远看不到 raw) + ├─ progress 判 GAINED/NO_GAIN(连续 NO_GAIN → 饱和停止) + └─ ↺ 循环直到:出草稿 或 受控停止(预算/饱和/协议违规) + → 分支 A(有结论草稿): + EvidenceGuard 验引用(key=runId+toolCallId 查账本,READY + kind 匹配) + ├─ 失败 → EvidenceRepair 只修引用(prompt 锁死 + 语义不变性)→ 复查 + │ └─ 仍失败 → SafeFallback(EVIDENCE_VALIDATION_FAILED) + └─ 通过 → SemanticGuard 判支持度(隔离守卫模型) + ├─ SUPPORTED → Release → SUCCESS(唯一出口) + └─ UNSUPPORTED/不可用 → SafeFallback(SEMANTIC_UNSUPPORTED/UNAVAILABLE) + → 分支 B(无草稿/无结论):Release 凭 progress 快照 → SafeFallback(INSUFFICIENT_EVIDENCE 等) + └─ 无安全进展 → fail closed 抛异常 → FAILED +``` + +**讲解要点**(讲主链路时抓四个时间点): +1. Agent 运行前先建立 Run 边界; +2. Tool 调用经过 Harness,但 Tool 选择仍由 Agent 决定; +3. Agent 只看到有界观察,完整事实由系统独立保管; +4. Draft 必须经过唯一发布出口,不能直接发送给用户。 + +**三个词记忆**:循环(多轮 ReAct + 收敛)/ 分支(验真失败有修复、无草稿也能降级)/ 分离(真相在 canonical,Agent 只见有界观察)。 + +### 3.2 图二:职责迁移(推理合并,权力拆分) + +**为什么迁移**:早期多 Agent(Planner/Executor/Verifier/Composer)加上 Gatekeeper、StateGraph,代价是四套 Prompt/JSON/上下文策略。后来发现四个角色**加起来正好是一次完整 ReAct**: + +```text +思考 → Planner +行动观察 → Executor + Tool +自我检查 → Verifier +最终回答 → Composer +``` + +外层在重复实现框架已有的循环。于是收敛成单个 React Agent + 控制面拆分: + +| 早期角色 | 干什么 | 现在迁移到哪 | +|---|---|---| +| Planner | 制定排查计划 | Diagnosis Agent(ReAct 思考) | +| Executor | 调用 Tool 收集证据 | Diagnosis Agent(ReAct tool 调用) | +| Gatekeeper | 机械验真证据引用 | EvidenceGuard(真理源改为 canonical store,对象改为 DiagnosisDraft) | +| Verifier | 判断 Claim 是否可信 | SemanticGuard(隔离,无 Tool 无记忆) | +| Composer | 组织最终回答 | Release(唯一发布点) | +| StateGraph | 显式状态/分支/终态 | Harness 状态分层(正交枚举 + first-terminal-wins) | +| 预算止损 | 防止空转 | Progress Control(信息增益收敛,从止损升级为正常收敛) | + +**两个洞察**: +1. 代码能机械证明的事实,不交给模型判断(Gatekeeper → EvidenceGuard 的思想延续); +2. 只有拥有不同数据权限、不同工具或真正独立业务目标的角色,拆成多 Agent 才值得——把一次 ReAct 的内部步骤外置成多角色,只会放大协议成本。 + +**结果**:业务推理合并回单个 Agent,但安全权力拆得更清楚——Agent 没有 canonical 读取权、不能自证引用、没有发布权。 + +### 3.3 图三:数据三层(一份工具结果,三个职责) + +```text +┌─ 第 1 层:canonical(Redis,TTL 2h,key=runId+toolCallId)────────┐ +│ request + raw_response + agent_result + status + evidenceStatus │ +│ 用途:EvidenceGuard 回读验真 / 短期完整真相 / 排查 │ +├─ 第 2 层:Model Observation(Projector 有界投影,冻结契约)──────┤ +│ 白名单 / 脱敏 / 截断,只含 Agent 下一步推理所需字段 │ +│ 用途:服务模型推理(不给 raw,防上下文膨胀/prompt injection) │ +├─ 第 3 层:Metadata Audit(MySQL 长期)────────────────────────────┤ +│ 只存 identity/状态/耗时/token/bytes,无正文无 raw 副本 │ +│ 用途:长期回放(配合 trace 时序线) │ +└───────────────────────────────────────────────────────────────────┘ +``` + +**三分字段**(一次调用): + +```text +request = 我问了什么(模型入参) +raw_response = 工具回了什么(完整返回,存 canonical 不外发) +agent_result = 我能信什么 / 模型能看到什么(投影后有界,供推理 + 验真) +``` + +**为什么不能共用一份数据**:raw 直接给模型 → 上下文膨胀 + 敏感泄漏 + prompt injection;只存裁剪观察 → EvidenceGuard 无法独立验真;raw 永久进审计 → 制造敏感副本。三个目标冲突,所以真相、观察、元数据各存各的。 + +**关键事实**:MySQL 不存完整工具返回和 agent_result——`JpaToolInvocationAuditSink` 落库时只提取 `output_preview`(默认仅 status/evidence_status)和 `output_length`(字节长度)。TTL 过期后只能元数据回放。 + +## 4. 九域五段式面试讲法 + +每域固定叙事结构:**① 动机 → ② 决策 → ③ 实现 → ④ 边界 → ⑤ 话术**,外加高频追问。 + +### 4.1 core:执行控制 + +- **① 动机**:非确定性 Agent 执行时,身份归属、截止时间、预算、取消、终态必须确定——异步调用和多轮会话中,事实到底属于哪次执行?迟到结果能不能发布? +- **② 决策**:RunContext 显式传递(不用 ThreadLocal);唯一终态 first-terminal-wins。 +- **③ 实现**:`checkActive` 三道闸(模型调用前/工具调用前/工具执行中逐行);取消广播(onCancel → future.cancel);deadline;RunBudget;预算耗尽/取消走终态。 +- **④ 边界**:协作式取消——同步 Provider 计算未必立即停止;终态防迟到发布但不物理强杀。 +- **⑤ 话术**: +> "core 管一次 Run 的确定性边界:显式 RunContext 跨线程传递(挂进 config metadata,防并发串线),first-terminal-wins 保证唯一终态——第一个写入的终态不可被迟到结果覆盖;checkActive 在模型前、工具前、工具执行中逐行检查,预算/取消/超时到点即 abort;取消是协作式的,逻辑终态和发布被保护,但同步 Provider 计算未必立即停。" +- **追问**:取消是强杀吗?(协作式,检查点中止 + 终态防迟到)RunContext 为什么显式?(跨线程 + 并发隔离)预算和 Ledger 区别?(Run 资源门禁 vs 审计账本) + +### 4.2 retry:显式可计量重试 + +- **① 动机**:框架/Agent 自带的重试是盲目重试——同一请求无限重试、不计量、不可审计,一个不可靠的工具能把整个 Run 预算耗光。 +- **② 决策**:重试权从框架收归 Harness,做成显式可计量的 attempt 循环。 +- **③ 实现**:分类裁决(技术性失败可重试 / 业务性失败不重试);次数/时间/成本三重封顶;剩余超时递减(总超时耗尽不再重试);attempt 可审计。 +- **④ 边界**:业务性失败不重试(重试也没用);SDK 关闭后由 Harness 全权控制。 +- **⑤ 话术**: +> "重试归 Harness 因为它是成本行为:框架自带的盲目重试不可计量不可审计,Harness 做成显式 attempt 循环——技术性失败才重试、业务性失败不重试,次数/时间/成本三重封顶,每次尝试有分类有记录可审计。Agent 和 Tool 不重试,它们只负责执行,要不要再来一次由 Harness 裁决。" +- **追问**:为什么 Agent/Tool 不重试?(重试是成本裁决权,执行层只管执行) + +### 4.3 progress:信息增益收敛 + +- **① 动机**:预算只能止损(不能继续消耗资源),不能判断「继续查是否有价值」——Agent 可能拿着通用知识、相似查询、空日志反复空转,最后撞预算。 +- **② 决策**:预算之外的第二套停止机制——信息增益控制。 +- **③ 实现**:GAINED/NO_GAIN 判定(结果是否推进诊断);重复检测;Tracker 双计数/pending;连续 NO_GAIN → SATURATED 饱和停止(软/硬停止);拦截器五道门。 +- **④ 边界**:SATURATED 只停收集,不是 Run 终态;`READY + NO_EVIDENCE` 是成功执行但空结果,不是技术异常。 +- **⑤ 话术**: +> "progress 是预算之外的第二套停止机制:预算管能不能继续消耗资源,progress 管继续查是否推进诊断。它用信息增益判定(GAINED/NO_GAIN)+ 重复检测 + 饱和停止——连续 NO_GAIN 就停,防止 Agent 拿通用知识或空日志空转;空查询(NO_EVIDENCE)也是被完整建模的负向观察,不是技术异常。" +- **追问**:Agent 为什么不会无限调用 Tool?(预算止损 + 信息增益收敛双保险) + +### 4.4 tool:事实边界 + +- **① 动机**:工具是证据边界——能查什么、查到多少、看到什么必须封死;工具直接连数据库/检索库有破坏面。 +- **② 决策**:ToolBoundary 统一执行规则 + canonical 存真相 + Projector 有界投影;数据三层分离。 +- **③ 实现**:preflight 五项(run 匹配/授权/只读/JSON/key)失败不落库;预算门禁(beforeToolCall + bytes 三笔);canonical 状态机(PROJECTING→READY/ERROR);审计 best-effort;每类工具一个 Projector(严格校验 + 脱敏 + 截断)。 +- **④ 边界**:不理解业务内容(投影交给 ToolResultProjector);不做信息增益判断(progress 的事);只允许 READY/ERROR 离开。 +- **⑤ 话术**: +> "tool 域是证据边界:ToolBoundary 统一四项职责——preflight(run 匹配/授权/只读/JSON 合法性/key 生成,失败不落库)、预算门禁(Tool 预算 + request→raw→agent_result 三笔字节预留)、canonical 状态机(PROJECTING→READY/ERROR)、审计。执行结果分三层:完整真相存 Redis canonical(2h),有界投影给模型,长期审计只留元数据。它明确不做业务投影和信息增益判断——那是 Projector 和 progress 的事。" +- **追问**:Tool 结果为什么不直接给模型?(三层数据责任:推理/验真/留存冲突);MySQL 沙箱怎么防?(语义/连接/输出三层防线) + +### 4.5 guard:证据安全链双闸 + +- **① 动机**:Agent 会撒谎——编造工具调用、夸大结论。不信任模型自述。 +- **② 决策**:双闸分离——EvidenceGuard 机械验引用真实(规则、可审计、不调模型),SemanticGuard 隔离判结论支持度(无工具无记忆的单轮二值判断)。 +- **③ 实现**:EvidenceGuard 三层校验(结构校验 → 逐条引用验真 key=runId+toolCallId 查账本 + isReferencableBy + kind 匹配 → 重读投影重建证据,20 个违规码);SemanticGuard 严格 schema(恰好 {verdict, reason})+ 预算/重试/取消全栈衔接。 +- **④ 边界**:EvidenceGuard 只问引用真不真,不问语义;SemanticGuard 不能探索事实、不能改写报告。 +- **⑤ 话术**: +> "防编造证据用双闸:EvidenceGuard 是机械验真——模型草稿里每个 tool_call_id 都要去 canonical 账本查到真实记录(key 绑定 runId 防跨 Run、记录必须 READY、kind 与证据语义匹配、投影内部自洽),纯规则可审计不调模型;SemanticGuard 是隔离语义审查——无工具、无记忆、单轮二值判断,只判结论是否被已验证证据支持,输出硬校验为 {verdict, reason} 两个字段。先机械后语义:引用假的直接拦,不浪费模型调用。" +- **追问**:为什么 EvidenceGuard 通过还要 SemanticGuard?(引用真实 ≠ 结论被支持:一个防编造证据,一个防夸大结论) + +### 4.6 release:唯一发布点 + +- **① 动机**:模型输出的是未经证明的断言,不能直接当答案返回。 +- **② 决策**:唯一发布点——SUCCESS 只有一条路径(验真过 + 语义支持),其余全降级 SafeFallback。 +- **③ 实现**:三分支决策树(受控停止凭 progress 快照 / 无结论验引用按进展降级 / 有结论走 Evidence→Repair→Semantic 链);fail closed(无安全进展抛异常);EvidenceRepair 只修引用(prompt 锁死 + 语义不变性检查);SafeFallbackFactory 五种降级(有界/去重/诚实)。 +- **④ 边界**:终态异常透传(取消/预算耗尽不伪装成业务 FALLBACK);SafeFallback conclusion 恒 null。 +- **⑤ 话术**: +> "release 是唯一发布点:任何对外内容必须经过验证。它按 Draft 形态三分支——受控停止(无草稿)凭 progress 快照发布 INSUFFICIENT_EVIDENCE,且必须有已验真事实否则 fail closed;无结论只验引用按进展降级;有结论走完整链——EvidenceGuard 验引用,失败则 EvidenceRepair 只修引用(prompt 锁死只能改引用字段 + 语义不变性保证用户可见内容不变)再复查,仍失败降级 EVIDENCE_VALIDATION_FAILED;验真通过后 SemanticGuard 判支持度,SUPPORTED 是唯一 SUCCESS 出口,其余降级。所有降级走 SafeFallbackFactory:有界、去重、诚实,保留已验证事实但不发布未证明的根因。" +- **追问**:FALLBACK 算成功还是失败?(正交:可 RunState.SUCCESS + FALLBACK,是安全发布结果不是失败) + +### 4.7 application:Run 应用所有者 + +- **① 动机**:一次请求从创建到公开结果需要编排:建 Run、路由、执行分支、持久化、SSE 输出。 +- **② 决策**:ChatApplicationUseCase 六步编排,不做业务判断,不把 HTTP/SSE 细节塞 Core。 +- **③ 实现**:startRun → 读会话上下文(RoutingHistory + PreviousTurn)→ 路由 → executePath 分支 → completePath + persistFinish;统一失败出口(terminalOutcome + safeFailure);取消句柄(CoreRunControl → core.cancel)。 +- **④ 边界**:路由只给枚举不执行;预算耗尽的 FALLBACK 不二次 completeSuccess。 +- **⑤ 话术**: +> "application 是 Run 的应用所有者:六步编排——建 Run 边界(core.startRun)、意图路由(单次模型调用、输出契约严格为 {intent} 枚举)、按意图分叉执行、路径完成后写终态并持久化发布契约,异常统一走失败出口映射成安全的 ChatFailureCode;取消能力通过 SSE 句柄暴露给客户端(断连即 core.cancel)。路由只回答走哪条分支,分支执行权在 executePath。" +- **追问**:多轮记忆怎么实现?(RoutingHistory + PreviousTurn,只传发布后的安全摘要) + +### 4.8 audit:可观测账本(含 trace) + +- **① 动机**:要能回放决策过程,又不永久保存敏感正文——两个目标冲突。 +- **② 决策**:metadata-only + trace 时序线 + Token 对账账本;audit 是域,trace 是域内子体系。 +- **③ 实现**:trace 17 种事件按 sequence_no 单调落 diagnosis_trace_event(七阶段:RUN/ROUTING/AGENT/TOOL/EVIDENCE/SEMANTIC/RELEASE);明细账本(agent_step/tool_invocation/agent_reasoning_audit/diagnosis_run)承载完整字段;Token 三写闭环(ledger 分账 → Run 预算 → agent_step 回写);DiagnosisTraceService 三级回放。 +- **④ 边界**:审计不阻断主流程(fail-safe);正文只在受限审计表;TTL 过期后只能元数据回放。 +- **⑤ 话术**: +> "audit 是可观测账本,trace 是它内部的事件回放子体系。trace 用一张表按 sequence_no 记录全链路七阶段的事件时序线(每帧只带摘要和关联键),明细账本(agent_step/tool_invocation/agent_reasoning_audit/diagnosis_run)承载完整字段,两层通过 step_id/run_id 互链不重复存储。三个边界:审计不阻断主流程(fail-safe)、metadata-only(正文只在受限审计表)、Token 三写闭环(ledger 分账→Run 预算→明细回写)。回放三级:时间线→明细→推理。" +- **追问**:audit 和 trace 什么关系?(包含关系:trace 是 audit 域内的时序事件流,audit 还含 ledger/工具审计/推理审计) + +### 4.9 contract:状态流正交 + +- **① 动机**:技术停了不等于用户看到失败;查了没查到不等于系统出错——状态语义混在一个枚举里就糊了。 +- **② 决策**:分层 + 正交 + 显式映射。 +- **③ 实现**:11 个状态枚举五层(技术 RunState / 证据 InvocationStatus+EvidenceStatus / 收集 StopReason / 发布 ReleaseOutcome+FallbackType / 协议 ChatApplicationStatus+SseOutcome+ChatFailureCode);四个正交轴;纵向映射链。 +- **④ 边界**:SSE 只有三态(取消连接已断发不出 done);SseOutcome 未接线;FallbackType.BUDGET_EXHAUSTED 命名债务。 +- **⑤ 话术**: +> "状态设计核心是分层正交:RunState(技术终态)和 ReleaseOutcome(用户结局)是正交轴——预算耗尽有安全进展→FALLBACK、没进展→FAILED,同一技术终态诚实映射到不同用户结局;工具调用也是两个正交轴(InvocationStatus 生命周期 vs EvidenceStatus 证据语义),查了但空仍是成功执行。映射链:CANCELLED→CANCELLED+RUN_CANCELLED,SUPPORTED→SUCCESS 唯一出口;协议层(SSE)复用 ReleaseOutcome 三态拒绝 CANCELLED。" +- **追问**:这些状态为什么不合并成一个枚举?(不同层回答不同问题,正交后独立演进、映射显式可审计) + +## 5. 六个易错点(带「为什么」) + +| # | ❌ 不说 | ✅ 应说 | 为什么 | +|---|---|---|---| +| 1 | Harness 安排 Agent 执行步骤 | ReAct Agent 自己选 Tool 和下一步;Harness 只管边界 | 边界 ≠ 编排:Harness 不替 Agent 决定查什么 | +| 2 | SemanticGuard 是第二个诊断 Agent | 无 Tool、无记忆、单轮二值判断的隔离审查器 | 职责 ≠ 角色:它没有探索能力,判完就走 | +| 3 | Tool 返回 SUCCESS 就找到证据 | READY 只表示调用完成,还要看 EvidenceStatus | 生命周期 ≠ 证据:调完成功和有没有证据是两回事 | +| 4 | FALLBACK 就是 Run 失败 | Fallback 是安全发布结果,可 RunState.SUCCESS + FALLBACK | 技术 ≠ 用户:两个正交维度 | +| 5 | Redis 是长期审计库 | canonical 只存当前 Run 短期真相(TTL 2h);长期审计只留元数据 | 短期真相 ≠ 长期审计:可验真 vs 不留敏感副本 | +| 6 | 取消能立刻杀死所有模型调用 | 协作式取消:终态防迟到发布,同步 Provider 未必立即停 | 逻辑保护 ≠ 物理强杀:终态定了但线程未必立刻停 | + +## 6. 高频追问应对大全 + +| 追问 | 回答主线 | +|---|---| +| 什么是 Harness?30 秒讲清 | 确定性控制边界:运行/事实/发布三层边界 | +| 为什么不用多 Agent? | 四角色加起来是一次 ReAct;推理合并、权力拆分 | +| 为什么 Harness 不是工作流引擎? | Agent 选择下一步,Harness 只检查边界和发布资格 | +| RunContext 为什么显式传递? | 跨线程异步链 + 并发隔离,ThreadLocal 会丢/串 | +| 取消是强杀吗? | 协作式:检查点中止 + first-terminal-wins 防迟到 | +| 预算和 Ledger 区别? | Run 资源门禁 vs 审计账本(不同域) | +| FALLBACK 算成功还是失败? | 正交:技术终态(RunState)与用户结局(ReleaseOutcome) | +| 重试为什么归 Harness? | 盲目重试不可计量;显式可计量 attempt 循环 | +| 为什么 Agent/Tool 不重试? | 重试是成本裁决权,执行层只管执行 | +| 如何防止 Agent 编造证据? | framework Tool ID + canonical store + EvidenceGuard | +| 为什么双闸? | 引用真实(机械)≠ 结论被支持(语义) | +| Tool 结果为什么不直接给模型? | 真相/观察/审计三个数据责任冲突 | +| Agent 为什么不会无限调 Tool? | 预算止损 + 信息增益收敛双保险 | +| Tool 报错是不是 Run 就失败? | 局部失败先看是否可继续及是否已有安全进展 | +| 如何回放决策? | metadata audit + trace 时序线 + 三级回放 | +| 当前还有什么限制? | 语义去重、TTL、同步取消、SemanticGuard 不确定性、阈值校准 | + +## 7. 支付超时案例(2 分钟完整版) + +> "用户要求诊断支付服务超时。Application 先创建独立 Run,为 Router、Agent、Tool、Guard 共享同一套 deadline、预算和取消能力。Diagnosis Agent 自主调用知识库和日志 Tool;ToolBoundary 执行调用并把完整事实保存为当前 Run 的 canonical invocation,只把有界 Observation 返回给 Agent。Agent 根据两个 Tool 结果生成 Draft,但 Draft 没有直接发给用户。EvidenceGuard 回读 canonical store 后发现引用无法完成真实性校验,因此 Release 没有继续让模型润色或猜测,而是发布 EVIDENCE_VALIDATION_FAILED SafeFallback。最终数据库记录请求处理成功、ReleaseOutcome 为 FALLBACK,SSE 也完整结束,但未经验证的根因没有离开系统。" + +**三个不等于**:Tool READY ≠ 引用已验真 / 引用已验真 ≠ 结论被支持 / Agent 生成 Draft ≠ 报告允许发布。 + +## 8. 复习中纠正的认知清单(最容易踩的坑) + +| 错误认知 | 纠正为 | +|---|---| +| Harness 顶层所以控制 retry/progress | 不只是位置——重试是成本行为必须可计量封顶;progress 解决「预算不能判断价值」 | +| RunContext 因为「回调」显式传 | 跨线程异步链 + 并发隔离;显式挂 config metadata | +| ToolBoundary 管 token/收敛/重试次数 | 那些归 core/retry/progress;ToolBoundary 只四项(preflight/预算/状态机/审计) | +| 工具失败抛异常 | ToolBoundary 转 ERROR 状态 + 错误观察,Agent 可继续换工具;Run 是否失败看 hasObservedFacts | +| FALLBACK 可能因超时 | 超时(TIMED_OUT)通常走 FAILED;FALLBACK 前提是有已验证事实 | +| agent_result 也存 MySQL | 不存——MySQL 只留 output_preview + output_length;完整 agent_result 在 Redis canonical(2h) | +| 注入 skill/知识域给 agent | 注入的是 query + PreviousTurn + 系统 prompt;知识靠工具主动查 | +| 非法 draft 由 release 捕捉 | 反序列化在 agent 出口(recoverInvalidDraft);release 只做验证+裁决 | +| preflight 失败也落库 | 失败不落库(errorAndNoRecord)——只有 preflight 全过才写 PROJECTING | +| 多 Agent 一定不好 | 只有不同数据权限/独立业务目标的角色才值得拆;拆 ReAct 内部步骤只会放大协议成本 | + +## 9. 面试前一天 Checklist + +```text +□ 30 秒电梯陈述背熟(§2.1),三个重点不丢(§2.3) +□ 默画三张白板图(§3):主链路含分支 / 职责迁移对照 / 数据三层 +□ 六个易错点扫一遍(§5)——重点看「为什么」列 +□ 九域话术:挑 3 个最可能被追问的(guard/release/contract)背熟 +□ 2 分钟支付超时案例 + 三个不等于(§7) +□ 过一遍纠正认知清单(§8)——这些是踩过的坑 +□ 读一遍面试速查 §7 追问表,心里有数 +``` + +## 10. 代码位置索引 + +| 组件 | 文件 | +|---|---| +| ToolBoundary(preflight/预算/状态机/审计) | `src/main/java/com/superbiz/agent/harness/tool/boundary/ToolBoundary.java` | +| JpaToolInvocationAuditSink(output_preview 落库) | `.../audit/JpaToolInvocationAuditSink.java` | +| DiagnosisAgentUseCase(RunContext 挂 config metadata) | `.../agent/DiagnosisAgentUseCase.java` | +| EvidenceGuard / SemanticGuard | `.../guard/evidence/` + `.../guard/semantic/` | +| DiagnosisReleaseUseCase / EvidenceRepair / SafeFallbackFactory | `.../release/` | +| DiagnosisProgressProjector / Tracker / Snapshot | `.../progress/` | +| ChatApplicationUseCase / IntentRouter | `.../application/` | +| DiagnosisTraceService / DiagnosisTraceController | `.../service/` + `.../controller/` | +| 状态枚举(RunState/ReleaseOutcome/FallbackType/...) | `.../contract/` + `.../core/RunState.java` | diff --git a/mvp/engineering/harness/Harness组件学习路线-进度追踪.md b/mvp/engineering/harness/Harness组件学习路线-进度追踪.md index d95cc7d..9153cf7 100644 --- a/mvp/engineering/harness/Harness组件学习路线-进度追踪.md +++ b/mvp/engineering/harness/Harness组件学习路线-进度追踪.md @@ -82,12 +82,12 @@ |---|---|---|---|---| | `core` | **执行控制**:身份 / deadline / 预算 / 取消 / 唯一终态,checkActive 三道闸 | ✅ 深入 | RunContext、budget、cancel、lifecycle、checkActive、termination | [执行控制笔记](Harness执行控制笔记-终态检查与取消广播.md)、[RunBudget 时序图](RunBudget预算流程-一次Run的资源门禁时序图.md) | | `retry` | **显式可计量重试**:分类裁决(技术/业务)、次数/时间/成本三重封顶、attempt 可审计 | ✅ 深入 | 设计动机、分类裁决、剩余超时、幂等性、SDK 关闭 | [Retry 重试机制](Retry重试机制-显式可计量的attempt循环.md) | -| `contract` | **跨层类型化语言**:Draft / PublishedResult / SafeFallback / 状态枚举,防字符串漂移 | ⬜ 部分 | RunState / ReleaseOutcome / SseOutcome / PublishedResult(只摸过枚举) | 状态流(未系统学) | +| `contract` | **跨层类型化语言**:Draft / PublishedResult / SafeFallback / 状态枚举,防字符串漂移 | ✅ 深入 | 11 个状态枚举五层全景、四个正交轴(RunState⊥ReleaseOutcome、InvocationStatus⊥EvidenceStatus)、纵向映射链、SseOutcome 未接线发现 | [状态流笔记](Harness%20contract%20状态流学习笔记-11个状态枚举的正交全景.md) | | `agent` | **框架 ReAct 接入**:拦截器把预算/审计/停止协议挂到框架循环上,不复制 loop | ⬜ 部分 | HarnessModelInterceptor(预算/Token 记账) | — | -| `audit` | **可观测账本**:Trace 事件回放、Token 对账、metadata-only(不存敏感正文) | ⬜ 部分 | ModelCallLedger / ModelCallAuditor | — | -| `application` | **Run 应用所有者**:创建 Run / 路由意图 / 执行分支 / 持久化 / SSE 输出 | ⬜ 部分 | ChatApplicationUseCase 入口(cancel 链路) | — | -| `guard` | **验证分离**:EvidenceGuard 机械验引用真实性 + SemanticGuard 隔离判结论支持度 | ⬜ 部分 | GuardModelCall(预算/超时/取消订阅) | — | -| `release` | **唯一发布点**:SUCCESS / FALLBACK 裁决,EvidenceRepair 只修引用,SafeFallback 确定性构造 | ⬜ 部分 | DiagnosisReleaseResult(结果类型) | — | +| `audit` | **可观测账本**:Trace 事件回放、Token 对账、metadata-only(不存敏感正文) | ✅ 深入 | trace 时序线(15 帧真实数据)、Ledger 分账、模型步审计 hook、RunConclusionExtractor、DiagnosisTraceService 三级回放 | [application+audit 笔记](Harness%20application+audit%20学习笔记-从%20Run%20编排到可回放审计.md) | +| `application` | **Run 应用所有者**:创建 Run / 路由意图 / 执行分支 / 持久化 / SSE 输出 | ✅ 深入 | 六步编排、取消句柄(CoreRunControl)、统一失败出口、多轮记忆有界化、PublishedResultPolicy 落库 | [application+audit 笔记](Harness%20application+audit%20学习笔记-从%20Run%20编排到可回放审计.md) | +| `guard` | **验证分离**:EvidenceGuard 机械验引用真实性 + SemanticGuard 隔离判结论支持度 | ✅ 深入 | 20 个违规码、三层校验(结构/验真/重读投影)、语义不变性、守卫模型受控调用、全栈衔接 | [证据安全链笔记](Harness%20证据安全链学习笔记-从收敛控制到唯一发布点.md) | +| `release` | **唯一发布点**:SUCCESS / FALLBACK 裁决,EvidenceRepair 只修引用,SafeFallback 确定性构造 | ✅ 深入 | 三分支决策树、fail closed、终态透传、EvidenceRepair 语义不变性、SafeFallbackFactory 五种降级 | [证据安全链笔记](Harness%20证据安全链学习笔记-从收敛控制到唯一发布点.md) | | `tool` | **证据边界**:ToolBoundary 统一执行规则、canonical 保存真相、projector 有界投影、MySQL 只读沙箱 | ✅ 深入 | 49 文件全注释、Boundary/Contract/Projection/Store/Adapter/注册链、RAG 后端(L0/RRF/qualityScore/降级)、MySQL 沙箱(Validator/Executor/Projector 三层防线) | [tool 域注册调用执行链路](Harness%20tool%20域代码学习笔记-工具的注册调用与执行链路.md)、[Tool 调用链旅程](Harness%20Tool%20调用链-一次工具调用的完整旅程.md)、[RAG 检索体系](Harness%20RAG%20检索体系学习笔记-从%20query%20到可验证证据.md)、[MySQL 沙箱](Harness%20MySQL%20沙箱学习笔记-从%20SQL%20校验到脱敏投影.md) | | `progress` | **收敛控制**:信息增益(GAINED/NO_GAIN)、重复检测、饱和停止(预算之外的第二套停止机制) | ✅ 深入 | 设计动机、Tracker 双计数/pending/软硬停止、拦截器五道门、canonical 生命周期、Projector 投影、Release 消费 | [代码学习笔记](Harness progress 代码学习笔记-从拦截器五道门到唯一发布点.md)(与[设计视角](Harness信息增益停止-让无证据诊断正常收敛.md)配套) | @@ -106,6 +106,10 @@ | [Harness Tool 调用链-一次工具调用的完整旅程](Harness%20Tool%20调用链-一次工具调用的完整旅程.md) | 动态时序:模型决定 → 拦截器 → invoke → Adapter → ToolBoundary → 返回 → 模型观察 | ✅ 已沉淀 | | [Harness RAG 检索体系学习笔记-从 query 到可验证证据](Harness%20RAG%20检索体系学习笔记-从%20query%20到可验证证据.md) | RAG 后端:L0/多路召回+RRF/qualityScore/去重判级/降级/契约/审计/离线评测/讨论沉淀 | ✅ 已沉淀 | | [Harness MySQL 沙箱学习笔记-从 SQL 校验到脱敏投影](Harness%20MySQL%20沙箱学习笔记-从%20SQL%20校验到脱敏投影.md) | MySQL 工具:三层防线(语义/连接/输出)/白名单/参数化强制/取消联动/脱敏/与 RAG 对照 | ✅ 已沉淀 | +| [Harness 证据安全链学习笔记-从收敛控制到唯一发布点](Harness%20证据安全链学习笔记-从收敛控制到唯一发布点.md) | progress→guard→release 联动:双通道验证架构/六层状态流转与映射/关键字段来源与使用/设计要点/面试话术 | ✅ 已沉淀 | +| [Harness application+audit 学习笔记-从 Run 编排到可回放审计](Harness%20application+audit%20学习笔记-从%20Run%20编排到可回放审计.md) | application 六步编排/取消句柄/持久化策略;audit 域层次(trace 子体系/Ledger/审计表);**audit vs trace 区别**(真实数据对照)/三级回放 | ✅ 已沉淀 | +| [Harness contract 状态流学习笔记-11个状态枚举的正交全景](Harness%20contract%20状态流学习笔记-11个状态枚举的正交全景.md) | 五层状态/四正交轴/纵向映射链/真实数据案例/面试叙事模板与追问应对 | ✅ 已沉淀 | +| [Harness 面试复习笔记-五步复习与白板图沉淀](Harness%20面试复习笔记-五步复习与白板图沉淀.md) | **详细版**:30 秒陈述展开/三张白板图/九域五段式讲法(动机→决策→实现→边界→话术)/六易错点带原因/追问应对大全/支付超时案例/纠正认知清单/面试 Checklist | ✅ 已沉淀 | ## 3. 一次请求的完整学习主线 @@ -114,25 +118,27 @@ flowchart LR A["core
执行控制 ✅"] --> B["retry
重试 ✅"] B --> C["progress
信息增益 ✅"] C --> D["tool
事实边界 ✅"] - D --> E["guard
验证 ⬜"] - E --> F["release
发布 ⬜"] - F --> G["application + audit
收尾 ⬜"] - G --> H["contract
类型化语言 ⬜"] + D --> E["guard
验证 ✅"] + E --> F["release
发布 ✅"] + F --> G["application + audit
收尾 ✅"] + G --> H["contract
类型化语言 ✅"] ``` ## 4. 下一步规划 ```text -tool 域 ✅ 完成(49 文件全注释 + 5 篇笔记:注册执行链路 / Tool 调用链 / RAG 检索体系 / MySQL 沙箱) +主线九域全部 ✅ + 面试五步复习 ✅(共沉淀 13 篇笔记) -下一个:guard(15 个文件:EvidenceGuard + SemanticGuard) - —— 证据安全链核心:机械验引用真实性 + 语义判结论支持度 - —— 面试高频:如何防止 Agent 编造证据 +面试前一天建议: + 1. 重读「面试速查」§1-2 + §8(30 秒陈述 / 一张图 / 六易错点) + 2. 默画三张白板图(复习笔记 §3) + 3. 背诵每域 30 秒话术(复习笔记 §5) + 4. 过一遍纠正的认知清单(复习笔记 §6,最容易踩的坑) + 5. 2 分钟支付超时案例(复习笔记 §7) -之后顺序: - release(6 个文件,小而关键:唯一发布点——已接触 DiagnosisReleaseUseCase) - 补 application(路由/执行器/SSE 收尾)和 audit(Trace 回放) - 最后状态流(RunState ↔ ReleaseOutcome ↔ SseOutcome 正交全景) +可选深化(不阻塞面试): + 1. agent 域收尾:HarnessModelInterceptor / HarnessToolInterceptor 装配细节 + 2. audit 域深化:RagLookupAuditEnricher 检索审计明细 ``` ## 5. 建议每次学完一个域后更新 diff --git a/src/main/java/com/superbiz/agent/harness/contract/SafeFallback.java b/src/main/java/com/superbiz/agent/harness/contract/SafeFallback.java index 47be2f8..1fec76e 100644 --- a/src/main/java/com/superbiz/agent/harness/contract/SafeFallback.java +++ b/src/main/java/com/superbiz/agent/harness/contract/SafeFallback.java @@ -4,15 +4,41 @@ import com.fasterxml.jackson.annotation.JsonProperty; import java.util.List; +/** + * 安全回退契约(Release 层 FALLBACK 的对外载荷):不发布根因报告时, + * 把「为什么降级 + 已验证事实 + 下一步建议」结构化地交给用户。 + * + *

设计要点: + *

+ * + *

构造:仅 {@code SafeFallbackFactory}(release 域); + * 包装对外:{@code FallbackContent}(application 域); + * 审计提取:{@code RunConclusionExtractor}(audit 域)。 + */ public record SafeFallback( + /** 降级细分类型:EVIDENCE_VALIDATION_FAILED / SEMANTIC_UNSUPPORTED / ... */ @JsonProperty("type") FallbackType type, + /** 恒为 null(降级不发布根因结论,保留字段仅为契约完整性)。 */ @JsonProperty("conclusion") String conclusion, + /** 一句话降级原因(用户可读)。 */ @JsonProperty("message") String message, + /** 已验证来源(去重):查过哪些来源。 */ @JsonProperty("verified_sources") List verifiedSources, + /** 限制声明:检查范围 / 缺失项 / 降级原因。 */ @JsonProperty("limitations") List limitations, + /** 下一步建议(用户可执行)。 */ @JsonProperty("next_steps") List nextSteps, + /** 失败阶段标识:DIAGNOSIS_INPUT / DIAGNOSIS_COLLECTION / EVIDENCE_VALIDATION / SEMANTIC_VALIDATION。 */ @JsonProperty("failure_stage") String failureStage, + /** 已观察事实(去重、有界):每条 = 来源 + 范围 + 摘要,供继续排查。 */ @JsonProperty("observed_facts") List observedFacts, + /** 验证违规明细(EVIDENCE_VALIDATION_FAILED 时携带 code + target)。 */ @JsonProperty("validation_issues") List validationIssues) { public SafeFallback { @@ -33,12 +59,14 @@ public record SafeFallback( null, List.of(), List.of()); } + /** 已验证来源:来源类型 + 来源 + 范围(发布层可对外展示的最小来源单位)。 */ public record VerifiedSource( @JsonProperty("source_type") String sourceType, @JsonProperty("source") String source, @JsonProperty("scope") String scope) { } + /** 已观察事实:来源类型 + 来源 + 范围 + 有界摘要(一条工具调用结果投影)。 */ public record ObservedFact( @JsonProperty("source_type") String sourceType, @JsonProperty("source") String source, @@ -46,6 +74,7 @@ public record SafeFallback( @JsonProperty("summary") String summary) { } + /** 验证违规明细:违规码 + 目标字段(EVIDENCE_VALIDATION_FAILED 时携带)。 */ public record ValidationIssue( @JsonProperty("code") String code, @JsonProperty("target") String target) { diff --git a/src/main/java/com/superbiz/agent/harness/guard/evidence/EvidenceGuard.java b/src/main/java/com/superbiz/agent/harness/guard/evidence/EvidenceGuard.java index c72cbc6..ceb1ffb 100644 --- a/src/main/java/com/superbiz/agent/harness/guard/evidence/EvidenceGuard.java +++ b/src/main/java/com/superbiz/agent/harness/guard/evidence/EvidenceGuard.java @@ -26,6 +26,27 @@ import java.util.Map; import java.util.Objects; import java.util.Set; +/** + * 证据机械验真(证据安全链第 1 道闸):不信任模型自述,以 Canonical Store 账本为准。 + * + *

职责:校验 DiagnosisDraft 结构合法 + 每个 tool_call_id 引用真实可查 + * + 投影内部自洽,并把账本投影「重读重建」为 VerifiedEvidence 快照。 + * + *

三个阶段: + *

    + *
  1. {@link #validateDraft}:草稿结构完整性(analysis 非空、id 唯一、kind/text/引用齐全、limitations 必填);
  2. + *
  3. {@link #verifyInvocation}:引用验真(key=runId+toolCallId 查账本、记录必须 READY、 + * 同 Run、agent_result 非空、kind 与证据语义匹配、工具受支持);
  4. + *
  5. readRag/readLogs/readMysql:严格反序列化投影并校验内部一致性, + * 重读重建 VerifiedEvidence(证据内容来自账本,不是模型复述)。
  6. + *
+ * + *

纯规则门控、不调大模型:20 个违规码全部可枚举可审计; + * 产出 {@link VerifiedEvidenceSnapshot} 供 SemanticGuard 语义裁决与 release 发布。 + * + *

被 {@code DiagnosisReleaseUseCase} 调用(validate / validateNoConclusionReferences), + * 是「唯一发布点」的第一道门。 + */ public final class EvidenceGuard { private final CanonicalInvocationStore store; @@ -35,6 +56,12 @@ public final class EvidenceGuard { private final ObjectReader mysqlRequestReader; private final ObjectReader mysqlResultReader; + /** + * 构造:注入账本(canonical store)+ key 工厂 + 严格模式 reader。 + * + *

四种 reader 全部开启 FAIL_ON_UNKNOWN_PROPERTIES + FAIL_ON_TRAILING_TOKENS—— + * 多余字段、尾随内容一律解析失败(fail closed,不接受「看起来差不多」的投影)。 + */ public EvidenceGuard(CanonicalInvocationStore store, ToolCallKeyFactory keyFactory, ObjectMapper objectMapper) { @@ -55,6 +82,12 @@ public final class EvidenceGuard { .with(DeserializationFeature.FAIL_ON_TRAILING_TOKENS); } + /** + * 主入口:draft 结构校验 → 逐条 tool_call 引用验真 → 重读投影重建证据。 + * + *

任何违规都收集到 violations(不中断,尽量报全);全部通过才产出快照。 + * 每个 analysis 只保留「证据非空」的条目——引用为空/无效的分析不进快照。 + */ public EvidenceGuardResult validate(RunContext context, DiagnosisDraft draft) { Objects.requireNonNull(context, "context must not be null"); List violations = validateDraft(draft); @@ -79,6 +112,11 @@ public final class EvidenceGuard { : EvidenceGuardResult.invalid(violations); } + /** + * 无结论场景的引用校验(conclusion == null 时由 release 调用): + * 只验引用真实性,不产出快照(返回 empty)——没有结论就没有「是否被支持」可判。 + * 供 {@code DiagnosisReleaseUseCase.releaseNoConclusion} 发布前兜底验引用。 + */ public EvidenceGuardResult validateNoConclusionReferences( RunContext context, DiagnosisDraft draft) { Objects.requireNonNull(context, "context must not be null"); @@ -113,6 +151,11 @@ public final class EvidenceGuard { : EvidenceGuardResult.invalid(violations); } + /** + * 阶段 A:草稿结构完整性校验(纯规则,不碰 store)。 + * 先收集全部 analysis id 到集合,再校验报告级引用只能指向这些已登记 id + * (封死「结论引用不存在的分析」路径)。 + */ private List validateDraft(DiagnosisDraft draft) { List violations = new ArrayList<>(); if (draft == null) { @@ -149,6 +192,10 @@ public final class EvidenceGuard { return violations; } + /** + * 报告级引用校验:conclusion / action_plan / recommendations 的 + * based_on_analysis_ids 必须存在且指向已登记 analysis id;limitations 必填。 + */ private void validateReportReferences(DiagnosisDraft draft, Set ids, List violations) { if (draft.conclusion() != null) { @@ -178,6 +225,10 @@ public final class EvidenceGuard { } } + /** + * 单条报告文本 + 其 based_on_analysis_ids 的合法性: + * 文本非空、引用列表非空、每个引用都必须指向已登记 analysis id。 + */ private void validateTextAndReferences(String target, String text, List references, Set ids, List violations) { if (isBlank(text)) { @@ -200,6 +251,18 @@ public final class EvidenceGuard { } } + /** + * 阶段 B(核心):验真单条 tool_call 引用。链条逐环检查,任何一环不过即记违规并跳过: + * + *

+     * id 非空 → key 构造(runId 绑定,防跨 Run 引用)→ 账本可查 → id 一致
+     * → isReferencableBy(READY + 同 Run + agent_result 非空 + 合法证据语义)
+     * → kind 匹配证据语义(NORMAL↔FOUND / NEGATIVE_OBSERVATION↔NO_EVIDENCE)
+     * → 工具受支持(RAG / LOGS / MYSQL)
+     * 
+ * + * 该引用不被采信不代表整体失败:继续检查其余引用,违规全部汇总。 + */ private void verifyInvocation(RunContext context, DiagnosisDraft.AnalysisItem analysis, int analysisIndex, String toolCallId, List evidence, @@ -250,6 +313,11 @@ public final class EvidenceGuard { } } + /** + * 阶段 C(RAG):严格反序列化 RagToolResult 投影,校验内部一致性 + * (toolCallId / evidenceStatus / returnedCount==evidence.size() / NO_EVIDENCE 时证据必须为空) + * 后,把每条命中重建为 VerifiedEvidence。 + */ private void readRag(CanonicalToolInvocation invocation, List evidence, String target, List violations) { RagToolResult result; @@ -301,6 +369,11 @@ public final class EvidenceGuard { } } + /** + * 阶段 C(LOGS):校验 QueryLogsToolResult 结构(topic/query/时间窗/matchCount 齐全、 + * returnedCount==events.size()、NO_EVIDENCE 时 matchCount 必须为 0 且无 patterns/events), + * 把日志模式与事件重建为 VerifiedEvidence。 + */ private void readLogs(CanonicalToolInvocation invocation, List evidence, String target, List violations) { QueryLogsToolResult result; @@ -364,6 +437,7 @@ public final class EvidenceGuard { } } + /** 投影通用一致性校验:toolCallId 与 evidenceStatus 必须与账本记录一致(防投影与账本脱节)。 */ private boolean projectionMatches(CanonicalToolInvocation invocation, String toolCallId, com.superbiz.agent.harness.contract.EvidenceStatus evidenceStatus, String target, List violations) { @@ -378,6 +452,10 @@ public final class EvidenceGuard { return true; } + /** + * 阶段 C(MYSQL):校验 MysqlToolResult(列唯一非空、returnedCount==rows.size()、 + * 每行 keySet 必须恰好等于 columns),把每一行重建为 VerifiedEvidence(带 _row_number)。 + */ private void readMysql(CanonicalToolInvocation invocation, List evidence, String target, List violations) { MysqlToolRequest request; diff --git a/src/main/java/com/superbiz/agent/harness/guard/evidence/EvidenceGuardResult.java b/src/main/java/com/superbiz/agent/harness/guard/evidence/EvidenceGuardResult.java index 0c777e2..21047e3 100644 --- a/src/main/java/com/superbiz/agent/harness/guard/evidence/EvidenceGuardResult.java +++ b/src/main/java/com/superbiz/agent/harness/guard/evidence/EvidenceGuardResult.java @@ -7,6 +7,10 @@ public record EvidenceGuardResult( List violations, VerifiedEvidenceSnapshot snapshot) { + /** + * 结构不变量:valid 必须有 snapshot、invalid 必须有 violations,二选一无中间态 + * (有效结果不可能带违规,无效结果不可能带快照)。 + */ public EvidenceGuardResult { violations = violations == null ? List.of() : List.copyOf(violations); if (violations.isEmpty() == (snapshot == null)) { diff --git a/src/main/java/com/superbiz/agent/harness/guard/semantic/GuardModelCall.java b/src/main/java/com/superbiz/agent/harness/guard/semantic/GuardModelCall.java index a4e876b..0d28b70 100644 --- a/src/main/java/com/superbiz/agent/harness/guard/semantic/GuardModelCall.java +++ b/src/main/java/com/superbiz/agent/harness/guard/semantic/GuardModelCall.java @@ -24,6 +24,19 @@ import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +/** + * 守卫模型调用器:通用「隔离判定模型」的受控调用(SemanticGuard 等守卫用)。 + * + *

与主 Agent 调用不同:守卫模型单轮、无工具、强约束输出,但同样受 Run 生命周期管: + *

    + *
  • core.beforeModelCall / checkActive:与 core 门禁对齐;
  • + *
  • auditor.begin / recordUsage:Token 记账(ModelCallLedger);
  • + *
  • executor.submit + future.get(timeout):独立线程 + 超时截断;
  • + *
  • context.cancellation().onCancel → future.cancel(true):Run 取消强杀在途调用;
  • + *
  • 输出限制:非文本 / 带 tool_calls / 超 maxOutputBytes → SCHEMA_INVALID 重试;
  • + *
  • 终态异常(RunAborted / BudgetExceeded)原样穿出,不吞。
  • + *
+ */ public final class GuardModelCall { private final DiagnosisHarnessCore core; @@ -44,6 +57,11 @@ public final class GuardModelCall { this.auditor = Objects.requireNonNull(auditor, "auditor must not be null"); } + /** + * 受控调用:beforeModelCall 门禁 → 记账开始 → 提交执行 → 注册取消回调 + * → future.get(timeout) 等待。超时/取消/中断/执行异常分别归类映射 RetryFailure; + * RunAbortedException / BudgetExceededException 原样穿出(Run 终态事实,不可重试)。 + */ public String call(RunContext context, ModelCallComponent component, Prompt prompt, Duration timeout, long maxOutputBytes) { Objects.requireNonNull(context, "context must not be null"); @@ -91,6 +109,11 @@ public final class GuardModelCall { } } + /** + * 执行侧:真实 chatModel.call,成功则记账 usage 并 checkActive; + * 输出形状违规(null/空白/带 tool_calls)或超 maxOutputBytes 归类 SCHEMA_INVALID; + * 输出字节也 reserveRunBytes 计入预算(守卫模型的花费不是无底洞)。 + */ private String invoke(RunContext context, ModelCallLedger.Call call, Prompt prompt, long maxOutputBytes) { ChatResponse response; @@ -119,6 +142,7 @@ public final class GuardModelCall { return output.getText(); } + /** 记账 usage;缺 metadata/usage 时按 0 记账(保持账本完整性,可对账)。 */ private void recordUsage(RunContext context, ModelCallLedger.Call call, ChatResponse response) { if (response == null || response.getMetadata() == null) { auditor.recordUsage(context, call, 0, 0, false); diff --git a/src/main/java/com/superbiz/agent/harness/guard/semantic/SemanticGuard.java b/src/main/java/com/superbiz/agent/harness/guard/semantic/SemanticGuard.java index 4dec111..649bf5f 100644 --- a/src/main/java/com/superbiz/agent/harness/guard/semantic/SemanticGuard.java +++ b/src/main/java/com/superbiz/agent/harness/guard/semantic/SemanticGuard.java @@ -24,6 +24,24 @@ import java.util.Objects; import java.util.Set; import java.util.function.Consumer; +/** + * 语义裁决(证据安全链第 2 道闸):判「结论是否被已验证证据支持」。 + * + *

在 EvidenceGuard 机械验真通过后调用——结构错、引用假根本到不了这里。 + * 职责:把用户可见视图(SemanticDraftView,不含内部 id)+ 已验证证据交给 + * 隔离的守卫模型,硬校验输出(恰好 {verdict, reason} 两个字段), + * 裁决 SUPPORTED / UNSUPPORTED。 + * + *

与 Harness 全栈衔接: + *

    + *
  • 预算:输入输出字节都 {@code core.reserveRunBytes} 计入 Run 预算;
  • + *
  • 重试:走 {@code context.retryPolicies().semanticGuard()},每次 attempt 递减剩余超时;
  • + *
  • 取消:GuardModelCall 内 onCancel → future.cancel(true);
  • + *
  • 审计:ModelCallLedger 记账 + TraceAuditEvents.semanticAttempt 落 trace。
  • + *
+ * + *

被 {@code DiagnosisReleaseUseCase.releaseConclusion} 调用(唯一入口)。 + */ public final class SemanticGuard { private static final Set OUTPUT_FIELDS = Set.of("verdict", "reason"); @@ -65,6 +83,12 @@ public final class SemanticGuard { this.prompt = SemanticGuardPrompt.load(); } + /** + * 主入口:序列化输入(超限即拒绝 SCHEMA_INVALID)→ 计入预算 + * → 构造 System(prompt)+User(输入 JSON) 双消息 + * → 经 HarnessRetryExecutor 按 semanticGuard 策略执行模型调用 + * → 硬校验输出 schema → 返回裁决。每次 attempt 都记审计 trace。 + */ public SemanticGuardDecision review(RunContext context, SemanticGuardInput input) { Objects.requireNonNull(context, "context must not be null"); Objects.requireNonNull(input, "input must not be null"); @@ -91,6 +115,11 @@ public final class SemanticGuard { }); } + /** + * 硬校验模型输出:必须是 JSON、字段恰好 {verdict, reason}、verdict 合法枚举、 + * reason 非空;否则按失败类型抛 GuardModelCallException(可重试: + * PARSE_ERROR / SCHEMA_INVALID)。防止模型夹带多余字段或输出不完整。 + */ private SemanticGuardDecision parse(String output) { JsonNode root; try { @@ -115,6 +144,10 @@ public final class SemanticGuard { return new SemanticGuardDecision(verdict, root.path("reason").asText()); } + /** + * 每次 attempt 的剩余超时 = min(总超时 - 已用, 单次上限); + * 总超时耗尽即抛 TIMEOUT(不再重试)——守卫判定有硬截止线。 + */ private Duration remainingTimeout(long startedNanos) { long elapsed = Math.max(0L, System.nanoTime() - startedNanos); long remaining = limits.totalTimeout().toNanos() - elapsed; @@ -125,6 +158,10 @@ public final class SemanticGuard { return Duration.ofNanos(Math.min(remaining, limits.perAttemptTimeout().toNanos())); } + /** + * 失败分类:GuardModelCallException 自带 RetryFailure; + * 其余未知异常归 UNKNOWN(不重试,直接失败)。 + */ private RetryFailure classify(Exception exception) { if (exception instanceof GuardModelCallException guardFailure) { return guardFailure.failure(); diff --git a/src/main/java/com/superbiz/agent/harness/progress/DiagnosisProgressSnapshot.java b/src/main/java/com/superbiz/agent/harness/progress/DiagnosisProgressSnapshot.java index 3e078db..6e2da0b 100644 --- a/src/main/java/com/superbiz/agent/harness/progress/DiagnosisProgressSnapshot.java +++ b/src/main/java/com/superbiz/agent/harness/progress/DiagnosisProgressSnapshot.java @@ -4,12 +4,29 @@ import com.superbiz.agent.harness.contract.SafeFallback; import java.util.List; +/** + * 停止后的「安全进展快照」:Run 已完成的验证事实 + 限制声明 + 停止原因。 + * + *

由 {@link DiagnosisProgressProjector} 在 Agent 停止后投影生成: + * 把 Tracker 里的调用 identity 回读 Canonical Store 的 READY 记录, + * 重读投影成有界、去重的事实——不是模型自述,是账本背书。 + * + *

被 {@code DiagnosisReleaseUseCase} 消费: + * 受控停止 / 无结论 / 非法 Draft 三条降级路径都靠它决定发布形态 + * (有 observed_facts 才允许发布 INSUFFICIENT_EVIDENCE,否则 fail closed)。 + */ public record DiagnosisProgressSnapshot( + /** 已验证来源(去重后):只到「查过哪些来源」粒度,供 FALLBACK 展示。 */ List verifiedSources, + /** 已观察事实(去重后):每条 = 来源类型 + 来源 + 范围 + 有界摘要, + * 是「Run 真的查过什么、结果如何」的证据性记录(空查询也算事实)。 */ List observedFacts, + /** 限制声明:无法验真/不可读/截断等原因的诚实说明。 */ List limitations, + /** 停止原因(受控停止时):信息饱和 / 预算耗尽 / 协议违规。 */ DiagnosisStopReason stopReason) { + /** 防御:三列表全部转不可变,null 视为空列表。 */ public DiagnosisProgressSnapshot { verifiedSources = verifiedSources == null ? List.of() : List.copyOf(verifiedSources); observedFacts = observedFacts == null ? List.of() : List.copyOf(observedFacts); @@ -20,6 +37,11 @@ public record DiagnosisProgressSnapshot( return new DiagnosisProgressSnapshot(List.of(), List.of(), List.of(), null); } + /** + * 「是否有安全进展」的判断依据:observedFacts 非空即视为有已验真事实。 + * release 域的 fail-closed 分支全靠它——没有事实就不能把 + * 「没查到」伪装成业务结果发布。 + */ public boolean hasObservedFacts() { return !observedFacts.isEmpty(); } diff --git a/src/main/java/com/superbiz/agent/harness/release/DiagnosisReleaseResult.java b/src/main/java/com/superbiz/agent/harness/release/DiagnosisReleaseResult.java index 16a85ce..ef49f7d 100644 --- a/src/main/java/com/superbiz/agent/harness/release/DiagnosisReleaseResult.java +++ b/src/main/java/com/superbiz/agent/harness/release/DiagnosisReleaseResult.java @@ -7,12 +7,23 @@ import com.superbiz.agent.harness.guard.evidence.VerifiedEvidenceSnapshot; import java.util.Objects; +/** + * 发布裁决结果(Release 域唯一出口的结果类型)。 + * + *

结构不变量:SUCCESS 必须有 draft 且不允许带 fallback; + * FALLBACK 必须有 fallback 且不允许带 draft—— + * 成功只能带验证过的草稿、降级只能带安全回退,绝无「半真半假」的中间产物。 + */ public record DiagnosisReleaseResult( ReleaseOutcome outcome, DiagnosisDraft draft, SafeFallback fallback, VerifiedEvidenceSnapshot verifiedEvidence) { + /** + * 结构不变量:outcome 必填;SUCCESS ↔ draft、FALLBACK ↔ fallback 严格互斥; + * 本域只支持 SUCCESS / FALLBACK 两个出口(FAILED/CANCELLED 由 Application 层写)。 + */ public DiagnosisReleaseResult { Objects.requireNonNull(outcome, "outcome must not be null"); Objects.requireNonNull(verifiedEvidence, "verifiedEvidence must not be null"); diff --git a/src/main/java/com/superbiz/agent/harness/release/EvidenceRepair.java b/src/main/java/com/superbiz/agent/harness/release/EvidenceRepair.java index d014510..bce4d93 100644 --- a/src/main/java/com/superbiz/agent/harness/release/EvidenceRepair.java +++ b/src/main/java/com/superbiz/agent/harness/release/EvidenceRepair.java @@ -26,6 +26,25 @@ import java.util.List; import java.util.Objects; import java.util.function.Consumer; +/** + * 引用修复器(证据安全链的一环):EvidenceGuard 验真失败后,「只修引用、不修结论」。 + * + *

核心约束: + *

    + *
  • prompt 锁死:只能改 analysis_id / tool_call_ids / based_on_analysis_ids + * 三个引用字段,结论、分析正文、kind、limitations 一律禁止动;
  • + *
  • 语义不变性检查:修复前后 {@link SemanticDraftView#hasSameUserVisibleSemantics} + * 逐字段比对——用户可见内容一个字节不许变,变了判 SCHEMA_INVALID 重试;
  • + *
  • 受控调用:与 SemanticGuard 同款全栈衔接(输入计预算、独立重试策略 + * evidenceRepair、超时限制、GuardModelCall 受控调用、审计 trace)。
  • + *
+ * + *

为什么调大模型而不是 Harness 机械替换:修引用需要理解语义 + * (哪条 analysis 该锚哪个调用),机械替换做不到;但修复器的自由度 + * 被 prompt + 语义不变性双重锁死。 + * + *

被 {@code DiagnosisReleaseUseCase.releaseConclusion} 调用。 + */ public final class EvidenceRepair { private final DiagnosisHarnessCore core; @@ -69,6 +88,14 @@ public final class EvidenceRepair { this.prompt = EvidenceRepairPrompt.load(); } + /** + * 主入口:输入(query + 原 draft + 违规清单)序列化并计预算 + * → 构造 System(prompt)+User(输入) 双消息 + * → 按 evidenceRepair 重试策略执行模型修复 + * → 解析修复结果并做语义不变性检查(变了即失败)。 + * + * @return 修复后的 DiagnosisDraft(仅引用字段可能变化) + */ public DiagnosisDraft repair(RunContext context, String query, DiagnosisDraft original, List violations) { Objects.requireNonNull(context, "context must not be null"); @@ -110,6 +137,7 @@ public final class EvidenceRepair { }); } + /** 严格反序列化修复输出(FAIL_ON_UNKNOWN_PROPERTIES + FAIL_ON_TRAILING_TOKENS),失败归类 PARSE_ERROR。 */ private DiagnosisDraft parse(String output) { try { return draftReader.readValue(output); @@ -119,6 +147,7 @@ public final class EvidenceRepair { } } + /** 失败分类:GuardModelCallException 自带 RetryFailure;其余归 UNKNOWN。 */ private RetryFailure classify(Exception exception) { return exception instanceof GuardModelCallException failure ? failure.failure() : RetryFailure.UNKNOWN; diff --git a/src/main/java/com/superbiz/agent/harness/release/EvidenceRepairLimits.java b/src/main/java/com/superbiz/agent/harness/release/EvidenceRepairLimits.java index 333a1bb..15b735a 100644 --- a/src/main/java/com/superbiz/agent/harness/release/EvidenceRepairLimits.java +++ b/src/main/java/com/superbiz/agent/harness/release/EvidenceRepairLimits.java @@ -3,6 +3,10 @@ package com.superbiz.agent.harness.release; import java.time.Duration; import java.util.Objects; +/** + * EvidenceRepair 的限额:输入/输出字节 + 单次修复超时。 + * 防修复器本身成为无底洞(超大 draft 或无限重试)。 + */ public record EvidenceRepairLimits( long maxInputBytes, long maxOutputBytes, diff --git a/src/main/java/com/superbiz/agent/harness/release/SafeFallbackFactory.java b/src/main/java/com/superbiz/agent/harness/release/SafeFallbackFactory.java index 9416491..93e9100 100644 --- a/src/main/java/com/superbiz/agent/harness/release/SafeFallbackFactory.java +++ b/src/main/java/com/superbiz/agent/harness/release/SafeFallbackFactory.java @@ -13,11 +13,28 @@ import java.util.List; import java.util.Map; import java.util.Objects; +/** + * 安全回退工厂:构造所有 FALLBACK 形态的有界、去重、诚实降级。 + * + *

设计要点: + *

    + *
  • 诚实降级:降级不抹掉进展——SEMANTIC_UNSUPPORTED / INSUFFICIENT_EVIDENCE + * 保留已验证事实(observed_facts / verified_sources)供用户继续排查;
  • + *
  • 有界:observed_facts 最多 12 条、摘要 320 字符,missing_info 最多 8 条 + * (绝不泄露 raw / 敏感正文,空摘要降级为受限审计说明文案);
  • + *
  • conclusion 恒为 null:降级不发布根因结论;
  • + *
  • fail closed:insufficientEvidence 要求 progress 必须有已验真事实, + * missingRequiredContext 要求 missing_info 非空,否则拒绝构造。
  • + *
+ * + *

被 {@code DiagnosisReleaseUseCase} 的各个降级出口调用。 + */ public final class SafeFallbackFactory { private static final int MAX_OBSERVED_FACTS = 12; private static final int MAX_SUMMARY_CHARS = 320; + /** 引用验真失败(含 repair 后仍失败):只给违规明细,零事实。 */ public SafeFallback evidenceValidationFailed(List violations) { return fallback( FallbackType.EVIDENCE_VALIDATION_FAILED, @@ -30,6 +47,7 @@ public final class SafeFallbackFactory { issues(violations)); } + /** 结论不被证据支撑(verdict=UNSUPPORTED):保留已验证事实与来源。 */ public SafeFallback semanticUnsupported(VerifiedEvidenceSnapshot snapshot) { return fallback( FallbackType.SEMANTIC_UNSUPPORTED, @@ -42,6 +60,7 @@ public final class SafeFallbackFactory { List.of()); } + /** 语义评审技术不可用(超时等):不发布根因,保留已验证事实。 */ public SafeFallback semanticUnavailable(VerifiedEvidenceSnapshot snapshot) { return fallback( FallbackType.SEMANTIC_UNAVAILABLE, @@ -54,6 +73,10 @@ public final class SafeFallbackFactory { List.of()); } + /** + * 有限检查但证据不足(受控停止/无结论/非法 draft 降级的共同出口): + * 必须已有已验真事实(否则 fail closed),展示检查过的范围 + 缺失项。 + */ public SafeFallback insufficientEvidence( DiagnosisProgressSnapshot progress, List missingInfo) { Objects.requireNonNull(progress, "progress must not be null"); @@ -78,6 +101,7 @@ public final class SafeFallbackFactory { List.of()); } + /** 缺上下文未开始有效查询:只列缺失项,无事实。 */ public SafeFallback missingRequiredContext(List missingInfo) { List safeMissingInfo = boundedMissingInfo(missingInfo); if (safeMissingInfo.isEmpty()) { @@ -112,6 +136,10 @@ public final class SafeFallbackFactory { return Objects.requireNonNull(snapshot, "snapshot must not be null").verifiedSources(); } + /** + * 从验证快照提取去重后的可观察事实:key = 来源类型+来源+范围+摘要, + * 最多 12 条、摘要 320 字符;空摘要降级为受限审计说明(不泄露 raw)。 + */ private List facts(VerifiedEvidenceSnapshot snapshot) { Objects.requireNonNull(snapshot, "snapshot must not be null"); Map unique = new LinkedHashMap<>(); @@ -133,6 +161,7 @@ public final class SafeFallbackFactory { return List.copyOf(unique.values()); } + /** 把违规明细映射为对外可用的 ValidationIssue 列表(code + target)。 */ private List issues(List violations) { List result = new ArrayList<>(); for (EvidenceViolation violation : violations == null ? List.of() : violations) {