从函数到 Tool:用 Vercel AI SDK 打开工具调用的大门

如果你已经习惯在 Next.js 里写 API Route、调用第三方接口,那你现在离「做一个能自己调接口的大模型助手」只差半步:把这些函数包装成 Tool,交给模型自己决定什么时候用。
这篇文章,我们先不聊 MCP、Agent 框架这些「大词」,只做一件事:
帮你从「写一个普通函数」走到「写一个可以被大模型调用的 Tool」,顺便把背后的心智模型讲清楚。
读完之后,你应该能做到:
知道什么是 Vercel AI SDK 里的 Tool,和普通函数有什么区别。
会写一个最小可用的 Tool,并在 Next.js 里跑起来。
了解如何设计 Tool 的入参/返回值,让模型更好用你的代码。
大致知道:今天先只用 Tool 就能搞定很多应用,MCP 这种协议可以留到后面再说。
1. 为什么需要「工具」这一层?
先想象一个简单需求:你想做一个「查天气的聊天机器人」。
最 naive 的做法是:
用户输入「帮我查一下北京今天的天气」。
你在服务器上拿到这句自然语言。
自己写一段 if/else 或正则,把城市名抠出来。
调天气 API,拿到结果。
把结果塞回 prompt,让模型组织成一段话发给用户。
这个方案能用,但有明显问题:
逻辑写死在你的代码里,模型只是个「高级模板引擎」。
每加一个功能(查汇率、查日程)都要多写一段解析 + 调用逻辑。
模型完全不知道「有哪些能力可以用」,你得手把手「开道」。
Tool 这一层,就是把「你那段 if/else + 调接口」上升为一个模型可见的能力。
换个说法:
你写函数,定义好「我能做什么」「需要什么参数」「会返回什么」。
你告诉模型:这些是你可以调用的工具。
模型根据用户输入,自行决定要不要调用哪个工具,以及传什么参数。
这一步的抽象,正是 Vercel AI SDK 的 Tool 在帮你做的。
2. Vercel AI SDK 里的 Tool 是什么?
你可以先把 Tool 理解为:
一个「带说明书的函数」,说明书是给模型看的,函数体是给你自己的代码看的。
在 Vercel AI SDK 里,一个 Tool 一般包含三块信息:
description:一句话说明这个工具是干嘛的。parameters:用 JSON Schema(或者 Zod)描述需要什么入参。execute:真正执行的函数,比如调用 HTTP API、查数据库。
它长相大概是这样(伪代码,先感受定义):
import { tool } from 'ai';
const getWeather = tool({
description: '查询指定城市的实时天气',
parameters: {
type: 'object',
properties: {
city: { type: 'string', description: '城市名称,比如 Beijing' },
},
required: ['city'],
},
execute: async ({ city }) => {
// 这里就是你平时会写的业务代码
const res = await fetch(`https://api.example.com/weather?city=${city}`);
return await res.json();
},
});之后,在调用模型的时候,你把这个 getWeather 传给 AI SDK:
SDK 会负责把
description和parameters转成模型能理解的「工具列表」。模型在理解用户意图时,会思考:「要不要用 getWeather?如果用,该传什么
city?」一旦模型决定调用,SDK 会把参数喂给
execute,拿到返回结果,再继续让模型生成最终回复。
你不用再自己写「解析城市名」「决定什么时候调接口」这一坨逻辑,把「主动权」交给了模型。
3. 一个最小可用的 Tool 示例
我们来写一个真正能跑起来的最小例子:查当前时间。
3.1 定义一个简单 Tool
假设你已经在 Next.js 项目里装好了 ai 这个包(Vercel AI SDK)。在服务端我们创建一个简单 Tool:
// app/api/chat/tools.ts
import { tool } from 'ai';
export const getCurrentTime = tool({
description: '获取当前服务器时间(ISO 字符串)',
parameters: {
type: 'object',
properties: {},
},
async execute() {
return {
now: new Date().toISOString(),
};
},
});这里有几个点值得注意:
parameters是一个空对象:表示这个工具不需要入参。execute返回一个对象{ now: string },而不是直接返回字符串,这样方便后面做结构化处理(比如前端展示、后续计算)。
3.2 在 API Route 里使用 Tool
接下来,我们写一个最简单的聊天 API,允许模型在对话中调用这个 Tool。具体调用细节会随版本略有差异,这里只强调「结构」和「思路」,伪代码类似这样:
// app/api/chat/route.ts
import { NextRequest } from 'next/server';
import { streamText } from 'ai'; // 假设使用 AI SDK 的某个调用方法
import { getCurrentTime } from './tools';
import { openai } from '@ai-sdk/openai';
export async function POST(req: NextRequest) {
const { messages } = await req.json();
const response = await streamText({
model: openai('gpt-4o'), // 你选的模型
messages,
tools: {
getCurrentTime,
},
});
return response.toAIStreamResponse();
}这里有几个核心点:
tools这个字段,把你定义的 Tool 暴露给了模型。模型收到用户消息后,如果判断需要当前时间,就会「发起一次对 getCurrentTime 的调用」,SDK 会自动帮你执行
execute。Tool 的调用结果会被喂回模型,让它根据时间继续生成最终回复。
3.3 一个简单的前端聊天界面
前端可以用 useChat 这类 Hook 来做一个最简聊天 UI,这部分不是这篇文章的重点,就先不展开说了。重要的是:从前端视角看,和普通 Chat API 没什么差别,只是模型现在「会自己查时间」了。
4. Tool 和普通函数的本质区别
到这里你可能会问:那我直接在路由里写 new Date().toISOString() 不也能查时间吗?为什么要绕一圈 Tool?
关键区别在于 谁负责「决策」:
普通函数:你在业务代码里显式调用,完全由你来决定「什么时候调」。
Tool:你只是把「这个能力」挂出来,并告诉模型「如何使用」,至于「用不用」「怎么用」,交给模型决策。
这带来几个实际好处:
少写 if/else
不用你来解析自然语言、匹配关键词、选择分支。
模型根据上下文自己判断要不要用某个 Tool。
支持多个 Tool 自动编排
定义多个工具(查时间、查天气、查日程),模型可以在一次对话中按需调用多个。
随着工具增多,你不需要维护一堆「业务路由表」,模型自己「想办法」。
跨模型统一
不同模型厂商的 Tool / Function Calling 接口各不相同,而 AI SDK 帮你统一掉了这些细节。
你只需要维护一套 Tool 定义,就可以在不同模型之间切换。
你可以把 Tool 看成是:
在「模型 ↔ 你的代码」之间加了一层「带结构化元数据的函数」,让这条链路既智能,又可控。
5. 设计一个好用的 Tool:参数与返回值
写 Tool 不是「能跑就行」,参数和返回值设计好了,模型才能更稳定、可控地使用它。
5.1 参数:帮模型把事情说清楚
参数定义建议遵循几个小原则:
能枚举的就枚举:比如
unit: 'celsius' | 'fahrenheit'。能拆成字段的就拆开:不要把「城市+日期」合成一个字符串,让模型自己拆。
用
description给模型一些例子:它真的会看(或者说,训练时看过类似模式)。
示例:
const getWeather = tool({
description: '查询指定城市在指定日期的天气',
parameters: {
type: 'object',
properties: {
city: {
type: 'string',
description: '城市名称,比如 Beijing、Shanghai',
},
date: {
type: 'string',
description: '日期,格式为 YYYY-MM-DD,比如 2025-05-17',
},
unit: {
type: 'string',
enum: ['celsius', 'fahrenheit'],
description: '温度单位,默认 celsius',
},
},
required: ['city', 'date'],
},
async execute({ city, date, unit = 'celsius' }) {
// ...
},
});5.2 返回值:尽量结构化,而不是一坨字符串
虽然你可以直接在 execute 里返回一句「北京今天 25 度,晴」,但这会损失很多灵活性:
前端没法直接拿这个结果做后续展示或计算。
模型也没法方便地在后续调用中复用结构化信息。
更推荐的做法是返回一个清晰的对象:
async execute({ city, date, unit = 'celsius' }) {
const data = await fetchWeather(city, date, unit);
return {
city,
date,
unit,
temperature: data.temp,
condition: data.condition, // sunny, cloudy, etc.
};
}这样模型既可以自己组织语言回复用户,也可以在后续步骤中继续使用这些字段(比如再调用别的 Tool)。
6. 一次性接多个 Tool:让模型自己选
前面我们只用一个 getCurrentTime,现在可以再加一个 getWeather,一起丢给模型。
// app/api/chat/tools.ts
export const tools = {
getCurrentTime,
getWeather,
};在调用模型时:
const response = await streamText({
model: openai('gpt-4o'),
messages,
tools,
});此时,如果用户说:
「帮我看下明天上海的天气,如果是晴天顺便提醒一下现在的时间。」
模型有机会做这样一件事(逻辑层面):
先调用
getWeather,判断「是不是晴天」。再调用
getCurrentTime,拿到当前时间。生成一段最终回复,融合两次调用结果。
你不需要为这串逻辑写任何「业务流程图」,只要:
把工具列好,参数定义清楚。
在系统 Prompt 里大致告诉模型「遇到这种需求要善用工具」。
这就是「工具调用」的真正威力所在:你从「写流程」变成「定义能力」。
7. 这一篇的收尾:先把 Tool 玩熟,再考虑协议层
这篇我们只做了三件事:
把 Tool 从概念上拆开:它就是「带说明书的函数」,说明书是给模型看的。
写了一个最小的 Tool 示例,并展示了如何在 Next.js API Route 里用它。
讲了如何设计参数和返回值,让模型更好用你的工具。
只靠 Vercel AI SDK 的 Tool,你已经可以做出:
内部的客服助手 / 工单助手。
SaaS 产品里的智能查询 / 智能生成。
各种「帮我查」「帮我算」「帮我记」的业务助手。
而且在这个阶段,你完全可以不关心 MCP 等协议层的东西——它们主要解决的是「跨宿主、跨项目复用工具」的问题,更适合在工具数量和调用环境都变复杂之后再慢慢引入。
下一篇,我们会在「只用 Tool」的基础上,专门聊聊:当工具越来越多、调用环境越来越复杂时,单靠 Tool 会遇到什么边界和痛点,以及为什么会出现像 MCP 这样的协议。
在 Google 上继续关注
把 HeyBinyang 添加为 Google 首选来源
如果你愿意继续在 Google 里读到我的更新,可以把这个站点添加为 preferred source,之后更容易在相关内容场景里看到它。
SHARE
分享
分享这篇文章。