# 工具注册与分发 · 从 model 请求到工具执行

> 一堆异质的工具怎么装进一个集合、按什么顺序摆给模型、一次工具调用又怎么从名字走到执行。这一章拆 ToolBridge 与 FinalizedToolset：注册期的类型擦除、finalize 的定序、以及一次 call 的完整数据流。

- 作者：David（道雾轩）
- 专栏：Grok Build 源码解读（https://daiw.org/manual/grok-build.md）
- 最后更新：2026-09-15
- 原文：https://daiw.org/manual/grok-build/tool-registry
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

# 工具注册与分发 · 从 model 请求到工具执行

上一章看了「一个工具是什么」。这一章看「一堆工具怎么组织、怎么被调到」。

## ToolBridge:agent 和工具之间那道缝

[主循环](https://daiw.org/manual/grok-build/agent-loop)不直接碰工具，只通过 **`ToolBridge`**(`bridge.rs`)。它就是「agent ↔ 工具」的接缝，对上暴露：

- `tool_definitions()`——给模型的工具清单(manifest);
- `call(name, params, id)`——派发一次工具调用；
- `register_mcp_tools` / `unregister_*`——运行时增删 [MCP](https://daiw.org/manual/grok-build/mcp-client) 工具；
- `render_prompt(...)`——用 `TemplateRenderer` 渲染 prompt。

`ToolBridge` 内部其实很瘦：一个 `Arc<FinalizedToolset>` + 一个**摆在锁外**的终端后端句柄(这样取消 `kill_foreground_commands` 能在 `call()` 持锁时照样工作)。

## builder → finalize → 不可变集合

工具集的生命周期是「可变构建 → 定格」两段：

- **`ToolRegistryBuilder`**：一个**无序** `HashMap<String, ToolEntry>`。注册 `register_with_params::<T, P>()` 时，把具体类型 `T`/`P` 擦除进一组 boxed 闭包(`output_converter`、`validate_params`、`parse_input`……)——这就是异质工具能装进同一个容器的关键 Rust 技巧：**用闭包把泛型擦成同一类型**。
- **`finalize(config, ctx)`**：按 `ToolServerConfig.tools` 的**顺序**逐个定格成 `FinalizedTool`，收进不可变的 **`FinalizedToolset`**。

<Callout type="info">
  一个容易忽略但很重要的点：**模型看到的工具顺序 = `ToolServerConfig.tools` 这个 Vec 的顺序**。builder 的 HashMap 是无序的，顺序完全由客户端配置的 Vec 提供。工具排序会影响模型的选择倾向，所以这不是无所谓的细节。
</Callout>

## SessionContext → Resources：依赖注入

`finalize` 时传入的 `SessionContext` 装着这个会话的一切具体依赖：终端后端、文件系统、cwd、[记忆后端](https://daiw.org/manual/grok-build/memory)、各功能配置……它**不被存下来**，而是被转成一个 `Resources`——一个 `HashMap<TypeId, Box<dyn Any>>` 的类型化容器(依赖注入 + 运行时状态都放这)。

`Resources` 里用两个 newtype 区分：`Params<T>`(配置)与 `State<T>`(运行时状态，如 TodoState)——同一个 `T` 靠不同 `TypeId` 并存，只有它们会被序列化进 `resources_state.json` 持久化。

## 一次 call 的完整数据流

这是本章的核心。`ToolBridge::call` 最终走这条链(全在 `registry/types.rs`):

```mermaid
flowchart TB
  C["call(name, params, id)"] --> L["tools.read() 找 FinalizedTool<br/>克隆出转换器/映射 → 立刻放锁"]
  L --> R["参数改名:client → canonical"]
  R --> CTX["建 ToolCallContext<br/>注入 Resources/renderer/dispatch"]
  CTX --> H["local_registry.find → ToolHandle"]
  H --> EX["handle.execute → ToolStream"]
  EX --> V["ToolDyn 内:结构校验 args → 执行 → 输出转 TypedToolOutput"]
  V --> F["finalize_output:<br/>转 ToolOutput + 跑 Reminder + 追加 system-reminder + 持久化"]
  F --> OUT["ToolRunResult{ output, prompt_text }"]
```

几个要点：

- **锁只持微秒**：在 `prepare_dispatch` 里就把要用的东西(转换器、反向参数映射)从读锁下**克隆出来**，`.await` 之前放锁——绝不跨 await 持锁。
- **一份结果两个去向**：`ToolRunResult`(可转成 `ToolBridgeResult`)把干净的结构化 `output`(给 JSON/ACP/遥测)和追加了提醒的 `prompt_text`(给模型)分开。
- **提醒是横切的**：`finalize_output` 每次调用后跑所有 `Reminder`(LSP 诊断、技能发现、任务完成)，把结果包成 `<system-reminder>` 追加进 prompt_text(客户端声明不投递提醒时，这一步整个跳过)——这就是[主循环](https://daiw.org/manual/grok-build/agent-loop)里那些提醒的来源。
- **持久化**：每次调用后把注册的 `Params`/`State` 防抖写盘，所以 TodoState 这类东西能跨重启恢复。

## 和《从 LLM 到 Coding Agent》对照

对照 [tool-use](https://daiw.org/manual/llm-to-agent/tool-use)：最小实现里，工具派发常是 `tools[name](args)` 一句话。grok 的增量全在「真实产品的工具集是动态、异质、有状态、要排序、要持久化、还要横切提醒」这些事上。下一章看一个更深的问题：这套派发怎么做到「同一份契约，工具在同进程或跨进程都能跑」。

---

上一篇：[一个工具的完整契约](https://daiw.org/manual/grok-build/tool-contract) · 下一篇：[工具协议 · 进程内与跨进程](https://daiw.org/manual/grok-build/tool-protocol)
