数据截至 (上游 commit 2174f202c21f)
03 · 工具系统
本章讲什么: 一个"工具"到底是什么数据结构、为什么分四种、
tool()助手做了什么、模型给的工具输入怎么被校验和修复、以及"危险操作要人点同意"的 approval 机制。
3.1 工具是什么
一个工具 = 告诉模型"你有这个能力"的元数据 + 一段真正干活的代码。最小形态就三样:
import { tool } from 'ai';
import { z } from 'zod';
const getWeather = tool({
description: '查询某城市天气', // 告诉模型这工具干嘛
inputSchema: z.object({ city: z.string() }), // 模型该给什么参数
execute: async ({ city }) => { // SDK 真正执行的代码
return { tempC: 21, city };
},
});
inputSchema既用来"告诉模型该生成什么形状的参数",也用来"校验模型真给了对的形状"(tool.ts:86-93)。execute可选。不给execute的工具,SDK 不会自动执行——它会把工具调用交还给你(常见于前端工具:模型说要调,实际由浏览器侧处理)。
3.2 四种工具(按"谁定义 schema / 谁执行"分)
这是工具系统最该先建立的心智模型。两条正交的轴:schema 是用户定义还是 provider 定义?执行在客户端还是 provider 端? 交叉出四类:
| 类型 | schema 谁定义 | 谁执行 | 典型例子 |
|---|---|---|---|
FunctionTool | 用户 | 客户端(SDK 调你的 execute) | 自定义 getWeather |
DynamicTool | 用户(运行时才知道类型) | 客户端 | MCP 动态工具 |
ProviderDefinedTool | provider | 客户端 | local shell:schema 由 provider 定,但命令在你本地跑 |
ProviderExecutedTool | provider | provider 端 | 内置 web search / code execution |
对应类型定义在 packages/provider-utils/src/types/tool.ts:FunctionTool(:221)、DynamicTool(:235)、ProviderDefinedTool(:275)、ProviderExecutedTool(:295)。
为什么这个分类重要? 因为它决定了 02 那个循环要不要执行某个工具:循环只执行 !providerExecuted 的工具(generate-text.ts:1160)。provider-executed 的工具(isProviderExecuted: true)由模型那边执行,SDK 只是把结果收下、拼进消息。
ProviderExecutedTool 还有个 supportsDeferredResults 标记(tool.ts:318),处理"结果不在同一轮返回"的场景(对应 02 里的 pendingDeferredToolCalls)。
3.3 tool() 助手:为什么它"几乎什么都不做"
你可能以为 tool() 做了很多。实际上它的运行时实现是:
// packages/provider-utils/src/types/tool.ts —— 多个重载,最后实现
export function tool(tool: any): any {
return tool; // 原样返回!
}
见 tool(packages/provider-utils/src/types/tool.ts:368)。运行时它就是恒等函数。它存在的全部价值在类型层:那一堆函数重载(tool.ts:351-365)让 TypeScript 能从 inputSchema 推断出 execute 参数的类型、从 execute 返回值推断输出类型。
精华: "开发体验"在这里是纯编译期产物。tool() 不增加任何运行时开销,只是给你的工具对象"贴上正确的类型"。
3.4 模型给的工具调用怎么被校验/修复
模型输出的工具调用是不可信的:工具名可能不存在、参数可能不符合 schema、JSON 可能拼错。parseToolCall 是这道关卡:
// packages/ai/src/generate-text/parse-tool-call.ts —— 关键路径
// · 工具名不在 tools 里 → 抛 NoSuchToolError
// · 输入不符合 inputSchema → 抛 InvalidToolInputError
// · 有 repairToolCall → 给模型/自定义逻辑一次"修"的机会
相关符号:NoSuchToolError(parse-tool-call.ts:11)、InvalidToolInputError(parse-tool-call.ts:10)、repairToolCall(parse-tool-call.ts:22)。
repair 机制的思路: 模型偶尔会给出"差一点点"的工具调用(比如少个引号、字段名写错)。与其直接报错中断,不如把"坏的调用 + 错误信息"再丢给一个修复函数(常见做法:再调一次模型让它改)。你通过 experimental_repairToolCall 传入这个函数,它在 generateText 入口被接住(generate-text.ts:250)并下传给 parseToolCall。
还有个相关旋钮 experimental_refineToolInput(generate-text.ts:251):在执行前"精修"已解析的输入(形状不变),用于在工具执行/回调/遥测之前统一修正输入。