第 8 章 · 工具调用
本章目标:
- 掌握 tool 的四大要素:
description、inputSchema、execute、strict- 理解多步调用(multi-step calls)与
stopWhen停止条件的工作机制- 学会用
prepareStep在每步动态调整模型、消息与工具- 了解 dynamic tools、tool choice、activeTools 与工具错误处理
8.1 工具的定义
如 Foundations 所述,tools 是可以被模型调用来执行特定任务的对象。函数工具(function tools)和动态工具(dynamic tools)包含几个核心元素:
description:可选的工具描述,影响模型何时选择该工具。可以是字符串,也可以是从工具上下文和实验性 sandbox 派生描述的函数inputSchema:定义输入参数的 Zod schema 或 JSON schema。schema 会被 LLM 消费,也用于校验 LLM 的工具调用execute:可选的异步函数,以工具调用的输入为参数执行并产出结果。它是可选的——你可能想把工具调用转发给客户端或队列而不是在本进程执行strict:(可选布尔值)在 provider 支持时启用严格工具调用
💡 可以用
tool辅助函数推断execute参数的类型。
generateText 和 streamText 的 tools 参数是一个对象:键是工具名,值是工具:
import { z } from 'zod';
import { generateText, tool, isStepCount } from 'ai';
const result = await generateText({
model,
tools: {
weather: tool({
description: 'Get the weather in a location',
inputSchema: z.object({
location: z.string().describe('The location to get the weather for'),
}),
execute: async ({ location }) => ({
location,
temperature: 72 + Math.floor(Math.random() * 21) - 10,
}),
}),
},
stopWhen: isStepCount(5),
prompt: 'What is the weather in San Francisco?',
});💡 模型使用工具时称为「tool call」,工具的输出称为「tool result」。工具调用不限于文本生成,还可以用来渲染用户界面(Generative UI)。
本章示例的 model 由第 3 章的 provider 模块构造:
import { createGateway } from 'ai';
export const gateway = createGateway({
apiKey: process.env.AI_GATEWAY_API_KEY ?? '',
});
// 自定义 OpenAI 兼容 Provider 等价写法:
// import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
// export const myProvider = createOpenAICompatible({ name: 'my-provider', baseURL: ..., apiKey: ... });8.2 动态描述与严格模式
Dynamic Descriptions
工具描述可以是固定字符串或函数。当发送给模型的描述应取决于当前工具上下文或实验性 sandbox 时(比如租户、项目、环境或工作区),就用函数。描述函数会在每步生成前、工具定义发给模型之前解析,接收来自 toolsContext 的匹配 context 与当前 experimental_sandbox:
import { generateText, tool } from 'ai';
import { z } from 'zod';
const shell = tool({
contextSchema: z.object({
projectName: z.string(),
}),
description: ({ context, experimental_sandbox }) =>
[
`Run shell commands for the ${context.projectName} project.`,
experimental_sandbox != null
? `Sandbox: ${experimental_sandbox.description}`
: undefined,
]
.filter(Boolean)
.join('\n'),
inputSchema: z.object({
command: z.string(),
}),
execute: async ({ command }, { experimental_sandbox }) => {
if (!experimental_sandbox) {
throw new Error('Experimental sandbox is not available');
}
return experimental_sandbox.run({ command });
},
});
const result = await generateText({
model,
tools: { shell },
toolsContext: {
shell: { projectName: 'web-app' },
},
experimental_sandbox,
prompt: 'List the project files.',
});Strict Mode
启用后,支持严格工具调用的 provider 将只生成符合你定义的 inputSchema 的合法工具调用,提升可靠性。但并非所有 schema 都被严格模式支持(取决于 provider)。默认关闭,按工具开启:
tool({
description: 'Get the weather in a location',
inputSchema: z.object({
location: z.string(),
}),
strict: true, // 为该工具启用严格校验
execute: async ({ location }) => ({
// ...
}),
});⚠️ 并非所有 provider 或模型都支持 strict mode;不支持时该选项会被忽略。
Input Examples
可以为工具指定输入示例,帮助模型理解输入数据的结构。当 JSON schema 本身无法完全说明预期用法或存在可选值时特别有用:
tool({
description: 'Get the weather in a location',
inputSchema: z.object({
location: z.string().describe('The location to get the weather for'),
}),
inputExamples: [
{ input: { location: 'San Francisco' } },
{ input: { location: 'London' } },
],
execute: async ({ location }) => {
// ...
},
});💡 目前仅 Anthropic provider 原生支持工具输入示例,其他 provider 会忽略该设置。
8.3 多步调用(stopWhen)
默认情况下 generateText/streamText 只触发一次生成。但提供工具后,模型可以选择生成普通文本回复或者生成工具调用——如果它生成了工具调用,这一步就结束了。
你往往希望模型在工具执行后再生成文本(比如结合用户问题总结工具结果),甚至在一个响应里连续使用多个工具。这就是多步调用的价值:可以把它想象成与真人对话——对方没有相关知识时,会先查资料(用工具)再回答你;每次生成(工具调用或文本)都是一步。
通过 stopWhen 设置即可启用多步调用。设置后只要模型生成了工具调用且未满足停止条件,SDK 就会把工具结果传回并触发新一轮生成。内置停止条件:
isStepCount(count)— 达到指定步数后停止(默认isStepCount(20))hasToolCall(...toolNames)— 调用了任一指定工具时停止isLoopFinished()— 永不触发,让循环自然运行到结束
多个条件可以组合成数组或自定义条件(详见第 19 章循环控制)。
⚠️ 只有当最后一步包含工具结果时才会评估
stopWhen条件。
下面的例子有两个步骤:第一步 prompt 发给模型 → 模型生成工具调用 → 工具被执行;第二步工具结果发回模型 → 模型结合结果生成回答:
import { z } from 'zod';
import { generateText, tool, isStepCount } from 'ai';
const { text, steps } = await generateText({
model,
tools: {
weather: tool({
description: 'Get the weather in a location',
inputSchema: z.object({
location: z.string().describe('The location to get the weather for'),
}),
execute: async ({ location }) => ({
location,
temperature: 72 + Math.floor(Math.random() * 21) - 10,
}),
}),
},
stopWhen: isStepCount(5), // 若调用了工具,最多 5 步后停止
prompt: 'What is the weather in San Francisco?',
});streamText 同样可以这样用。
8.4 steps 与 onStepEnd
要访问中间过程的工具调用与结果,可使用结果对象的 steps 属性或 streamText 的 onEnd 回调。steps 包含每一步的全部文本、工具调用、工具结果和性能数据:
import { generateText } from 'ai';
const { steps } = await generateText({
model,
stopWhen: isStepCount(10),
// ...
});
// 从所有步骤中提取全部工具调用:
const allToolCalls = steps.flatMap(step => step.toolCalls);onStepEnd 回调在某步完成时触发(该步的所有文本增量、工具调用与工具结果都已就绪),多步时会逐步触发。回调收到从 0 开始计数的 stepNumber:
import { generateText } from 'ai';
const result = await generateText({
// ...
onStepEnd({
stepNumber,
text,
toolCalls,
toolResults,
finishReason,
usage,
performance,
}) {
console.log(`Step ${stepNumber} finished (${finishReason})`, {
usage,
performance,
});
// 自定义逻辑,例如保存聊天历史或记录用量
},
});也可以用 onToolExecutionStart / onToolExecutionEnd 观察每次工具执行的时机、耗时、输入输出与错误(见第 6 章)。
8.5 prepareStep:每步定制
prepareStep 回调在每步开始前调用,参数包括 model、stopWhen、stepNumber、steps、instructions、messages、responseMessages、runtimeContext 等。可以用它在某一步提供不同的设置,包括修改输入消息:
import { generateText } from 'ai';
const result = await generateText({
// ...
prepareStep: async ({ model, stepNumber, steps, messages }) => {
if (stepNumber === 0) {
return {
// 这一步换一个模型:
model: modelForThisParticularStep,
// 强制这一步进行某个工具调用:
toolChoice: { type: 'tool', toolName: 'tool1' },
// 限制这一步可用的工具:
activeTools: ['tool1'],
};
}
// 不返回任何内容时,使用默认设置
},
});如果返回了 instructions,它们会延续到后续步骤,直到某次 prepareStep 返回新的覆盖值;需要还原顶层指令时用 initialInstructions。
长循环中的消息修改(上下文压缩)
在较长的 agent 循环中,可以通过 messages 参数改写后续步骤将使用的消息状态,这对上下文压缩尤其有用。默认 messages 是 initialMessages 加上累积的 responseMessages;若之前的 prepareStep 返回过 messages,则后续步骤基于那份持久化消息再加最近一步的 response messages。内置的 pruneMessages helper 提供了简单的压缩策略:
import { generateText, pruneMessages, type ModelMessage } from 'ai';
const COMPACTION_THRESHOLD = 100_000;
const estimateTokens = (messages: ModelMessage[]) => {
return JSON.stringify(messages).length / 4;
};
const result = await generateText({
// ...
prepareStep: ({ messages }) => {
if (estimateTokens(messages) > COMPACTION_THRESHOLD) {
return {
messages: pruneMessages({
messages,
reasoning: 'all',
toolCalls: 'before-last-3-messages',
emptyMessages: 'remove',
}),
};
}
},
});返回的消息变更会跨步骤持久化。如果希望每步都从原始输入加离散响应消息重新推导,可以在每次回调中用 initialMessages 和 responseMessages 重建:
prepareStep: ({ initialMessages, responseMessages, stepNumber }) => {
if (stepNumber > 0) {
return {
messages: [...initialMessages, ...responseMessages.slice(-10)],
};
}
},8.6 Dynamic Tools 与初步结果
dynamicTool
当工具 schema 在编译期未知时可以使用动态工具,典型场景:无 schema 的 MCP 工具、用户在运行时定义的函数、从外部加载的工具:
import { dynamicTool } from 'ai';
import { z } from 'zod';
const customTool = dynamicTool({
description: 'Execute a custom function',
inputSchema: z.object({}),
execute: async input => {
// input 类型为 'unknown'
// 需要在运行时校验/断言
const { action, parameters } = input as any;
// 执行动态逻辑
return { result: `Executed ${action}` };
},
});同时使用静态与动态工具时,用 dynamic 标志做类型收窄:
const result = await generateText({
model,
tools: {
// 已知类型的静态工具
weather: weatherTool,
// 动态工具
custom: dynamicTool({
/* ... */
}),
},
onStepEnd: ({ toolCalls, toolResults }) => {
// 类型安全的迭代
for (const toolCall of toolCalls) {
if (toolCall.dynamic) {
// 动态工具:input 是 'unknown'
console.log('Dynamic:', toolCall.toolName, toolCall.input);
continue;
}
// 静态工具:完整类型推断
switch (toolCall.toolName) {
case 'weather':
console.log(toolCall.input.location); // 类型为 string
break;
}
}
},
});Preliminary Tool Results(初步工具结果)
工具的 execute 可以返回一个 AsyncIterable 输出多个结果,最后一个值作为最终工具结果。配合 generator 函数可以在工具执行期间流式输出状态信息:
tool({
description: 'Get the current weather.',
inputSchema: z.object({
location: z.string(),
}),
async *execute({ location }) {
yield {
status: 'loading' as const,
text: `Getting weather for ${location}`,
weather: undefined,
};
await new Promise(resolve => setTimeout(resolve, 3000));
const temperature = 72 + Math.floor(Math.random() * 21) - 10;
yield {
status: 'success' as const,
text: `The weather in ${location} is ${temperature}°F`,
temperature,
};
},
});8.7 toolChoice 与 activeTools
Tool Choice
用 toolChoice 设置影响模型何时选择工具:
auto(默认):模型自行决定是否以及调用哪个工具required:模型必须调用一个工具,可自选哪个none:禁止调用工具{ type: 'tool', toolName: string }:必须调用指定的工具
import { z } from 'zod';
import { generateText, tool } from 'ai';
const result = await generateText({
model,
tools: {
weather: tool({
description: 'Get the weather in a location',
inputSchema: z.object({
location: z.string().describe('The location to get the weather for'),
}),
execute: async ({ location }) => ({
location,
temperature: 72 + Math.floor(Math.random() * 21) - 10,
}),
}),
},
toolChoice: 'required', // 强制模型调用一个工具
prompt: 'What is the weather in San Francisco?',
});Active Tools
语言模型同一时间只能处理有限数量的工具。为了既静态类型化大量工具又限制模型可见的工具集,AI SDK 提供 activeTools 属性——当前激活的工具名数组,默认 undefined 即全部激活:
const { text } = await generateText({
model,
tools: myToolSet,
activeTools: ['firstTool'],
});8.8 工具错误处理
AI SDK 有三类工具调用相关错误:
NoSuchToolError:模型试图调用 tools 对象中不存在的工具InvalidToolInputError:模型调用的输入不符合工具的 input schemaToolCallRepairError:工具调用修复过程中发生的错误
工具执行失败(你的 execute 抛错)时,错误会作为 tool-error 内容部分加入结果,以便多步场景下自动进行 LLM 往返处理。
generateText 中处理
generateText 对 schema 校验等问题抛出异常可用 try/catch 处理;工具执行错误则出现在结果的步骤中:
try {
const result = await generateText({
//...
});
} catch (error) {
if (NoSuchToolError.isInstance(error)) {
// 处理 no such tool 错误
} else if (InvalidToolInputError.isInstance(error)) {
// 处理非法工具输入错误
} else {
// 处理其他错误
}
}const { steps } = await generateText({
// ...
});
// 在步骤中检查工具错误
const toolErrors = steps.flatMap(step =>
step.content.filter(part => part.type === 'tool-error'),
);
toolErrors.forEach(toolError => {
console.log('Tool error:', toolError.error);
console.log('Tool name:', toolError.toolName);
console.log('Tool input:', toolError.input);
});streamText 中处理
streamText 把错误作为 stream 结果的一部分发送:工具执行错误表现为 tool-error 部分,其他错误表现为 error 部分。此外 SDK 还提供工具调用修复机制(tool call repair),例如用结构化输出模型重试解析,或用 re-ask 策略让模型重新给出调用参数。
本章小结
- 工具四要素:description(可为动态函数)、inputSchema(Zod/JSON Schema)、execute(可省略以转发到客户端)、strict(provider 支持时的严格模式)
- 多步调用由
stopWhen驱动:isStepCount/hasToolCall/isLoopFinished可组合;条件只在最后一步含工具结果时评估 steps与onStepEnd提供每一步的完整中间状态;prepareStep可按步切换模型、强制 toolChoice、限制 activeTools 或压缩上下文- 动态场景用
dynamicTool+toolCall.dynamic收窄类型;generator 形式的 execute 可产出初步进度结果 - 三类错误:NoSuchToolError / InvalidToolInputError / ToolCallRepairError;execute 内部抛错会成为 tool-error 内容参与自动往返
🛠️ 动手实践
- 定义三个工具(查天气、查汇率、查时间)挂到一个
generateText上,设置stopWhen: isStepCount(6),问「旧金山现在几点、天气如何、100 美元兑人民币多少」,打印steps.length与每个 step 的 toolCalls。 - 用
prepareStep实现策略:第一步强制toolChoice: required且只开放搜索类工具,第二步起放开全部工具;对比不设 prepareStep 时回答质量的差异。 - 写一个故意抛错的
execute,分别观察generateText的 try/catch 分支与 steps 中的tool-error部分;再给这个工具加上inputExamples与strict: true测试行为变化。