Files
SuperBizAgent-java/mvp/engineering/harness/Harness tool 域代码学习笔记-工具的注册调用与执行链路.md
zhuyongxin ff0752a16c docs(harness): annotate tool domain classes and add tool chain learning notes
- Annotate 43 tool domain classes (contract/projection/boundary/store/adapter/mysql)
- Add tool registration and execution chain learning note
- Add tool call chain runtime journey note (model decision to observation)
2026-08-04 18:37:17 +08:00

383 lines
17 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Harness tool 域代码学习笔记:工具的注册、调用与执行链路
**更新日期**:2026-08-03
**主题**:tool 域 49 个文件的完整链路——装配 → 注册 → 调用 → 执行 → 返回,拆分阶段讲,最后合并
**设计视角**:[Harness 组件全景-职责-设计原因与边界](Harness组件全景-职责-设计原因与边界.md) §7 Tool
**代码视角**:[Harness progress 代码学习笔记](Harness%20progress%20代码学习笔记-从拦截器五道门到唯一发布点.md)(progress 域衔接,本笔记是 tool 域)
## 1. 定位:tool 域管什么
**职责**:工具如何安全执行、保存真相并只暴露必要内容。
| 问题 | 不解决会怎样 | 催生的层 |
|---|---|---|
| 每个 Adapter 自己写授权/预算/审计 → 漂移 | 三个工具三种行为 | **Boundary**(统一门禁) |
| raw 结果直接给模型 | 敏感数据泄露、超大响应、无结构 | **Projector**(有界投影) |
| 模型可能编造证据 | 结论无法验真、审计黑洞 | **Store**(canonical 真相) |
| MySQL 查询不可控 | 写库、删库、危险 SQL | **MySQL 沙箱**(只读红线) |
**49 文件分 6 组**:
| 组 | 数量 | 角色 |
|---|---|---|
| Contract | 18 | 跨层类型化语言(Call/Request/Result) |
| Boundary | 7 | 统一门禁(ToolBoundary + 配套) |
| Projection | 3 | raw → 有界 agent 契约 |
| Store | 9 | canonical 真相持久化 |
| Adapter | 3 | 接线(boundary + 后端 + 投影器) |
| MySQL 沙箱 | 9 | 只读执行 + fail-closed 校验 |
**核心设计**:几乎不用继承——用「接口 + 组合 + 函数式接口」三件套解耦。
---
## 2. 阶段一:装配(config → bean 注入链)
### 2.1 注入链
```mermaid
flowchart TD
subgraph 底层
R["RedisCanonicalInvocationStore"]
B["ToolBoundary"]
RP["RagResultProjector"]
LP["QueryLogsResultProjector"]
end
subgraph 中层
RA["RagToolAdapter"]
QA["QueryLogsToolAdapter"]
MA["MysqlToolAdapter"]
end
subgraph 顶层
ET["HarnessEvidenceTools"]
end
R --> B
B --> RA
B --> QA
B --> MA
RP --> RA
LP --> QA
ET --> RA
ET --> QA
ET --> MA
```
注入规律:所有 `@Bean` 方法参数 = 依赖注入点;**没有任何类 extends 别人**。
### 2.2 为什么不用继承
```
❌ 继承方案(没采用):
abstract class BaseToolAdapter { ... }
RagToolAdapter extends BaseToolAdapter { ... }
→ 加一个工具就得改基类,横切逻辑散落
✅ 组合方案(实际):
Adapter = ToolBoundary(门禁) + 后端(执行) + Projector(投影)
↑ 构造注入持有引用,不是继承
→ 每个 Adapter 独立组装,改一个不影响其他
```
组合的优势:
1. **ToolBoundary 对三种工具完全无感知**——只认 `ToolExecutor` / `ToolResultProjector` 两个端口,三个工具共用同一个实例;
2. **后端各不相同**(LookupKnowledgeTool / QueryLogsTools / JDBC),无法抽象成共同基类,用函数式接口适配;
3. **开闭原则**:加新工具 = 新写 Adapter + Projector + config 注册,**不动已有类**(门禁/真相/进度自动继承)。
---
## 3. 阶段二:注册(HarnessEvidenceTools 门面)
### 3.1 两个平行的注册表
```mermaid
flowchart LR
subgraph fromAdapters
RAG["ragAdapter::execute"]
LOGS["logsAdapter::execute"]
MYSQL["mysqlAdapter::execute"]
end
subgraph HarnessEvidenceTools
direction TB
CALL["callbacks(List)<br/>模型可见 Schema + 必炸"]
INV["invokers(Map)<br/>toolName → bridge 闭包"]
end
RAG -->|bridge| INV
LOGS -->|bridge| INV
MYSQL -->|bridge| INV
INV -.同一个工具名串起.-> CALL
CALL --> MODEL["模型(可见工具目录)"]
INV --> INTERCEPTOR["拦截器(执行入口)"]
```
**同一工具名字符串串起两个表**:模型从 callbacks 决定调 `lookup_knowledge` → 拦截器用同一个名字去 invokers 取执行器。
### 3.2 bridge:方法引用绑定实际调用者
```java
private static EvidenceToolInvoker bridge(String toolName, AdapterCall adapter) {
// lambda 闭包捕获 adapter 实例 + 固定的 toolName
return (context, toolCallId, arguments) -> adapter.execute(
context,
new ToolCallRequestEnvelope(
context.runId(), toolCallId, toolName, arguments, true, true));
}
```
关联链(三层绑定):
```
① config:new RagToolAdapter(boundary, mapper, projector, backend)
→ adapter 实例已组合好 boundary + projector + backend
② fromAdapters:ragAdapter::execute 是「绑定实例的方法引用」
→ bridge lambda 捕获它 —— invoker 与 Adapter 的关联在此固化
③ 构造方法:按工具名常量 put 进 invokers —— "lookup_knowledge" → 捕获了 ragAdapter 的 lambda
```
**invoker 与调用者的关联 = 方法引用绑定**:取出来直接 `adapter.execute(...)`,不需要再查表找调用者。
### 3.3 三个关键设计点
**① callbacks 的必炸保护**:
```java
FunctionToolCallback.builder(name, ignored -> {
throw new IllegalStateException(
"Harness evidence Tools require the framework Tool interceptor");
})
```
| 场景 | callback 行为 |
|---|---|
| 正常(拦截器接管) | 不执行(拦截器直接 evidenceTools.invoke) |
| 异常(某处 handler.call / 直接调) | **抛异常** → 暴露「绕过门禁」的 bug |
结构性保证:唯一能执行证据工具的路径 = 拦截器接管 → 门禁永远在线;绕过不可能静默成功(fail-fast)。
**② mysql 条件注册**:
```java
// config
boolean mysqlEnabled = 数据源配置了 jdbcUrl ? true : false;
return fromAdapters(rag, logs, mysqlEnabled ? mysql : null);
// fromAdapters
EvidenceToolInvoker mysql = mysqlAdapter == null ? null : bridge(QUERY_MYSQL, mysqlAdapter::execute);
// 构造方法里 null 也不注册 invokers / callbacks
```
没配数据源 → query_mysql 从模型视野和执行注册表**都消失**(不暴露「必死工具」)。RAG/日志后端内置,无条件注册。
**③ definition 的 typed 输入类**:
```java
definition(AgentToolContracts.LOOKUP_KNOWLEDGE, ..., RagToolCall.class)
```
输入类型 = 模型必须匹配的 Schema——`RagToolCall{previous_observation, input}`。parse 时用 `FAIL_ON_UNKNOWN_PROPERTIES + FAIL_ON_TRAILING_TOKENS` 强制匹配,**模型输出多一个字段都炸**(INVALID_ENVELOPE)。
---
## 4. 阶段三:调用(拦截器 → 注册表)
```mermaid
sequenceDiagram
participant M as 模型
participant F as 框架 ReactAgent
participant I as HarnessToolInterceptor
participant ET as HarnessEvidenceTools
M->>F: 决定调用 lookup_knowledge(输出 tool_call JSON)
F->>I: 回调 interceptToolCall(request, handler)
I->>I: supports(toolName) ? 注册检查
I->>ET: parse(toolName, arguments, mapper) → typed Envelope
ET-->>I: ParsedAgentToolCall(previous_observation + input)
I->>I: 协议校验 / 判重(不通过不执行)
I->>ET: invoke(context, toolName, toolCallId, args)
ET->>I: bridge lambda → adapter.execute
```
调用链要点:
| 点 | 说明 |
|---|---|
| **工具名是模型决定的** | `request.getToolName()` 来自模型输出,拦截器拿它查 invokers |
| **invoke 前有三道门** | supports 分流 → parse 严格契约 → 协议/判重——判重不通过不执行 |
| **envelope 现造** | bridge 里构造,`authorized=true, readOnly=true` 写死——每个进 ToolBoundary 的信封都声明「已授权 + 只读」 |
| **工具名被闭包捕获** | 即使调用方传错名字,envelope 里也是正确的工具名(防混淆) |
---
## 5. 阶段四:执行(Adapter → ToolBoundary → 后端)
### 5.1 Adapter = 接线员
```mermaid
flowchart LR
AD["Adapter.execute"] -->|"boundary.execute(context, envelope,"| TB["ToolBoundary"]
AD -->|"executor = ignored -> legacyExecutor.execute(query)"| TB
AD -->|"projector = raw -> projector.project(...)"| TB
TB -->|"executor 跑 backend"| BK["具体后端<br/>LookupKnowledgeTool / QueryLogsTools / JDBC"]
TB -->|"projector 投影"| PR["RagResultProjector / QueryLogsResultProjector / MysqlResultProjector"]
TB -->|"markReady"| ST["CanonicalInvocationStore"]
```
**两个端口**:executor(跑 backend 拿 raw)+ projector(raw → 有界脱敏契约)。**模型永远看不到 raw**——这是执行链的核心目的。
### 5.2 LegacyExecutor vs 专用 Executor
| | RAG/日志 | MySQL |
|---|---|---|
| 后端来源 | 重构前旧类(LookupKnowledgeTool / QueryLogsTools) | 全新实现(JdbcMysqlReadOnlyExecutor) |
| 端口位置 | Adapter **内部**定义 LegacyExecutor | mysql **包**里定义 MysqlReadOnlyExecutor |
| 注入方式 | `backend::lookupKnowledge` 方法引用 / lambda | 直接注入专用实现 |
| 为什么 | 复用成熟旧代码 | 新工具直接面向沙箱设计 |
```
RAG: Adapter → LegacyExecutor(Adapter内部) → LookupKnowledgeTool(旧后端)
MySQL: Adapter → MysqlReadOnlyExecutor(mysql包) → JdbcMysqlReadOnlyExecutor(新实现)
```
### 5.3 ToolBoundary 五阶段(执行门禁)
```mermaid
flowchart TD
A["① preflight + Tool 预算 + request bytes → begin(PROJECTING)"]
B["② executor.execute(requestJson) → backend raw"]
C["③ raw 大小校验 + Run bytes 预留"]
D["④ projector.project(raw) → 有界 agent_result + evidenceStatus"]
E["⑤ agent_result 校验 + bytes → markReady(READY)"]
A --> B --> C --> D --> E
```
**三笔 bytes 预留**:request(①)/ raw(③)/ agent_result(⑤)。
### 5.4 脱敏与有界(投影器)
- **日志**:sanitize 抹掉密码/token/主机/Pod/IP/PID/SQL 字面量;均匀采样 + 模式聚合;
- **MySQL**:敏感列(password/token/secret 等)单元格 → `[REDACTED]`;行数/字符/字节三重截断;
- **RAG**:chunk 级去重 + 摘录截断 + fitBudget 总字节兜底。
---
## 6. 阶段五:返回(双源校验 → 记账 → 有界观察)
```mermaid
sequenceDiagram
participant TB as ToolBoundary
participant I as HarnessToolInterceptor
participant P as DiagnosisProgressTracker
participant M as 模型
TB-->>I: ToolBoundaryResult(READY/ERROR)
I->>I: 双源交叉验证(声明值 vs 内容重算 evidence_status)
alt 不一致
I->>M: OBSERVATION_CONTRACT_MISMATCH(拒绝)
else 一致
I->>P: recordCompleted(call, evidenceStatus)
P-->>I: 快照(NO_EVIDENCE 立即 NO_GAIN / FOUND 挂 pending)
I->>I: modelObservation 加工(含 stop_required / stopReason)
I-->>M: 有界 observation(模型永远看不到 raw)
end
```
**错误处理三种形态**:
| 场景 | 处理 |
|---|---|
| Adapter 业务/参数异常 | catch → `INVALID_REQUEST`(不泄露内部细节) |
| MySQL 安全异常 | `MysqlSecurityException` 单独 catch → `INVALID_REQUEST` |
| 日志后端缺失 | `ObjectProvider.getIfAvailable()` → 返回空结果 JSON(不炸) |
---
## 7. 全链路合起来
```mermaid
sequenceDiagram
participant C as config(启动)
participant ET as HarnessEvidenceTools(单例)
participant I as 拦截器(per-Run)
participant AD as Adapter(单例)
participant TB as ToolBoundary(单例)
participant ST as CanonicalStore(单例)
participant M as 模型
rect rgb(240, 248, 255)
Note over C,ET: ① 装配(应用启动一次)
C->>C: 建 boundary / 3 个 Adapter(注入 boundary+后端+投影器)
C->>ET: fromAdapters(rag, logs, mysql?)
ET->>ET: bridge → invokers + definition → callbacks(平行,同名串起)
end
rect rgb(255, 250, 240)
Note over M,AD: ② 调用+执行(每次 Tool Call)
M->>I: 模型决定工具名 → 框架回调拦截器
I->>I: supports 分流 → parse(typed 严格契约)→ 协议/判重
I->>ET: invoke → invokers.get(名字) → bridge 闭包
ET->>AD: adapter.execute(context, envelope[现造,授权只读写死])
AD->>TB: boundary.execute(context, envelope, executor, projector)
TB->>ST: begin(PROJECTING) → executor 跑 raw → projector 投影 → markReady(READY)
TB-->>I: ToolBoundaryResult
I->>I: 双源校验 → recordCompleted → modelObservation
I-->>M: 有界 observation(无 raw)
end
```
**四阶段汇总**:
| 阶段 | 做什么 | 关键类 |
|---|---|---|
| 装配 | Spring 组合依赖(无继承) | config / Adapter / ToolBoundary |
| 注册 | bridge 成 invoker + definition 成 callback(平行同名串起) | HarnessEvidenceTools |
| 调用 | supports → parse 严格契约 → 协议/判重 → invoke | Interceptor / HarnessEvidenceTools |
| 执行+返回 | boundary 五阶段 → 投影脱敏 → canonical → 双源校验 → 记账 → 有界观察 | Adapter / ToolBoundary / Projector / Store |
---
## 8. 易错点
| 易错 | 正确 |
|---|---|
| 必炸 = 死工具 | 必炸是**保护**:模型通过拦截器正常执行,只有绕过路径才炸 |
| LegacyExecutor 是通用 executor | 它只服务于「复用旧后端」;新工具直接注入专用 Executor(如 MysqlReadOnlyExecutor) |
| callbacks 是执行器 | 它是「模型可见目录」+ 必炸占位;真执行走 invokers |
| 执行链只有 executor | 还有 **projector**(raw → 有界契约)——模型永远看不到 raw |
| MySQL 没有 tool 类 | `JdbcMysqlReadOnlyExecutor` 就是它的后端执行类,只是不叫 Tool |
| 工具注册是静态列表 | **配置驱动**:没配数据源 → query_mysql 从两表消失 |
| 返回就是 ToolBoundaryResult | 返回后还有双源校验 → recordCompleted → modelObservation |
## 9. 面试话术合集(30 秒)
### 9.1 为什么不用继承
> "tool 域刻意不用继承:ToolBoundary 通过 ToolExecutor/ToolResultProjector 两个函数式端口接收执行和投影逻辑,三个 Adapter 各自用构造注入组合 boundary + 后端 + projector,HarnessEvidenceTools 再用 bridge 把 Adapter 包成统一的 EvidenceToolInvoker 注册表。类图里没有 extends 箭头——全是 has-a(组合)和函数适配(函数式接口),扩展新工具不改任何已有类。"
### 9.2 为什么必炸保护
> "必炸保护是执行不可绕过的结构性保证:证据工具的 ToolCallback 被故意定义为直接抛异常,使 handler 路径成为死路。唯一能执行证据工具的路径就是拦截器接管——预算、canonical、进度协议、脱敏门禁永远在线;任何绕过尝试要么抛异常暴露 bug(fail-fast),要么根本不执行(fail-closed)。非证据工具不需要门禁,所以拦截器放行、callback 正常。"
### 9.3 LegacyExecutor 是什么
> "LegacyExecutor 是 Adapter 内部定义的旧后端端口:RAG 和日志是重构前就有的工具,后端实现(backend::lookupKnowledge)被方法引用注入复用,通过 Adapter 包进 Harness 门禁——『旧后端复用,新门禁外挂』。它和 ToolBoundary 的 ToolExecutor 是两层:LegacyExecutor 是具体后端怎么查,ToolExecutor 是边界统一端口,Adapter 把前者包成后者。MySQL 是全新工具,没有 legacy,直接用新写的 MysqlReadOnlyExecutor。"
### 9.4 模型为什么看不到 raw
> "执行链是两个端口:executor 跑 backend 拿 raw,projector 把 raw 投影成有界脱敏契约(截断 + 脱敏 + 冻结 Schema)。ToolBoundary 只让 READY/ERROR 离开,模型拿到的是 modelObservation 加工后的有界观察——raw 只进 canonical Store 供审计和验真,永远不进入模型上下文。"
### 9.5 新工具怎么加(开闭原则)
> "新工具按 MySQL 模板:写 Contract 三件套 + 专用执行器 + 投影器 + Adapter,config 注册。要改的只有 AgentToolContracts 常量、HarnessEvidenceTools 构造、config;不用改 ToolBoundary、canonical、拦截器——新工具自动获得预算门禁、真相记录、脱敏投影、进度收敛、证据验真全套管控。"
## 10. 代码位置索引
| 类 | 文件 |
|---|---|
| `HarnessEvidenceTools` | `src/main/java/com/superbiz/agent/harness/agent/HarnessEvidenceTools.java`(agent 包,tool 域门面) |
| `RagToolAdapter` / `QueryLogsToolAdapter` / `MysqlToolAdapter` | `.../tool/adapter/` |
| `ToolBoundary` / `ToolBoundaryResult` / `ToolCallRequestEnvelope` / `ToolExecutor` / `ToolResultProjector` / `ProjectedToolResult` | `.../tool/boundary/` |
| `RagResultProjector` / `QueryLogsResultProjector` / `ToolProjectionLimits` | `.../tool/projection/` |
| `CanonicalInvocationStore` / `RedisCanonicalInvocationStore` / `CanonicalToolInvocation` / `ToolCallKeyFactory` / `CanonicalInvocationLimits` | `.../tool/store/` |
| Contract 18 个 | `.../tool/contract/` |
| `MysqlSqlValidator` / `JdbcMysqlReadOnlyExecutor` / `MysqlResultProjector` / `MysqlDataSourceDefinition` 等 | `.../tool/mysql/` |
| 装配 | `src/main/java/com/superbiz/agent/config/HarnessChatConfiguration.java` |