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:21)。
repair 机制的思路: 模型偶尔会给出"差一点点"的工具调用(比如少个引号、字段名写错)。与其直接报错中断,不如把"坏的调用 + 错误信息"再丢给一个修复函数(常见做法:再调一次模型让它改)。你通过 experimental_repairToolCall 传入这个函数,它在 generateText 入口被接住(generate-text.ts:250)并下传给 parseToolCall。
还有个相关旋钮 experimental_refineToolInput(generate-text.ts:251):在执行前"精修"已解析的输入(形状不变),用于在工具执行/回调/遥测之前统一修正输入。
3.5 tool approval:危险操作要人点同意
有些工具(删文件、转账、发邮件)不能让模型自己想调就调。AI SDK v7 把审批做成循环级机制(注意:工具上的 needsApproval 已 deprecated,tool.ts:105,改在 generateText/streamText 层用 toolApproval 配置)。
流程(读 generate-text.ts:1061-1137 那段):
模型要调工具X
│
▼
resolveToolApproval(X) —— 这工具要不要审批?
│
├─ 'not-applicable' → 直接执行(不消耗 approvalId)
├─ 'approved' → 自动批准(记一条 approval-response)
├─ 'denied' → 自动拒绝,X 被加入 blockedToolCallIds,不执行
└─ 'user-approval' → 生成 approvalId,X 进 blocked,产出一条
tool-approval-request,等人类回复
几个细节:
- 被 block 的工具不执行。
executeTools的输入会filter(... && !blockedToolCallIds.has(id))(generate-text.ts:1171-1173)。 - 审批请求可被签名防伪。 传
experimental_toolApprovalSecret后,每个审批请求用 HMAC 签名(maybeSignApproval,generate-text.ts:1078),回放时验签,防止客户端伪造"已批准"(generate-text.ts:363)。这是个值得借鉴的安全设计:人审批的"同意"凭证必须不可伪造。 - 审批是跨请求的。 人类的批准/拒绝下次随消息带回,入口处
collectToolApprovals+validateApprovedToolApprovals先把上一轮待审批的执行掉(generate-text.ts:652-759)。
3.6 巧妙之处
tool()是零运行时的类型助手 —— DX 全靠重载,实现是恒等函数(tool.ts:368)。- 四象限分类直接驱动循环行为 ——
providerExecuted一个布尔决定 SDK 执不执行(generate-text.ts:1160),分类不是文档摆设,是控制流开关。 - 审批凭证可 HMAC 签名 —— 把"人类同意"当成需要防篡改的安全凭证,而非简单布尔(
tool-approval-signature.ts)。 - repair 把"模型犯错"当可恢复事件 —— 解析失败不直接崩,给一次修复机会(
parse-tool-call.ts)。
3.7 边界与局限
- 工具级
needsApproval已废弃,审批统一上移到调用层toolApproval(tool.ts:105)。混用老代码要注意。 - 无
execute的工具不被自动执行——这是特性不是 bug(用于前端/人类侧执行),但容易让人误以为"工具没跑"。 - provider-executed 工具的结果完全由 provider 控制,SDK 无法在中途干预其执行。
3.8 代码地图
| 主题 | 文件 | 符号 |
|---|---|---|
| 工具类型与助手 | packages/provider-utils/src/types/tool.ts | Tool, tool, FunctionTool, ProviderExecutedTool |
| 工具执行选项 | packages/provider-utils/src/types/tool-execute-function.ts | ToolExecuteFunction, ToolExecutionOptions |
| 解析+修复 | packages/ai/src/generate-text/parse-tool-call.ts | parseToolCall, repairToolCall |
| 单工具执行 | packages/ai/src/generate-text/execute-tool-call.ts | executeToolCall |
| 审批解析 | packages/ai/src/generate-text/resolve-tool-approval.ts | resolveToolApproval |
| 审批签名 | packages/ai/src/generate-text/tool-approval-signature.ts | maybeSignApproval |
| 收集历史审批 | packages/ai/src/generate-text/collect-tool-approvals.ts | collectToolApprovals |
下一章:04 · 流式与 UI。