- 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)
204 lines
9.2 KiB
Markdown
204 lines
9.2 KiB
Markdown
# Harness Tool 调用链:一次工具调用的完整旅程
|
||
|
||
**更新日期**:2026-08-03
|
||
**主题**:从「模型决定调用工具」到「模型收到观察」的运行时完整链路——拦截器 → invoke → Adapter → ToolBoundary → 返回 → 二次加工 → ToolCallResponse
|
||
**结构篇**:[Harness tool 域代码学习笔记-工具的注册调用与执行链路](Harness%20tool%20域代码学习笔记-工具的注册调用与执行链路.md)(讲装配/注册/静态结构)
|
||
**本文**:动态时序(一次调用怎么跑完)
|
||
|
||
## 1. 旅程全景(一张图)
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant M as 模型
|
||
participant F as 框架 ReactAgent
|
||
participant I as HarnessToolInterceptor(per-Run)
|
||
participant ET as HarnessEvidenceTools(单例)
|
||
participant AD as RagToolAdapter(单例)
|
||
participant TB as ToolBoundary(单例)
|
||
participant P as DiagnosisProgressTracker
|
||
|
||
rect rgb(240, 248, 255)
|
||
Note over M,I: 阶段 A:模型决定 → 拦截器(执行前)
|
||
M->>F: 输出 tool_call(工具名 + 参数 JSON)
|
||
F->>I: 回调 interceptToolCall(request, handler)
|
||
I->>I: ① supports 注册检查
|
||
I->>ET: ② parse(typed 严格契约)
|
||
ET-->>I: ParsedAgentToolCall(previous_observation + input)
|
||
I->>P: ③ 协议校验(pending 评价)+ 判重
|
||
end
|
||
|
||
rect rgb(255, 250, 240)
|
||
Note over I,TB: 阶段 B:invoke → 执行(backend + 投影)
|
||
I->>ET: ④ invoke(context, toolName, toolCallId, args)
|
||
ET->>AD: bridge 闭包 → adapter.execute(context, envelope)
|
||
AD->>TB: boundary.execute(context, envelope, executor, projector)
|
||
TB->>TB: ⑤ 五阶段:preflight/预算/begin → executor 跑 backend → 校验 → projector 投影 → markReady
|
||
TB-->>I: ToolBoundaryResult(READY/ERROR)
|
||
end
|
||
|
||
rect rgb(245, 255, 245)
|
||
Note over I,M: 阶段 C:返回 → 模型(执行后)
|
||
I->>I: ⑥ 双源校验(controlView 重读 evidence_status)
|
||
I->>P: ⑦ recordCompleted(NO_EVIDENCE 立即 NO_GAIN / FOUND 挂 pending)
|
||
I->>I: ⑧ modelObservation 加工(有界观察 + stop_required/reason)
|
||
I-->>F: ToolCallResponse.of(toolCallId, toolName, observation)
|
||
F-->>M: observation 作为本轮 tool 结果
|
||
end
|
||
```
|
||
|
||
**三个阶段**:A 执行前(模型决定→门禁)→ B 执行中(invoke→backend→投影)→ C 执行后(校验→记账→成型)。
|
||
|
||
---
|
||
|
||
## 2. 阶段 A:模型决定 → 拦截器(执行前)
|
||
|
||
### 2.1 模型怎么知道有这个工具
|
||
|
||
```
|
||
模型 → callbacks 里看到工具(名字+描述+Schema)→ 决定调用 lookup_knowledge
|
||
→ 输出 tool_call JSON(工具名 + 参数)
|
||
```
|
||
|
||
工具名是**模型决定的**——框架把模型输出包成 `ToolCallRequest`(含 toolName + arguments),回调拦截器。
|
||
|
||
### 2.2 拦截器的三道执行前门
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
A["① supports(toolName)?"] -->|"否(非证据工具)"| X["handler.call 透传"]
|
||
A -->|"是"| B["② parse:typed 严格契约<br/>FAIL_ON_UNKNOWN_PROPERTIES + FAIL_ON_TRAILING_TOKENS"]
|
||
B -->|"违规"| Y["协议处理(不执行)"]
|
||
B --> C["③ 协议校验(pending 评价)+ 判重"]
|
||
C -->|"重复"| Z["recordDuplicateScope(不执行)"]
|
||
C -->|"通过"| D["进入阶段 B:invoke"]
|
||
```
|
||
|
||
关键:**不是「拿到名字就执行」**——parse(模型输出必须精确匹配 `RagToolCall{previous_observation, input}`,多一个字段都炸 INVALID_ENVELOPE)、协议校验、判重,三道门不通过都不执行 backend。
|
||
|
||
---
|
||
|
||
## 3. 阶段 B:invoke → 执行(backend + 投影)
|
||
|
||
### 3.1 invoke 的委托链
|
||
|
||
```
|
||
I.invoke(context, "lookup_knowledge", "call-1", args)
|
||
→ ET.invokers.get("lookup_knowledge") ← 注册表取 bridge 闭包
|
||
→ bridge lambda:adapter.execute(context,
|
||
new ToolCallRequestEnvelope(runId, "call-1", "lookup_knowledge", args, true, true))
|
||
→ ragAdapter.execute(context, envelope)
|
||
→ boundary.execute(context, envelope, executor, projector)
|
||
```
|
||
|
||
**envelope 是 bridge 里现造的**:`authorized=true, readOnly=true` 写死——每个进 ToolBoundary 的信封都声明「已授权 + 只读」。
|
||
|
||
### 3.2 Adapter 组装两个函数(接线员)
|
||
|
||
```java
|
||
return boundary.execute(context, envelope,
|
||
// executor:跑 backend 拿 raw(LookupResult 序列化成 JSON 文本)
|
||
ignored -> objectMapper.writeValueAsString(legacyExecutor.execute(request.query())),
|
||
// projector:raw → 有界契约 + evidenceStatus
|
||
raw -> projector.project(request, envelope.toolCallId(), raw));
|
||
```
|
||
|
||
| 端口 | 干什么 | 产物 |
|
||
|---|---|---|
|
||
| `executor` | 调具体后端 | rawResponse(JSON 文本,执行链「货币」) |
|
||
| `projector` | 净化定型 | ProjectedToolResult(agentResult, evidenceStatus) |
|
||
|
||
**模型永远看不到 raw**——raw 只用于校验、落 canonical、投影。
|
||
|
||
### 3.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) 或 markError(ERROR)"]
|
||
A --> B --> C --> D --> E
|
||
```
|
||
|
||
返回 `ToolBoundaryResult(READY/ERROR)`——PROJECTING 永不外泄。
|
||
|
||
---
|
||
|
||
## 4. 阶段 C:返回 → 模型(执行后)
|
||
|
||
### 4.1 拦截器的二次加工(不是直接返回)
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
A["ToolBoundaryResult"] --> B{"status == READY?"}
|
||
B -->|"否"| E1["error observation<br/>BUDGET_EXHAUSTED 额外 markBudgetLimitReached"]
|
||
B -->|"是"| C["⑥ 双源校验:controlView 重读 evidence_status"]
|
||
C -->|"不一致"| E2["OBSERVATION_CONTRACT_MISMATCH 拒绝"]
|
||
C -->|"一致"| D["⑦ recordCompleted<br/>NO_EVIDENCE → 立即 NO_GAIN<br/>FOUND → 挂 pending"]
|
||
D --> F["⑧ modelObservation 加工<br/>(有界观察 + stop_required/reason)"]
|
||
F --> G["ToolCallResponse.of(...) → 框架 → 模型"]
|
||
```
|
||
|
||
### 4.2 双源校验(自洽性防线)
|
||
|
||
```text
|
||
源1:result.evidenceStatus() ← Projector 投影时计算的声明值
|
||
源2:controlView(agentResult).evidenceStatus() ← 从 agent_result 内容重读
|
||
一致 ? 通过 : OBSERVATION_CONTRACT_MISMATCH 拒绝
|
||
```
|
||
|
||
防止「声明有证据但内容空 / 声明无证据但内容有」的不一致状态进入 progress 记账。
|
||
|
||
### 4.3 给模型的对象形态
|
||
|
||
```
|
||
ToolCallResponse.of(toolCallId, toolName, observation)
|
||
observation = 有界观察:
|
||
正常结果:脱敏后的契约内容(可能裁剪)
|
||
饱和时: 附加 stop_required:true + reason
|
||
协议错误:repair_required:true + violation_type/期望ID/指令
|
||
```
|
||
|
||
框架把 observation 作为本轮 tool 结果给模型——**模型下一轮读取它,决定继续调用(带评价)还是输出 Draft 收尾**。
|
||
|
||
---
|
||
|
||
## 5. 旅程的衔接点(模型视角的闭环)
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
A["模型调工具"] --> B["观察(有界契约)"]
|
||
B --> C{"模型决定"}
|
||
C -->|"继续"| D["下次 Tool Call + previous_observation 评价"]
|
||
C -->|"收尾"| E["输出 Draft → Release 发布"]
|
||
D --> B
|
||
```
|
||
|
||
**progress 协议的闭环**:模型每次继续调用,都要在 Envelope 里回带对上一轮的 GAINED/NO_GAIN 评价——这就是拦截器 ③ 校验的 pending 逻辑(可回看 progress 笔记)。
|
||
|
||
---
|
||
|
||
## 6. 关键点总结
|
||
|
||
| 阶段 | 关键认知 |
|
||
|---|---|
|
||
| A 执行前 | 工具名是模型决定的;parse 是 typed 严格契约(输出必须匹配 Schema);三道门不通过不执行 |
|
||
| B 执行中 | executor/projector 是 Adapter 组装进 boundary 的**参数**;执行链货币是 JSON 文本;模型永远看不到 raw |
|
||
| C 执行后 | 拦截器不直接返回——双源校验 + progress 记账 + modelObservation 成型;ToolCallResponse 才是模型拿到的对象 |
|
||
|
||
## 7. 面试 30 秒说法
|
||
|
||
> "一次工具调用的完整旅程分三段:执行前,模型从 callbacks 看到工具并决定调用,拦截器做 supports 分流、typed 严格 parse、协议校验和判重——三道门不通过都不执行 backend;执行中,invoke 经 bridge 到 Adapter,Adapter 把 executor(跑 backend 拿 raw)和 projector(raw 投影成有界脱敏契约)组装进 ToolBoundary 的五阶段门禁,返回 ToolBoundaryResult;执行后,拦截器不直接返回——先双源校验 evidence_status,再 recordCompleted 记进度,再 modelObservation 加工成有界观察,最后包装成 ToolCallResponse 给模型。模型看到的永远是脱敏后有界的观察,raw 只进 canonical 供审计验真。"
|
||
|
||
## 8. 代码位置索引
|
||
|
||
| 环节 | 文件 |
|
||
|---|---|
|
||
| 拦截器(A/C 阶段) | `src/main/java/com/superbiz/agent/harness/agent/HarnessToolInterceptor.java` |
|
||
| 注册表 + parse + invoke | `src/main/java/com/superbiz/agent/harness/agent/HarnessEvidenceTools.java` |
|
||
| Adapter 组装(B 阶段) | `src/main/java/com/superbiz/agent/harness/tool/adapter/RagToolAdapter.java` |
|
||
| 五阶段门禁 | `src/main/java/com/superbiz/agent/harness/tool/boundary/ToolBoundary.java` |
|
||
| 双源校验 + 观察成型 | `src/main/java/com/superbiz/agent/harness/agent/ToolResultViewProjector.java` |
|
||
| 模型观察形态 | `src/main/java/com/superbiz/agent/harness/agent/ToolControlView.java` |
|