Tool Use
循环保持稳定,能力注册进一张分发表。
一个工具一个函数,注册表统一分发。
问题
s01 里模型只能跑 bash。于是出现了这些丑陋的对话:
- 读文件:
cat src/main.ts—— 长文件会撑爆上下文; - 写文件:
echo '...' > file—— 引号转义噩梦; - 找文件:
find . -name "*.ts"—— 慢且啰嗦。
更糟的是,工具逻辑和循环逻辑混在一起:每加一个工具,都要改 execute 分支。循环应该保持稳定,能力增量式地注册进来。
解决方案
把工具定义成"名字 + schema + 执行函数"的三元组,注册进一张表;循环只认这张表,不再关心具体工具是什么。
const registry = new Map();
const define = (name, schema, run) => registry.set(name, { schema, run });
const execute = (name, args) => registry.get(name)?.run(args) ?? `unknown tool: ${name}`;
循环代码与 s01 完全一致——它只把 registry 展开成 OpenAI 的 tools 数组,并按名字分发调用。新增能力 = 新增一行 define(...)。
工作原理
第 1 步:定义 5 个常用工具(文件、目录、搜索 + bash 兜底)。
define("read_file", { type: "object", properties: { path: { type: "string" } }, required: ["path"] },
({ path }) => readFile(path));
define("write_file", { type: "object", properties: { path: { type: "string" }, content: { type: "string" } }, required: ["path", "content"] },
({ path, content }) => writeFile(path, content));
define("list_dir", { type: "object", properties: { path: { type: "string" } }, required: [] },
({ path = "." }) => listDir(path));
define("grep", { type: "object", properties: { pattern: { type: "string" }, path: { type: "string" } }, required: ["pattern"] },
({ pattern, path = "." }) => grep(pattern, path));
define("bash", { type: "object", properties: { command: { type: "string" } }, required: ["command"] },
({ command }) => runBash(command));
第 2 步:把注册表展开为模型可见的 tools 数组。
const TOOLS = [...registry.entries()].map(([name, { schema }]) => ({
type: "function",
function: { name, description: describe(name), parameters: schema },
}));
第 3 步:循环执行调用时按名字查表。分发逻辑永远不变。
const output = execute(call.function.name, JSON.parse(call.function.arguments));
注意 s01 的"模型一次调用多个工具"在这里自然展开:循环逐个执行,每个结果作为一个 tool 消息回传,模型在下一轮看到全部结果。工具是纯函数式的——对模型而言,它只关心输入 JSON 和输出字符串。
试一下
运行 s02,试试这些 prompt:
- "读取 package.json 并告诉我 scripts 有哪些"(read_file,而不是 cat)
- "把 s02_tool_use.mjs 里所有 TODO 找出来"(grep)
- "列出 src 目录下所有文件"(list_dir)
观察重点:模型在多个工具之间切换是否自然?它会不会把多个工具调用塞进一轮?如果你的 execute 需要并发执行多个工具,会踩到什么坑(共享状态、顺序依赖)?
接下来
工具分发很优雅,但有个现实问题:模型拿到 bash 工具就能删库。s03 Permission → 危险动作在 shell 执行前需要一道 Harness 的决策关卡。
以下内容基于 packages/core/tools 的核查。DSH 的工具系统不是一个 Map,而是一条带守卫的执行管道:
tool/call* → tools/pre-execute(可扩展的允许/拒绝闸门)
→ 单调守卫
→ tools/execute(环绕分发:超时/重试/指标插件挂这里)
→ tools/post-execute(检查/替换结果、附加上下文)
→ finalizeContent(定义所属的收尾)
→ tools/result(只读通知)
一、注册与作用域
ctx.tools.register(definition)注册工具,返回注销函数——注册是可逆的 effect,插件卸载时自动撤销。- 注册有作用域:普通插件上下文注册全局工具;
agent.ctx注册的只属于那个 Agent,同名时遮蔽全局工具。这正是 s18 预设组合的底座。
二、模型如何看到工具
tools.mode配置选择呈现方式:native(原生函数调用,默认)、code(Code Mode:保留的run_code传输通道 + 工具 SDK)、both。- 工具 schema 在每次 step 组装时汇入系统提示词(s10 会讲组装机制)。
三、你的 Map 和它的注册表差在哪
| 你的最小实现 | DSH 的真实实现 |
|---|---|
Map<string, {schema, run}> |
带作用域的注册表 + ToolDefinition(含强制 output 声明与 UI 呈现意图) |
直接 run(args) |
五段守卫管道,超时/重试/审计/审批都是管道上的插件 |
| 工具数量一多就靠描述文本 | 工具顺序(toolOrder)显式配置,防止注册顺序污染模型所见 |
一句话:你的分发表是 DSH 工具管道的最小核。管道本身不新增工具——它只是让"策略"可以环绕工具,而不必写进工具。