Skip to content

第 8 章 · 工具调用

本章目标:

  • 掌握 tool 的四大要素:descriptioninputSchemaexecutestrict
  • 理解多步调用(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 参数的类型。

generateTextstreamTexttools 参数是一个对象:键是工具名,值是工具:

ts
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 模块构造:

ts
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

ts
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)。默认关闭,按工具开启:

ts
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 本身无法完全说明预期用法或存在可选值时特别有用:

ts
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 发给模型 → 模型生成工具调用 → 工具被执行;第二步工具结果发回模型 → 模型结合结果生成回答:

ts
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 属性或 streamTextonEnd 回调。steps 包含每一步的全部文本、工具调用、工具结果和性能数据:

ts
import { generateText } from 'ai';

const { steps } = await generateText({
  model,
  stopWhen: isStepCount(10),
  // ...
});

// 从所有步骤中提取全部工具调用:
const allToolCalls = steps.flatMap(step => step.toolCalls);

onStepEnd 回调在某步完成时触发(该步的所有文本增量、工具调用与工具结果都已就绪),多步时会逐步触发。回调收到从 0 开始计数的 stepNumber

tsx
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 回调在每步开始前调用,参数包括 modelstopWhenstepNumberstepsinstructionsmessagesresponseMessagesruntimeContext 等。可以用它在某一步提供不同的设置,包括修改输入消息:

tsx
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 提供了简单的压缩策略:

tsx
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',
        }),
      };
    }
  },
});

返回的消息变更会跨步骤持久化。如果希望每步都从原始输入加离散响应消息重新推导,可以在每次回调中用 initialMessagesresponseMessages 重建:

tsx
prepareStep: ({ initialMessages, responseMessages, stepNumber }) => {
  if (stepNumber > 0) {
    return {
      messages: [...initialMessages, ...responseMessages.slice(-10)],
    };
  }
},

8.6 Dynamic Tools 与初步结果

dynamicTool

当工具 schema 在编译期未知时可以使用动态工具,典型场景:无 schema 的 MCP 工具、用户在运行时定义的函数、从外部加载的工具:

ts
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 标志做类型收窄:

ts
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 函数可以在工具执行期间流式输出状态信息:

ts
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 }:必须调用指定的工具
ts
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 即全部激活:

ts
const { text } = await generateText({
  model,
  tools: myToolSet,
  activeTools: ['firstTool'],
});

8.8 工具错误处理

AI SDK 有三类工具调用相关错误:

  • NoSuchToolError:模型试图调用 tools 对象中不存在的工具
  • InvalidToolInputError:模型调用的输入不符合工具的 input schema
  • ToolCallRepairError:工具调用修复过程中发生的错误

工具执行失败(你的 execute 抛错)时,错误会作为 tool-error 内容部分加入结果,以便多步场景下自动进行 LLM 往返处理。

generateText 中处理

generateText 对 schema 校验等问题抛出异常可用 try/catch 处理;工具执行错误则出现在结果的步骤中:

ts
try {
  const result = await generateText({
    //...
  });
} catch (error) {
  if (NoSuchToolError.isInstance(error)) {
    // 处理 no such tool 错误
  } else if (InvalidToolInputError.isInstance(error)) {
    // 处理非法工具输入错误
  } else {
    // 处理其他错误
  }
}
ts
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 可组合;条件只在最后一步含工具结果时评估
  • stepsonStepEnd 提供每一步的完整中间状态;prepareStep 可按步切换模型、强制 toolChoice、限制 activeTools 或压缩上下文
  • 动态场景用 dynamicTool + toolCall.dynamic 收窄类型;generator 形式的 execute 可产出初步进度结果
  • 三类错误:NoSuchToolError / InvalidToolInputError / ToolCallRepairError;execute 内部抛错会成为 tool-error 内容参与自动往返

🛠️ 动手实践

  1. 定义三个工具(查天气、查汇率、查时间)挂到一个 generateText 上,设置 stopWhen: isStepCount(6),问「旧金山现在几点、天气如何、100 美元兑人民币多少」,打印 steps.length 与每个 step 的 toolCalls。
  2. prepareStep 实现策略:第一步强制 toolChoice: required 且只开放搜索类工具,第二步起放开全部工具;对比不设 prepareStep 时回答质量的差异。
  3. 写一个故意抛错的 execute,分别观察 generateText 的 try/catch 分支与 steps 中的 tool-error 部分;再给这个工具加上 inputExamplesstrict: true 测试行为变化。