docs(mvp): add harness design and progressive guides

This commit is contained in:
zhuyongxin
2026-07-29 19:04:44 +08:00
parent 584639fa2a
commit 3a7eee8af4
15 changed files with 3550 additions and 0 deletions
@@ -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)。