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

17 KiB
Raw Permalink Blame History

Harness tool 域代码学习笔记:工具的注册、调用与执行链路

更新日期:2026-08-03 主题:tool 域 49 个文件的完整链路——装配 → 注册 → 调用 → 执行 → 返回,拆分阶段讲,最后合并 设计视角:Harness 组件全景-职责-设计原因与边界 §7 Tool 代码视角:Harness progress 代码学习笔记(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 注入链

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 两个平行的注册表

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:方法引用绑定实际调用者

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 的必炸保护:

FunctionToolCallback.builder(name, ignored -> {
    throw new IllegalStateException(
        "Harness evidence Tools require the framework Tool interceptor");
})
场景 callback 行为
正常(拦截器接管) 不执行(拦截器直接 evidenceTools.invoke)
异常(某处 handler.call / 直接调) 抛异常 → 暴露「绕过门禁」的 bug

结构性保证:唯一能执行证据工具的路径 = 拦截器接管 → 门禁永远在线;绕过不可能静默成功(fail-fast)。

② mysql 条件注册:

// 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 输入类:

definition(AgentToolContracts.LOOKUP_KNOWLEDGE, ..., RagToolCall.class)

输入类型 = 模型必须匹配的 Schema——RagToolCall{previous_observation, input}。parse 时用 FAIL_ON_UNKNOWN_PROPERTIES + FAIL_ON_TRAILING_TOKENS 强制匹配,模型输出多一个字段都炸(INVALID_ENVELOPE)。


4. 阶段三:调用(拦截器 → 注册表)

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 = 接线员

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 五阶段(执行门禁)

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. 阶段五:返回(双源校验 → 记账 → 有界观察)

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. 全链路合起来

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