docs(mvp): add harness design and progressive guides
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
# 01 运行控制:让一次请求有边界
|
||||
|
||||
这一篇只回答一个问题:**一次诊断请求由谁负责到底?**
|
||||
|
||||
## 1. 先看一个具体问题
|
||||
|
||||
用户发起支付超时诊断。请求开始后,Router 调了一次模型,Diagnosis Agent 又调用多轮模型和 Tool,SemanticGuard 最后还要调用一次模型。与此同时,客户端可能断开,某次调用可能超时,两个线程也可能同时报告成功和失败。
|
||||
|
||||
如果没有统一的运行控制,每个组件都会有自己的理解:
|
||||
|
||||
- Controller 认为连接断开了,后台 Agent 却还在继续查询;
|
||||
- Agent 认为还可以调用 Tool,Run 的总预算其实已经耗尽;
|
||||
- 超时线程先写入失败,迟到的模型结果又把它覆盖为成功;
|
||||
- SDK 在内部偷偷重试,系统无法解释多出来的延迟和 Token;
|
||||
- 线上只看到最终失败,却不知道请求在哪一步、因为什么停止。
|
||||
|
||||
所以运行控制不是一个计时器,而是由几个职责不同的组件共同完成。
|
||||
|
||||
## 2. 四组组件怎样协作
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant APP as Application
|
||||
participant CORE as Core / RunContext
|
||||
participant WORK as Agent、Tool、Guard
|
||||
participant RETRY as Retry
|
||||
participant AUDIT as Audit
|
||||
|
||||
APP->>CORE: 创建 RunContext
|
||||
CORE-->>APP: runId、deadline、budget、cancel、lifecycle
|
||||
APP->>WORK: 显式传入 RunContext
|
||||
WORK->>CORE: 每次消耗前检查 active 和预算
|
||||
WORK->>RETRY: 仅允许策略声明的技术重试
|
||||
RETRY->>AUDIT: 记录每个 attempt
|
||||
WORK->>AUDIT: 记录模型、Tool 和阶段元数据
|
||||
APP->>CORE: 尝试写入最终终态
|
||||
CORE-->>APP: first-terminal-wins
|
||||
APP->>AUDIT: 记录公开结果和预算对账
|
||||
```
|
||||
|
||||
第一次阅读可以把它们理解为:
|
||||
|
||||
- **Application 是负责人**:组织一次请求从创建到公开结果。
|
||||
- **Core 是执行规则**:统一管理身份、deadline、预算、取消和唯一终态。
|
||||
- **Retry 是重做规则**:明确什么失败允许再试一次。
|
||||
- **Audit 是运行账本**:记录发生过什么,但不保存敏感正文。
|
||||
|
||||
## 3. Application:负责人,而不是推理者
|
||||
|
||||
Application 负责建立一次请求的应用边界:读取安全历史、创建 Run、判断 intent、调用对应执行分支、持久化结果,并把安全内容交给 SSE。
|
||||
|
||||
设计它的原因,是这些步骤必须由一个地方协调。如果散落在 Controller、Agent 和 Guard 中,会出现多个结果出口和不同的失败语义。
|
||||
|
||||
它不负责判断支付超时的根因,也不负责自己拼一份诊断 Fallback。它只负责把正确的执行组件按顺序接起来,并确保结果经过统一出口。
|
||||
|
||||
主要代码入口:`ChatApplicationUseCase`、`DiagnosisChatExecutor`、`KnowledgeQueryExecutor`、`SystemChatExecutor`、`ChatRunStore`。
|
||||
|
||||
## 4. Core:一次 Run 的共同规则
|
||||
|
||||
Core 创建 `RunContext`。这个上下文显式携带:
|
||||
|
||||
```text
|
||||
这是谁的请求:sessionId + runId
|
||||
最晚执行到何时:deadline
|
||||
还能消耗多少:RunBudget
|
||||
是否要求停止:RunCancellation
|
||||
最终如何结束:RunLifecycle
|
||||
模型消耗如何对账:ModelCallLedger
|
||||
信息收集是否仍有价值:DiagnosisProgressTracker
|
||||
```
|
||||
|
||||
这里最重要的决策是**显式传递 RunContext**,而不是使用 ThreadLocal 或让每个组件自己查询全局状态。原因是模型和 Tool 可能跨线程运行;隐式上下文很容易丢失、串线,也很难在测试中证明归属关系。
|
||||
|
||||
`RunLifecycle` 使用 first-terminal-wins:第一个成功写入的终态不可被迟到结果覆盖。它解决的是并发一致性,不是公开结果的业务含义。
|
||||
|
||||
主要代码入口:`DiagnosisHarnessCore`、`RunContext`、`RunBudget`、`RunCancellation`、`RunLifecycle`。
|
||||
|
||||
## 5. Retry:不能让“再试一次”藏起来
|
||||
|
||||
重试会增加成本和延迟,也可能重复副作用,因此不能由 SDK、HTTP Client 和各组件各自决定。当前策略是:
|
||||
|
||||
- Router 和 SemanticGuard 的特定技术失败最多尝试两次;
|
||||
- Diagnosis Agent、业务 Tool 和 EvidenceRepair 不做隐藏自动重试;
|
||||
- 取消、预算耗尽和协议错误不能被重试吞掉。
|
||||
|
||||
这里没有设计通用重试 DSL。首版只需要不可变的 `RetryPolicy`、稳定的失败分类和统一的 `HarnessRetryExecutor`,复杂配置系统反而会掩盖真实执行路径。
|
||||
|
||||
## 6. Audit:留下解释,而不是留下全部内容
|
||||
|
||||
线上排障需要知道:调用了哪个组件、用了多少 Token、Tool 是否完成、为什么停止、为什么发布 Fallback。但这不等于长期保存 Prompt、SQL、日志原文、Tool raw response 和模型 reasoning。
|
||||
|
||||
因此 Audit 长期保留的是有界元数据,完整 Tool 真相只在 canonical store 中短期存在。这个决策同时满足可回放和最小暴露原则。
|
||||
|
||||
主要代码入口:`DiagnosisTraceRecorder`、`TraceAuditEvents`、`ToolInvocationAuditSink`、`HarnessAgentAuditHook`、`ModelCallAuditor`。
|
||||
|
||||
## 7. 为什么没有合并成一个 RunManager
|
||||
|
||||
把上述职责都放进一个 `RunManager` 看起来更简单,但它会同时知道 HTTP、路由、预算、模型、Tool、数据库和 SSE,最后变成新的业务工作流引擎。
|
||||
|
||||
当前拆分依据不是“代码越细越好”,而是决策权不同:
|
||||
|
||||
| 组件 | 它拥有的决定 | 它不能决定 |
|
||||
|---|---|---|
|
||||
| Application | 请求走哪条应用分支、何时持久化和返回 | 业务根因是否成立 |
|
||||
| Core | 是否仍可执行、资源是否允许、哪个终态生效 | 返回正常报告还是 Fallback |
|
||||
| Retry | 某类技术失败是否允许下一 attempt | 修改业务结果 |
|
||||
| Audit | 记录哪些安全元数据 | 影响执行结果 |
|
||||
|
||||
## 8. 先记住这些
|
||||
|
||||
第一次阅读只需记住:
|
||||
|
||||
1. Application 对一次请求负责,Core 对执行不变量负责。
|
||||
2. RunContext 必须显式传播,所有模型和 Tool 共用同一预算与取消信号。
|
||||
3. 第一个终态获胜,迟到结果不能翻案。
|
||||
4. Retry 必须显式、分类、可审计。
|
||||
5. Audit 记录控制事实,不长期复制敏感正文。
|
||||
|
||||
下一篇:[02 Agent 接入与收敛](02-Agent接入与收敛-让推理可以工作也可以停止.md)。
|
||||
|
||||
需要查完整状态转换时,阅读[生命周期与状态](../Harness生命周期与状态.md);需要查全部类型时,阅读[组件全景](../Harness组件全景-职责-设计原因与边界.md)。
|
||||
@@ -0,0 +1,148 @@
|
||||
# 02 Agent 接入与收敛:让推理可以工作,也可以停止
|
||||
|
||||
这一篇只回答一个问题:**怎样保留 Agent 的自主推理,同时阻止它重复查询或无限空转?**
|
||||
|
||||
## 1. 先看一个具体问题
|
||||
|
||||
Diagnosis Agent 第一次查询 10:00 到 10:10 的支付日志,没有发现异常;第二次换了关键词,仍然没有新信息;第三次又提交了与第一次等价的查询。
|
||||
|
||||
仅设置“最多调用 10 次 Tool”只能限制最坏损失,却回答不了:
|
||||
|
||||
- 上一次结果有没有推进诊断?
|
||||
- 这次查询是否已经做过?
|
||||
- 连续多少次没有新信息后应该停止?
|
||||
- Agent 已经收到停止要求,为什么还能继续调用 Tool?
|
||||
- 停止后,怎样安全地向用户说明已经检查过什么?
|
||||
|
||||
这就是 Agent 接入层和 Progress 组件共同解决的问题。
|
||||
|
||||
## 2. 一轮 ReAct 怎样经过 Harness
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant A as Diagnosis Agent
|
||||
participant MI as Model Interceptor
|
||||
participant TI as Tool Interceptor
|
||||
participant P as Progress Tracker
|
||||
participant T as Tool Boundary
|
||||
|
||||
A->>MI: 发起一轮模型调用
|
||||
MI->>MI: 检查 Run、预留预算、记录 Token
|
||||
A->>TI: 请求调用 Tool
|
||||
TI->>P: 评价上一轮是否有信息增益
|
||||
P-->>TI: 可继续 / 重复 / 已饱和 / 协议错误
|
||||
alt 允许继续
|
||||
TI->>T: 执行业务 Tool
|
||||
T-->>TI: Control View + Model Observation
|
||||
TI->>P: 登记已完成调用和 scope
|
||||
TI-->>A: 只返回 Model Observation
|
||||
else 必须停止
|
||||
TI-->>A: STOP_REQUIRED
|
||||
end
|
||||
```
|
||||
|
||||
第一次阅读可以把它们理解为:
|
||||
|
||||
- **Agent 接入层是检查站**:把框架原生 ReAct loop 接入预算、Tool 和审计边界。
|
||||
- **Progress Tracker 是收敛记录器**:判断是否重复、是否连续无增益、是否必须停止。
|
||||
|
||||
## 3. 为什么复用框架 ReAct,而不是自己重写循环
|
||||
|
||||
Diagnosis Agent 需要模型原生 Tool Calling 和多轮 ReAct。项目没有再写一套 `while` 循环,而是通过 Factory、Model Interceptor、Tool Interceptor 和 Audit Hook 接入框架。
|
||||
|
||||
这样做的决策依据是:
|
||||
|
||||
- ReAct 的业务推理由框架和模型负责;
|
||||
- 预算、Tool 授权、事实保存和停止协议由 Harness 负责;
|
||||
- 两者通过明确边界连接,不互相复制实现。
|
||||
|
||||
如果 Harness 自己维护另一套 ReAct 状态机,就会同时出现“框架认为的下一步”和“Harness 认为的下一步”,调试时很难确定谁才是真理源。
|
||||
|
||||
主要代码入口:`DiagnosisAgentFactory`、`DiagnosisAgentUseCase`、`HarnessModelInterceptor`、`HarnessToolInterceptor`、`HarnessEvidenceTools`。
|
||||
|
||||
## 4. Model Interceptor:每一轮模型调用都必须记账
|
||||
|
||||
一个 Diagnosis Agent 执行不等于一次模型调用。ReAct 可能经历多轮思考和 Tool 返回,因此每一轮都要:
|
||||
|
||||
1. 检查 Run 是否仍然 active;
|
||||
2. 在调用前预留模型预算;
|
||||
3. 调用后记录 Provider 返回的 Token usage;
|
||||
4. 再次检查取消或迟到结果。
|
||||
|
||||
Interceptor 不负责重试模型,也不判断输出是否支持根因。它只确保框架内部的每轮调用无法绕过 Harness。
|
||||
|
||||
## 5. Tool Interceptor:先检查进展,再允许查询
|
||||
|
||||
模型提交的 Tool Call 不只是业务参数,还携带对上一轮结果的评价。Tool Interceptor 会依次检查:
|
||||
|
||||
- previous observation 是否完整、顺序是否正确;
|
||||
- 上一轮是 `GAINED` 还是 `NO_GAIN`;
|
||||
- 当前 Tool scope 是否已经完成过;
|
||||
- 收集状态是否已经 `SATURATED`;
|
||||
- 通过检查后,才把业务请求交给 ToolBoundary。
|
||||
|
||||
它不执行后端查询,也不再次扣 Tool 预算。实际执行属于 ToolBoundary;收敛状态属于 ProgressTracker。Interceptor 只是两者与 ReAct 框架之间的接合点。
|
||||
|
||||
## 6. 为什么“有结果”不等于“有信息增益”
|
||||
|
||||
Tool 可以客观判断是否返回候选内容,却不知道这些内容是否推进了当前假设。例如同一条超时日志再次出现:
|
||||
|
||||
```text
|
||||
InvocationStatus = READY
|
||||
EvidenceStatus = EVIDENCE_FOUND
|
||||
InformationGain = NO_GAIN
|
||||
```
|
||||
|
||||
前三个状态回答不同问题:查询是否完成、是否有候选内容、内容是否推进当前诊断。把它们合成一个 `SUCCESS` 会让系统无法正常收敛。
|
||||
|
||||
当前由 Agent 在**下一次 Tool Call** 中评价上一轮的信息增益。这样评价发生在它真正做出下一步行动时,Harness 也能机械检查调用顺序,而不需要再增加一个 Progress Judge 模型。
|
||||
|
||||
## 7. Progress Tracker 保存什么
|
||||
|
||||
Tracker 只保存控制所需的最小状态:
|
||||
|
||||
- 已完成的 `tool_call_id` 和规范化 scope;
|
||||
- 哪个结果仍等待 Agent 评价;
|
||||
- 连续 `NO_GAIN` 次数;
|
||||
- 收集状态和停止原因;
|
||||
- 停止指令是否已经交付。
|
||||
|
||||
它不保存 raw response,也不判断日志是否证明支付线程池耗尽。业务价值判断仍由 Diagnosis Agent 负责,事实内容仍由 canonical store 负责。
|
||||
|
||||
主要代码入口:`DiagnosisProgressTracker`、`ToolScopeNormalizer`、`DiagnosisProgressProjector`。
|
||||
|
||||
## 8. 为什么停止不直接等于失败
|
||||
|
||||
连续无增益后停止,是一次正常的受控收敛,不是技术异常。Harness 会从已经完成的 canonical Tool 记录中投影 `ProgressSnapshot`,只保留可验真的检查范围、观察事实和限制,再形成安全 Fallback。
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
N["连续 NO_GAIN 或重复查询"] --> S["CollectionState = SATURATED"]
|
||||
S --> X["拒绝新的证据 Tool"]
|
||||
X --> P["生成安全 ProgressSnapshot"]
|
||||
P --> F["发布证据不足的 Fallback"]
|
||||
```
|
||||
|
||||
因此,“没有找到足够证据”可以正常结束;只有违反进展协议、预算耗尽且没有安全进展等情况,才可能升级为失败。
|
||||
|
||||
## 9. 为什么没有采用其他方案
|
||||
|
||||
| 方案 | 没有采用的原因 |
|
||||
|---|---|
|
||||
| 只设 Tool 次数上限 | 只能止损,不能识别查询已经没有价值 |
|
||||
| Harness 根据结果条数判断增益 | 条数是客观统计,不代表是否推进业务假设 |
|
||||
| 增加 Progress Judge Agent | 增加模型成本和新的非确定性判断点 |
|
||||
| 用自然语言相似度判断重复 | 首版难以稳定解释误判,当前只做 typed 参数级 scope 规范化 |
|
||||
| 把剩余预算告诉模型 | 容易让模型围绕阈值博弈,硬限制应由 Harness 保持 |
|
||||
|
||||
## 10. 先记住这些
|
||||
|
||||
1. 框架负责 ReAct loop,Harness 通过 Interceptor 接入控制能力。
|
||||
2. 预算回答“还能不能查”,信息增益回答“继续查有没有价值”。
|
||||
3. Tool 是否有结果与结果是否推进诊断是两件事。
|
||||
4. ProgressTracker 只保存控制状态,不保存完整证据。
|
||||
5. 信息饱和可以正常发布 Fallback,不等于 Run 执行失败。
|
||||
|
||||
下一篇:[03 Tool 事实边界](03-Tool事实边界-让模型看到必要信息而系统保留真相.md)。
|
||||
|
||||
需要深入停止协议时,阅读[信息增益停止专题](../Harness信息增益停止-让无证据诊断正常收敛.md)。
|
||||
@@ -0,0 +1,135 @@
|
||||
# 03 Tool 事实边界:让模型看到必要信息,系统保留真相
|
||||
|
||||
这一篇只回答一个问题:**Tool 返回的数据应该由谁保管,模型究竟可以看到多少?**
|
||||
|
||||
## 1. 先看一个具体问题
|
||||
|
||||
Agent 查询支付日志,后端一次返回了几千行内容,其中包含重复堆栈、敏感字段和远超上下文窗口的正文。
|
||||
|
||||
直接把原始结果塞回模型会产生几个问题:
|
||||
|
||||
- 敏感数据进入模型上下文;
|
||||
- 大结果挤占 Token,真正关键的证据反而被淹没;
|
||||
- 模型后续引用经过裁剪的文本,系统无法证明它对应哪次真实调用;
|
||||
- 若只保存裁剪结果,EvidenceGuard 又失去了独立验真的事实来源;
|
||||
- 若把完整 raw 长期写入数据库,泄露面和存储成本都会扩大。
|
||||
|
||||
所以 Tool 设计的核心不是“怎样调用后端”,而是**谁拥有原始事实,以及不同消费者应该看到哪一层数据**。
|
||||
|
||||
## 2. 一次 Tool 调用的数据怎样变化
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
REQ["Typed Tool Request"] --> B["ToolBoundary<br/>权限、只读、预算、大小检查"]
|
||||
B --> RAW["Backend Raw Result"]
|
||||
RAW --> P["Tool-specific Projector"]
|
||||
P --> C["Canonical Invocation<br/>短期保存 request、raw、agent_result 和状态"]
|
||||
C --> CV["Control View<br/>供 Harness 判断状态和进展"]
|
||||
C --> MO["Model Observation<br/>供 Agent 推理的白名单内容"]
|
||||
C --> EG["EvidenceGuard<br/>按当前 Run 独立验真"]
|
||||
B -.-> AU["Durable Audit<br/>长期只留有界元数据"]
|
||||
```
|
||||
|
||||
第一次阅读只需区分三份内容:
|
||||
|
||||
- **Canonical Invocation**:系统在当前 Run 内短期保管的完整调用真相。
|
||||
- **Control View**:Harness 用来判断状态、scope 和证据数量的控制字段。
|
||||
- **Model Observation**:模型真正看到的安全、有限内容。
|
||||
|
||||
## 3. ToolBoundary:所有 Tool 的统一入口
|
||||
|
||||
RAG、日志和 MySQL 后端完全不同,但它们都必须遵守相同的不变量:
|
||||
|
||||
1. Run 仍然 active,调用 ID 与当前 Run 匹配;
|
||||
2. Tool 已授权,请求声明和实际行为保持只读;
|
||||
3. 调用前预算允许,结果大小没有越界;
|
||||
4. canonical 状态只能从 `PROJECTING` 进入 `READY` 或 `ERROR`;
|
||||
5. 长期 Audit 不保存完整请求和 raw response。
|
||||
|
||||
如果每个 Adapter 自己实现这些规则,三种 Tool 很快会出现不同的错误码、预算顺序和存储行为。ToolBoundary 集中处理共同规则,Adapter 只处理业务协议。
|
||||
|
||||
ToolBoundary 不判断信息增益,也不判断某条日志是否支持根因。它只保证调用合法、状态可信、结果有界。
|
||||
|
||||
主要代码入口:`ToolBoundary`、`ToolCallRequestEnvelope`、`ToolBoundaryResult`、`ToolBoundaryErrorCode`。
|
||||
|
||||
## 4. Projector:把后端结果变成稳定事实
|
||||
|
||||
不同后端的 raw 数据不能直接成为 Agent 契约:
|
||||
|
||||
- RAG 需要限制证据数量和摘录长度,并保留文档身份;
|
||||
- Logs 需要限制事件、聚合 pattern,明确实际时间范围和是否截断;
|
||||
- MySQL 需要限制行、列、单元格和总 bytes,并保留查询 scope。
|
||||
|
||||
每种 Tool 使用自己的 `ToolResultProjector`。Projector 负责标准化和计算客观 `EvidenceStatus`,但它不调用模型,也不判断这些事实是否足以支持最终结论。
|
||||
|
||||
这是刻意保留的业务差异。强行做一个“万能 JSON Cleaner”,往往会丢掉每类证据真正需要的身份和范围信息。
|
||||
|
||||
## 5. Canonical Store:为什么要保留独立真相
|
||||
|
||||
Agent Observation 是为推理优化的,它会被裁剪和白名单投影,不能反过来充当验真依据。Canonical Store 保存当前调用的 request、raw、标准化 `agent_result`、状态和时间,使 EvidenceGuard 能绕开 Agent 上下文独立读取事实。
|
||||
|
||||
当前选择 Redis TTL,是因为完整调用记录:
|
||||
|
||||
- 只在当前 Run 的验证阶段需要;
|
||||
- 可能包含敏感内容,不应永久保存;
|
||||
- 需要按 `runId + tool_call_id` 快速定位;
|
||||
- 容量必须有硬上限,读取不能自动续期。
|
||||
|
||||
raw 超限时选择失败,而不是悄悄截断。因为一旦截断,canonical record 就不再代表后端真实返回,后续验真会建立在不完整事实之上。
|
||||
|
||||
主要代码入口:`CanonicalInvocationStore`、`RedisCanonicalInvocationStore`、`CanonicalToolInvocation`、`CanonicalInvocationLimits`。
|
||||
|
||||
## 6. Model Observation:模型只拿完成任务所需的内容
|
||||
|
||||
即使 canonical `agent_result` 已经标准化,其中仍可能包含 Harness 控制字段。`ToolResultViewProjector` 会进一步拆成:
|
||||
|
||||
```text
|
||||
Control View:status、evidence count、scope、截断状态等
|
||||
Model Observation:有界证据正文、可读来源和下一步推理所需字段
|
||||
```
|
||||
|
||||
模型不会看到 raw response、Redis key、预算阈值、重复指纹和完整 Harness 状态。这样既减少上下文噪声,也避免模型利用或复述内部控制信息。
|
||||
|
||||
## 7. MySQL 为什么还需要单独的只读沙箱
|
||||
|
||||
`readOnly=true` 只是请求声明,不能证明模型生成的 SQL 安全。MySQL Tool 在进入真实执行前还要经过:
|
||||
|
||||
- JSqlParser AST 解析;
|
||||
- 单条 SELECT 和保守语法子集限制;
|
||||
- 数据源、schema、table、column 精确 allowlist;
|
||||
- 禁止投影通配符和元数据探测;
|
||||
- 只读账号、timeout、LIMIT 与最大行数。
|
||||
|
||||
Validator 产生已经批准的 `MysqlQueryPlan`,Executor 只接受这个 plan,不重新信任原始字符串。这是纵深防御:任何一层都不能单独被当作完整授权。
|
||||
|
||||
## 8. 为什么短期真相和长期审计要分开
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
C["Canonical Store<br/>完整、敏感、短期"] -->|"用于"| V["当前 Run 验真"]
|
||||
A["Durable Audit<br/>有界、元数据、长期"] -->|"用于"| O["排障、统计、成本对账"]
|
||||
```
|
||||
|
||||
把两者合并会走向两个极端:要么长期复制所有敏感正文,要么为了安全只存元数据,导致当前 Run 无法验真。分开后,每种存储只承担自己的责任。
|
||||
|
||||
## 9. 为什么没有采用其他方案
|
||||
|
||||
| 方案 | 没有采用的原因 |
|
||||
|---|---|
|
||||
| raw 直接返回 Agent | 泄露、超限、噪声和无法独立验真 |
|
||||
| 只保存 Agent Observation | 投影内容不是完整事实,Guard 会依赖模型看到的版本 |
|
||||
| 完整 raw 永久写 MySQL | 扩大敏感数据暴露面和长期存储成本 |
|
||||
| raw 超限后静默截断 | canonical truth 会变成不完整真相 |
|
||||
| 所有 Tool 共用万能 Projector | 无法保持日志、RAG、表格各自的身份和 scope 语义 |
|
||||
|
||||
## 10. 先记住这些
|
||||
|
||||
1. ToolBoundary 统一执行规则,Adapter 处理具体后端。
|
||||
2. Canonical Invocation 是系统真相,Model Observation 是模型视图。
|
||||
3. Agent 看到的内容不能反过来成为 EvidenceGuard 的事实来源。
|
||||
4. 完整真相短期保存,长期 Audit 只留有界元数据。
|
||||
5. Tool 找到候选内容,不代表它支持最终根因。
|
||||
|
||||
下一篇:[04 验证与发布](04-验证与发布-让未经证明的结论无法越过出口.md)。
|
||||
|
||||
需要深入字段和存储取舍时,阅读[Tool 双视图专题](../Harness-Tool双视图-从原始结果到可验证证据.md)。
|
||||
@@ -0,0 +1,162 @@
|
||||
# 04 验证与发布:让未经证明的结论无法越过出口
|
||||
|
||||
这一篇只回答一个问题:**Agent 写完诊断草稿以后,为什么还不能直接返回给用户?**
|
||||
|
||||
## 1. 先看一个具体问题
|
||||
|
||||
Agent 在报告中写道:
|
||||
|
||||
> 10:03 出现数据库连接池耗尽,因此支付接口大量超时。
|
||||
|
||||
它同时引用了一次日志 Tool Call。系统即使确认这个调用真实存在、属于当前 Run,而且日志中确实出现“connection timeout”,仍然不能直接发布上述结论。
|
||||
|
||||
因为这里至少有三个不同问题:
|
||||
|
||||
1. Agent 引用的 Tool Call 是不是真的?
|
||||
2. 真实日志是否足以证明“数据库连接池耗尽导致支付超时”?
|
||||
3. 验证失败后,系统最终应该向用户发布什么?
|
||||
|
||||
它们分别属于 EvidenceGuard、SemanticGuard 和 Release。
|
||||
|
||||
## 2. 草稿怎样通过发布链
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
D["DiagnosisDraft<br/>Agent 写出的草稿"] --> E["EvidenceGuard<br/>机械验证引用和归属"]
|
||||
E -->|"结构或引用可修复"| R["EvidenceRepair<br/>只修引用,不改语义"]
|
||||
R --> E
|
||||
E -->|"得到已验真证据"| S["SemanticGuard<br/>判断证据是否支持报告"]
|
||||
S -->|"SUPPORTED"| P["Release SUCCESS<br/>发布原始安全 Draft"]
|
||||
E -->|"无法验真"| F["SafeFallback"]
|
||||
S -->|"UNSUPPORTED 或不可用"| F
|
||||
F --> O["Release FALLBACK"]
|
||||
```
|
||||
|
||||
第一次阅读只需记住:
|
||||
|
||||
- **EvidenceGuard 验真**:证明引用确实来自当前 Run 的已完成 Tool 调用。
|
||||
- **SemanticGuard 验义**:判断这些真实证据是否支持报告中的结论。
|
||||
- **Release 决定出口**:只发布验证通过的原 Draft,或者发布确定性的 SafeFallback。
|
||||
|
||||
## 3. EvidenceGuard:代码能证明的事交给代码
|
||||
|
||||
EvidenceGuard 会检查:
|
||||
|
||||
- Draft 结构和 analysis ID 是否合法;
|
||||
- 所有结论分析是否形成完整引用闭包;
|
||||
- `tool_call_id` 是否属于当前 Run;
|
||||
- canonical invocation 是否为 `READY`;
|
||||
- `EvidenceStatus` 与引用类型是否一致;
|
||||
- 引用的 evidence 是否确实存在于标准化 Tool 结果中。
|
||||
|
||||
这些都是确定性问题,不需要模型判断。让模型验证自己的引用,会让同一个非确定性来源同时当作者和裁判。
|
||||
|
||||
验证通过后,EvidenceGuard 生成 `VerifiedEvidenceSnapshot`。它只包含 SemanticGuard 所需的已验真证据、来源和 scope,不包含 Tool raw response。
|
||||
|
||||
主要代码入口:`EvidenceGuard`、`EvidenceGuardResult`、`VerifiedEvidenceSnapshot`、`EvidenceViolation`。
|
||||
|
||||
## 4. EvidenceRepair:为什么允许修,又为什么只能修一次
|
||||
|
||||
有些 Draft 的业务语义可能没有问题,只是引用结构出错,例如漏写一个 analysis ID。直接丢弃会浪费一次昂贵诊断,因此系统允许一次 EvidenceRepair。
|
||||
|
||||
但 Repair 不是第二个报告作者。它必须满足:
|
||||
|
||||
```text
|
||||
修复前用户可见语义 == 修复后用户可见语义
|
||||
```
|
||||
|
||||
它只能修结构和引用,不能新增事实、改变结论或补写建议;修复后还必须重新经过 EvidenceGuard。若语义视图发生变化,Repair 立即失败。
|
||||
|
||||
只允许一次,是为了避免形成“修复失败再修复”的隐式 Agent loop,也让成本和执行路径保持可解释。
|
||||
|
||||
## 5. SemanticGuard:引用真实不等于推论成立
|
||||
|
||||
一条真实的 `connection timeout` 日志,可能来自下游网络问题,也可能只是故障结果,不能自动证明数据库连接池耗尽。
|
||||
|
||||
这类支持关系无法完全用规则判断,因此 SemanticGuard 使用一次隔离的模型调用,只接收:
|
||||
|
||||
- 用户原始问题;
|
||||
- Draft 的用户可见语义;
|
||||
- EvidenceGuard 产生的 verified snapshot。
|
||||
|
||||
它没有 Tool、没有记忆、没有 ReAct loop、不访问 Redis,也不能改写报告。输出只有 `SUPPORTED / UNSUPPORTED` 和审计原因。
|
||||
|
||||
这个设计没有追求一个看似精确的置信度分数。首版真正需要的是发布门禁,而未经校准的 0.73 并不能形成比二元判定更可靠的协议。
|
||||
|
||||
主要代码入口:`SemanticGuard`、`SemanticGuardInput`、`SemanticGuardDecision`、`GuardModelCall`。
|
||||
|
||||
## 6. Release:为什么必须只有一个出口
|
||||
|
||||
如果 Agent、Guard、Application 都能各自构造最终结果,会出现:
|
||||
|
||||
- 同一验证失败被不同层解释成不同文案;
|
||||
- 某个分支忘记经过 SemanticGuard;
|
||||
- 迟到的 Draft 绕过已经确定的取消或失败;
|
||||
- Fallback 混入未经验证的 Agent 内容。
|
||||
|
||||
`DiagnosisReleaseUseCase` 因此拥有唯一发布决策。它协调 EvidenceGuard、可选 Repair、SemanticGuard 和 SafeFallbackFactory,但自己不写新根因。
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
V{"可以安全发布正常报告吗?"}
|
||||
V -->|"引用真实且语义受支持"| OK["ReleaseOutcome.SUCCESS<br/>发布原 Draft"]
|
||||
V -->|"证据不足或验证不通过"| FB["ReleaseOutcome.FALLBACK<br/>发布 SafeFallback"]
|
||||
V -->|"无法形成安全业务结果"| ER["ReleaseOutcome.FAILED"]
|
||||
```
|
||||
|
||||
这里必须区分:
|
||||
|
||||
```text
|
||||
RunState.SUCCESS + ReleaseOutcome.FALLBACK
|
||||
```
|
||||
|
||||
这表示系统完整、安全地处理了请求,但证据不足以发布正常诊断结论。Fallback 是安全产品结果,不等于执行失败。
|
||||
|
||||
## 7. SafeFallback:不是让另一个模型重新回答
|
||||
|
||||
验证失败后再次让模型“写得保守一点”,仍然可能产生新事实。SafeFallback 因此由代码确定性构造,只允许使用:
|
||||
|
||||
- 已验真的来源和检查范围;
|
||||
- 可以安全表达的观察事实;
|
||||
- 当前证据的限制;
|
||||
- 面向补充数据的下一步建议;
|
||||
- 稳定、可公开的问题类型。
|
||||
|
||||
它不会包含 Agent 原 Draft 的未验证结论、SemanticGuard 内部原因、Provider 错误或异常栈。
|
||||
|
||||
主要代码入口:`DiagnosisReleaseUseCase`、`EvidenceRepair`、`SafeFallbackFactory`、`DiagnosisReleaseResult`。
|
||||
|
||||
## 8. Contract:为什么状态和结果必须类型化
|
||||
|
||||
发布链横跨模型 JSON、Java、Redis、数据库和 SSE。如果各层都使用自由字符串 `SUCCESS`,很快就无法区分:
|
||||
|
||||
- Tool 调用完成;
|
||||
- Tool 找到候选证据;
|
||||
- 语义审查通过;
|
||||
- Run 执行成功;
|
||||
- 最终发布正常报告。
|
||||
|
||||
Contract 包用不同类型保持这些问题正交,例如 `InvocationStatus`、`EvidenceStatus`、`SemanticVerdict`、`RunState` 和 `ReleaseOutcome`。类型多不是为了复杂,而是为了阻止一个含糊的 `status` 穿过整个系统。
|
||||
|
||||
## 9. 为什么没有采用其他方案
|
||||
|
||||
| 方案 | 没有采用的原因 |
|
||||
|---|---|
|
||||
| 只检查 Tool Call ID 存在 | 只能证明引用存在,不能证明归属、状态和内容一致 |
|
||||
| 一个 Verifier Agent 同时验引用和语义 | 混合确定性与非确定性判断,失败后难以定位责任 |
|
||||
| Guard 自动改写报告 | Guard 会变成第二个作者,并可能引入未验证事实 |
|
||||
| SemanticGuard 使用 Tool 再查证 | 会形成第二条诊断链,预算和证据归属变复杂 |
|
||||
| 验证失败直接抛技术错误 | 证据不足是正常业务结果,用户仍需要安全说明 |
|
||||
| 用置信度阈值发布 | 未校准分数不能充当可靠安全协议 |
|
||||
|
||||
## 10. 先记住这些
|
||||
|
||||
1. Draft 是候选结果,不是已发布报告。
|
||||
2. EvidenceGuard 用代码证明引用真实,SemanticGuard 判断证据是否支持语义。
|
||||
3. Repair 只能修引用,不能改变用户可见语义,而且只尝试一次。
|
||||
4. Release 是唯一出口,只发布原 Draft 或确定性 SafeFallback。
|
||||
5. `FALLBACK` 可以对应一次正常完成的 Run。
|
||||
|
||||
四篇组件导读到这里结束。需要回看整体路径时返回[组件学习地图](README.md)。
|
||||
|
||||
需要深入验证规则时,阅读[证据安全链专题](../Harness证据安全链-从引用真实到结论可发布.md);需要查所有 contract 类型时,阅读[组件全景](../Harness组件全景-职责-设计原因与边界.md)。
|
||||
@@ -0,0 +1,22 @@
|
||||
# Harness 组件:从一次请求逐步认识
|
||||
|
||||
这里不按照 Java 包逐个介绍组件,而是沿着一次诊断请求,分四步回答四个问题。
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Q["一个诊断请求来了"] --> C1["01 运行控制<br/>怎样保证这次执行受控"]
|
||||
C1 --> C2["02 Agent 接入与收敛<br/>怎样让 Agent 工作但不空转"]
|
||||
C2 --> C3["03 Tool 事实边界<br/>怎样安全地取得事实"]
|
||||
C3 --> C4["04 验证与发布<br/>怎样决定结果能否交给用户"]
|
||||
```
|
||||
|
||||
| 顺序 | 先回答的问题 | 涉及的职责域 |
|
||||
|---|---|---|
|
||||
| [01 运行控制](01-运行控制-让一次请求有边界.md) | 谁创建 Run,谁限制资源,谁记录它如何结束? | Application、Core、Retry、Audit |
|
||||
| [02 Agent 接入与收敛](02-Agent接入与收敛-让推理可以工作也可以停止.md) | 怎样使用框架 ReAct,同时阻止重复查询和无增益空转? | Agent、Progress |
|
||||
| [03 Tool 事实边界](03-Tool事实边界-让模型看到必要信息而系统保留真相.md) | Tool 原始结果由谁保存,模型究竟能看到什么? | Tool |
|
||||
| [04 验证与发布](04-验证与发布-让未经证明的结论无法越过出口.md) | 引用真实是否等于结论成立,最终由谁决定发布? | Guard、Release、Contract |
|
||||
|
||||
建议一次只读一篇。每篇读到“先记住这些”就可以停下;类名只在最后用于定位代码。
|
||||
|
||||
需要查全部生产类型时,再使用[组件全景参考手册](../Harness组件全景-职责-设计原因与边界.md)。
|
||||
Reference in New Issue
Block a user