# 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)
模型可见 Schema + 必炸"] INV["invokers(Map)
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["具体后端
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` |