首页
首页/s02
s0280 行代码

Tool Dispatch

Register Once, Use Everywhere

核心洞察:

The loop stays stable while capabilities register into a dispatch table.

调度表将工具名称映射到处理函数。与不断膨胀的 if-else 链不同,循环只需一次 map 查找即可。添加新工具仅需一行代码:在 map 中注册名称及其处理函数。循环代码本身永远无需修改。

架构流程图

问题:如何在不修改循环的情况下添加新工具?

如果将工具执行逻辑内联在循环中,每个新工具都需要修改循环体:添加 elif 分支、新的 import 以及特殊处理。随着时间推移,循环会积累大量难以维护的分支,导致代码难以理解且修改脆弱。解决方案是采用调度表:一个将工具名称映射到处理函数的字典。循环调用 execute_tool(name, input),完全不需要了解具体哪个工具在运行。添加新工具只需一行注册代码。

开闭原则的实践

调度表体现了开闭原则:循环对修改关闭,但对扩展开放。循环本身从不改变,新能力以 TOOL_MAP 中新条目的形式加入。这一模式也简化了测试:每个处理函数都是独立可测试的函数,而调度逻辑本身仅是一个简单的字典查找,易于验证。处理函数的签名是标准化的,接受输入字典并返回输出字符串,因此任何符合此形式的函数都可以作为工具使用。

真实实现:双层注册机制

OpenCode 实际的工具系统采用双层注册模型,而非单一扁平映射。应用级工具(面向用户、进程全局)通过 ApplicationTools.Service 并使用 State.Transformable 进行注册。每位置工具(目录级作用域)单独注册,同名时覆盖应用工具。所有工具都是规范的,通过 Tool.make() 创建,返回一个带有输入/输出模式和执行器的不透明 Definition。Registry.materialize() 步骤派生工具定义,应用权限过滤器,并生成循环调用的 settle 函数。这一设计意味着相同的工具抽象适用于内置工具、MCP 服务器和插件,外部工具无需额外的路径。

设计决策

调度表使用 `dict.get(name)` 并回退到 `None`,而非使用会引发 KeyError 的 `dict[name]`。这意味着未知的工具名称会产生友好的错误消息,而不是让 agent 崩溃。

处理函数接收 `**input`(解包后的关键字参数),这意味着每个工具定义自己的参数模式。调度表不需要了解或验证参数,那是处理函数的职责。

对比 Claude Code

两个系统都使用调度表和注册表模式。Claude Code 提供内置工具集,并通过 MCP 服务器实现可扩展性。OpenCode 的 TOOL_MAP 在概念上类似,但设计为头等可扩展性:工具注册表是一个正式的数据结构(包含 ToolID、schema 和 handler),而非隐式的 if-else 链。新工具通过元数据注册自身,从而支持通配符权限规则和类型化参数验证等功能。

深入设计决策

调度表优于 If-Else 链

调度表将工具路由变成一次 map 查找,而不是不断增长的 if-else 链。新增工具只需注册到 map 中——循环代码永远不需要修改。

备选方案: If-else 链对 2-3 个工具有效,但无法扩展。调度表模式让循环保持稳定,无论添加多少工具。

统一处理函数签名

每个工具处理函数遵循相同模式:解析输入、执行、返回结果。这种统一性让调度循环可以一视同仁地处理所有工具。

备选方案: 工具可以返回不同的格式,但这会迫使循环分别处理每个工具的输出。统一性值得付出少量的抽象成本。

Learn OpenCode — Built with Next.js