docs(harness): add evidence-chain, application-audit, contract-state and interview review notes; annotate guard/release core classes

This commit is contained in:
aruo
2026-08-08 00:12:25 +08:00
parent 074d1aa5a9
commit e1b8d1fb2c
15 changed files with 1032 additions and 17 deletions
@@ -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["① 读会话上下文<br/>RoutingHistory + PreviousTurn"]
B --> C["② core.startRun<br/>创建 Run 边界"]
C --> D["③ persistStart + observer.onStarted<br/>(SSE metadata + 取消句柄 CoreRunControl)"]
D --> E["④ router.route<br/>意图路由(单次模型调用)"]
E --> F["⑤ executePath 按 intent 分叉<br/>SYSTEM_CHAT / KNOWLEDGE_QUERY / DIAGNOSIS"]
F --> G["⑥ completePath + persistFinish + 返回"]
G -.异常.-> H["统一失败出口<br/>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<br/>每模型步 → agent_step + agent_reasoning_audit"]
H2["ToolInvocationAuditSink<br/>工具 → tool_invocation"]
H3["ModelCallAuditor<br/>Token → ledger + core + agent_step 回写"]
H4["JpaDiagnosisTraceRecorder<br/>事件 → diagnosis_trace_event"]
H5["JpaChatRunStore<br/>Run → diagnosis_run"]
end
subgraph 读取侧["读取侧(回放)"]
S["DiagnosisTraceService"]
S --> R["DiagnosisTraceResponse<br/>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` |
@@ -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` |
@@ -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<br/>工具调用完成"]
TC -->|"完整记录"| CS["Canonical Store<br/>(唯一真相源, TTL 2h)"]
TC -->|"identity"| TR["Progress Tracker<br/>(toolCallId+toolName+scope)"]
end
subgraph 停止["Agent 结束(所有出口触发投影)"]
TR -->|"回读验真"| PP["DiagnosisProgressProjector"]
PP --> PS["ProgressSnapshot<br/>(observedFacts+limitations+stopReason)"]
end
subgraph 裁决["Release 裁决(唯一发布点)"]
EX["DiagnosisAgentExecution<br/>(draft / stopped)"]
EX --> RC["releaseConclusion<br/>(有结论)"]
RC -->|"tool_call_ids"| EG["EvidenceGuard<br/>机械验引用"]
EG -->|"失败"| ER["EvidenceRepair<br/>只修引用→复查"]
EG -->|"通过"| VES["VerifiedEvidenceSnapshot"]
VES --> SG["SemanticGuard<br/>判支持度"]
SG -->|"SUPPORTED"| SUCCESS["SUCCESS<br/>(唯一出口)"]
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/` |
@@ -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` |
@@ -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<br/>执行控制 ✅"] --> B["retry<br/>重试 ✅"]
B --> C["progress<br/>信息增益 ✅"]
C --> D["tool<br/>事实边界 ✅"]
D --> E["guard<br/>验证 ⬜"]
E --> F["release<br/>发布 ⬜"]
F --> G["application + audit<br/>收尾 ⬜"]
G --> H["contract<br/>类型化语言 ⬜"]
D --> E["guard<br/>验证 ✅"]
E --> F["release<br/>发布 ✅"]
F --> G["application + audit<br/>收尾 ✅"]
G --> H["contract<br/>类型化语言 ✅"]
```
## 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. 建议每次学完一个域后更新
@@ -4,15 +4,41 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
/**
* 安全回退契约(Release 层 FALLBACK 的对外载荷):不发布根因报告时,
* 把「为什么降级 + 已验证事实 + 下一步建议」结构化地交给用户。
*
* <p>设计要点:
* <ul>
* <li>{@code conclusion} 恒为 null:降级绝不发布未证明的根因结论;</li>
* <li>诚实降级:verified_sources / observed_facts 保留已验证事实
* (供用户继续排查),validation_issues 给出失败原因;</li>
* <li>有界冻结契约:全部列表不可变,内容经 SafeFallbackFactory 截断去重
* (来源 12 条以内、摘要 320 字),绝不泄露 raw / 敏感正文。</li>
* </ul>
*
* <p>构造:仅 {@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<VerifiedSource> verifiedSources,
/** 限制声明:检查范围 / 缺失项 / 降级原因。 */
@JsonProperty("limitations") List<String> limitations,
/** 下一步建议(用户可执行)。 */
@JsonProperty("next_steps") List<String> nextSteps,
/** 失败阶段标识:DIAGNOSIS_INPUT / DIAGNOSIS_COLLECTION / EVIDENCE_VALIDATION / SEMANTIC_VALIDATION。 */
@JsonProperty("failure_stage") String failureStage,
/** 已观察事实(去重、有界):每条 = 来源 + 范围 + 摘要,供继续排查。 */
@JsonProperty("observed_facts") List<ObservedFact> observedFacts,
/** 验证违规明细(EVIDENCE_VALIDATION_FAILED 时携带 code + target)。 */
@JsonProperty("validation_issues") List<ValidationIssue> 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) {
@@ -26,6 +26,27 @@ import java.util.Map;
import java.util.Objects;
import java.util.Set;
/**
* 证据机械验真(证据安全链第 1 道闸):不信任模型自述,以 Canonical Store 账本为准。
*
* <p>职责:校验 DiagnosisDraft 结构合法 + 每个 tool_call_id 引用真实可查
* + 投影内部自洽,并把账本投影「重读重建」为 VerifiedEvidence 快照。
*
* <p>三个阶段:
* <ol>
* <li>{@link #validateDraft}:草稿结构完整性(analysis 非空、id 唯一、kind/text/引用齐全、limitations 必填);</li>
* <li>{@link #verifyInvocation}:引用验真(key=runId+toolCallId 查账本、记录必须 READY、
* 同 Run、agent_result 非空、kind 与证据语义匹配、工具受支持);</li>
* <li>readRag/readLogs/readMysql:严格反序列化投影并校验内部一致性,
* 重读重建 VerifiedEvidence(证据内容来自账本,不是模型复述)。</li>
* </ol>
*
* <p>纯规则门控、不调大模型:20 个违规码全部可枚举可审计;
* 产出 {@link VerifiedEvidenceSnapshot} 供 SemanticGuard 语义裁决与 release 发布。
*
* <p>被 {@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。
*
* <p>四种 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 引用验真 → 重读投影重建证据。
*
* <p>任何违规都收集到 violations(不中断,尽量报全);全部通过才产出快照。
* 每个 analysis 只保留「证据非空」的条目——引用为空/无效的分析不进快照。
*/
public EvidenceGuardResult validate(RunContext context, DiagnosisDraft draft) {
Objects.requireNonNull(context, "context must not be null");
List<EvidenceViolation> 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<EvidenceViolation> validateDraft(DiagnosisDraft draft) {
List<EvidenceViolation> 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<String> ids,
List<EvidenceViolation> 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<String> references,
Set<String> ids, List<EvidenceViolation> violations) {
if (isBlank(text)) {
@@ -200,6 +251,18 @@ public final class EvidenceGuard {
}
}
/**
* 阶段 B(核心):验真单条 tool_call 引用。链条逐环检查,任何一环不过即记违规并跳过:
*
* <pre>
* id 非空 → key 构造(runId 绑定,防跨 Run 引用)→ 账本可查 → id 一致
* → isReferencableBy(READY + 同 Run + agent_result 非空 + 合法证据语义)
* → kind 匹配证据语义(NORMAL↔FOUND / NEGATIVE_OBSERVATION↔NO_EVIDENCE)
* → 工具受支持(RAG / LOGS / MYSQL)
* </pre>
*
* 该引用不被采信不代表整体失败:继续检查其余引用,违规全部汇总。
*/
private void verifyInvocation(RunContext context, DiagnosisDraft.AnalysisItem analysis,
int analysisIndex, String toolCallId,
List<VerifiedEvidence> 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<VerifiedEvidence> evidence,
String target, List<EvidenceViolation> 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<VerifiedEvidence> evidence,
String target, List<EvidenceViolation> 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<EvidenceViolation> 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<VerifiedEvidence> evidence,
String target, List<EvidenceViolation> violations) {
MysqlToolRequest request;
@@ -7,6 +7,10 @@ public record EvidenceGuardResult(
List<EvidenceViolation> violations,
VerifiedEvidenceSnapshot snapshot) {
/**
* 结构不变量:valid 必须有 snapshot、invalid 必须有 violations,二选一无中间态
* (有效结果不可能带违规,无效结果不可能带快照)。
*/
public EvidenceGuardResult {
violations = violations == null ? List.of() : List.copyOf(violations);
if (violations.isEmpty() == (snapshot == null)) {
@@ -24,6 +24,19 @@ import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/**
* 守卫模型调用器:通用「隔离判定模型」的受控调用(SemanticGuard 等守卫用)。
*
* <p>与主 Agent 调用不同:守卫模型单轮、无工具、强约束输出,但同样受 Run 生命周期管:
* <ul>
* <li>core.beforeModelCall / checkActive:与 core 门禁对齐;</li>
* <li>auditor.begin / recordUsage:Token 记账(ModelCallLedger);</li>
* <li>executor.submit + future.get(timeout):独立线程 + 超时截断;</li>
* <li>context.cancellation().onCancel → future.cancel(true):Run 取消强杀在途调用;</li>
* <li>输出限制:非文本 / 带 tool_calls / 超 maxOutputBytes → SCHEMA_INVALID 重试;</li>
* <li>终态异常(RunAborted / BudgetExceeded)原样穿出,不吞。</li>
* </ul>
*/
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);
@@ -24,6 +24,24 @@ import java.util.Objects;
import java.util.Set;
import java.util.function.Consumer;
/**
* 语义裁决(证据安全链第 2 道闸):判「结论是否被已验证证据支持」。
*
* <p>在 EvidenceGuard 机械验真通过后调用——结构错、引用假根本到不了这里。
* 职责:把用户可见视图(SemanticDraftView,不含内部 id)+ 已验证证据交给
* 隔离的守卫模型,硬校验输出(恰好 {verdict, reason} 两个字段),
* 裁决 SUPPORTED / UNSUPPORTED。
*
* <p>与 Harness 全栈衔接:
* <ul>
* <li>预算:输入输出字节都 {@code core.reserveRunBytes} 计入 Run 预算;</li>
* <li>重试:走 {@code context.retryPolicies().semanticGuard()},每次 attempt 递减剩余超时;</li>
* <li>取消:GuardModelCall 内 onCancel → future.cancel(true);</li>
* <li>审计:ModelCallLedger 记账 + TraceAuditEvents.semanticAttempt 落 trace。</li>
* </ul>
*
* <p>被 {@code DiagnosisReleaseUseCase.releaseConclusion} 调用(唯一入口)。
*/
public final class SemanticGuard {
private static final Set<String> 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();
@@ -4,12 +4,29 @@ import com.superbiz.agent.harness.contract.SafeFallback;
import java.util.List;
/**
* 停止后的「安全进展快照」:Run 已完成的验证事实 + 限制声明 + 停止原因。
*
* <p>由 {@link DiagnosisProgressProjector} 在 Agent 停止后投影生成:
* 把 Tracker 里的调用 identity 回读 Canonical Store 的 READY 记录,
* 重读投影成有界、去重的事实——不是模型自述,是账本背书。
*
* <p>被 {@code DiagnosisReleaseUseCase} 消费:
* 受控停止 / 无结论 / 非法 Draft 三条降级路径都靠它决定发布形态
* (有 observed_facts 才允许发布 INSUFFICIENT_EVIDENCE,否则 fail closed)。
*/
public record DiagnosisProgressSnapshot(
/** 已验证来源(去重后):只到「查过哪些来源」粒度,供 FALLBACK 展示。 */
List<SafeFallback.VerifiedSource> verifiedSources,
/** 已观察事实(去重后):每条 = 来源类型 + 来源 + 范围 + 有界摘要,
* 是「Run 真的查过什么、结果如何」的证据性记录(空查询也算事实)。 */
List<SafeFallback.ObservedFact> observedFacts,
/** 限制声明:无法验真/不可读/截断等原因的诚实说明。 */
List<String> 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();
}
@@ -7,12 +7,23 @@ import com.superbiz.agent.harness.guard.evidence.VerifiedEvidenceSnapshot;
import java.util.Objects;
/**
* 发布裁决结果(Release 域唯一出口的结果类型)。
*
* <p>结构不变量: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");
@@ -26,6 +26,25 @@ import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
/**
* 引用修复器(证据安全链的一环):EvidenceGuard 验真失败后,「只修引用、不修结论」。
*
* <p>核心约束:
* <ul>
* <li>prompt 锁死:只能改 analysis_id / tool_call_ids / based_on_analysis_ids
* 三个引用字段,结论、分析正文、kind、limitations 一律禁止动;</li>
* <li>语义不变性检查:修复前后 {@link SemanticDraftView#hasSameUserVisibleSemantics}
* 逐字段比对——用户可见内容一个字节不许变,变了判 SCHEMA_INVALID 重试;</li>
* <li>受控调用:与 SemanticGuard 同款全栈衔接(输入计预算、独立重试策略
* evidenceRepair、超时限制、GuardModelCall 受控调用、审计 trace)。</li>
* </ul>
*
* <p>为什么调大模型而不是 Harness 机械替换:修引用需要理解语义
* (哪条 analysis 该锚哪个调用),机械替换做不到;但修复器的自由度
* 被 prompt + 语义不变性双重锁死。
*
* <p>被 {@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<EvidenceViolation> 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;
@@ -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,
@@ -13,11 +13,28 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* 安全回退工厂:构造所有 FALLBACK 形态的有界、去重、诚实降级。
*
* <p>设计要点:
* <ul>
* <li>诚实降级:降级不抹掉进展——SEMANTIC_UNSUPPORTED / INSUFFICIENT_EVIDENCE
* 保留已验证事实(observed_facts / verified_sources)供用户继续排查;</li>
* <li>有界:observed_facts 最多 12 条、摘要 320 字符,missing_info 最多 8 条
* (绝不泄露 raw / 敏感正文,空摘要降级为受限审计说明文案);</li>
* <li>conclusion 恒为 null:降级不发布根因结论;</li>
* <li>fail closed:insufficientEvidence 要求 progress 必须有已验真事实,
* missingRequiredContext 要求 missing_info 非空,否则拒绝构造。</li>
* </ul>
*
* <p>被 {@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<EvidenceViolation> 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<String> missingInfo) {
Objects.requireNonNull(progress, "progress must not be null");
@@ -78,6 +101,7 @@ public final class SafeFallbackFactory {
List.of());
}
/** 缺上下文未开始有效查询:只列缺失项,无事实。 */
public SafeFallback missingRequiredContext(List<String> missingInfo) {
List<String> 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<SafeFallback.ObservedFact> facts(VerifiedEvidenceSnapshot snapshot) {
Objects.requireNonNull(snapshot, "snapshot must not be null");
Map<String, SafeFallback.ObservedFact> unique = new LinkedHashMap<>();
@@ -133,6 +161,7 @@ public final class SafeFallbackFactory {
return List.copyOf(unique.values());
}
/** 把违规明细映射为对外可用的 ValidationIssue 列表(code + target)。 */
private List<SafeFallback.ValidationIssue> issues(List<EvidenceViolation> violations) {
List<SafeFallback.ValidationIssue> result = new ArrayList<>();
for (EvidenceViolation violation : violations == null ? List.<EvidenceViolation>of() : violations) {