工具系统与 MCP:给模型装手脚
30 秒导读: 语言模型只会「输出文本」。要让它真的查数据库、调 API、读知识库,得给它「手脚」——工 具(Tool)。本章讲 VoltAgent 怎么把一个函数包成模型能调的工具、怎么组织成百上千个工具、当工具多到塞不进上下文时怎么按语义只挑相关的几个,以及怎么把外部 MCP server 的工具无缝接进来。
本章聚焦「agent 如何调用外部能力」。上一章 01-agent-runtime.md 讲了一次生成的完整生命周期;工具是这个循环里模型「往外伸手」的唯一途径。多 agent 之间互相委派用的 delegate_task 工具是另一回事,见 04-subagents-supervisor.md,本章不讲。
1. 这是什么(零基础也能懂)
一句话定义
工具 = 一个「有名字、有参数说明、有实现」的函数,包装成模型能理解、能调用的形式。
模型看到工具的 name + description + 参数 schema,就知道「有这么个能力、什么时候该用、要传什么参数」;它输出一段结构化的调用请求,运行时把请求落到真实的 execute 函数上,再把结果喂回给模型。
解决什么问题
假 设你在做一个「天气助手」agent。用户问「北京明天下雨吗?」——模型本身不知道明天的天气,它的知识停在训练截止日。你需要给它一个 getWeather(city, date) 工具:模型决定「我该查天气」,吐出 {city: "北京", date: "明天"},运行时真的去调气象 API,把 "小雨,12-18°C" 塞回对话,模型再据此回答。
没有工具,模型只能「聊天」;有了工具,它能「做事」。
它能做什么
VoltAgent 的工具系统提供这几层能力:
| 能力 | 干什么 |
|---|---|
| 定义单个工具 | createTool + Zod schema,类型安全 |
| 组织成工具包 | createToolkit 把相关工具打包,可带共享指令 |
| 统一编排 | ToolManager 管理独立工具、工具包、provider 工具,查重、拍平 |
| 内建推理工具 | 开箱即用的 think / analyze 让模型「先想再做」 |
| 语义路由 | 工具太多时,先 searchTools 按语义挑出相关的几个,再 callTool |
| 接入 MCP | 把远程 Model Context Protocol server 的工具当本地工具用 |
| RAG 检索 | 把「知识库检索」也包成一个工具 |
用起来什么样
一个最小的真实工具长这样(教学示意,createTool 的真实签名见后文):
import { createTool } from "@voltagent/core";
import { z } from "zod";
// 定义一个「查天气」工具
const getWeather = createTool({
name: "get_weather",
description: "查询某个城市某天的天气。用户问天气时调用。",
parameters: z.object({ // Zod schema = 参数的类型 + 给模型的说明
city: z.string().describe("城市名,如「北京」"),
date: z.string().describe("日期,如「明天」"),
}),
execute: async ({ city, date }) => {
// 这里才是真正干活的地方
return await weatherApi.query(city, date); // 返回给模型的结果
},
});
把 getWeather 放进 agent 的 tools: [...],模型就自动「学会」了查天气。
一句话直觉
工具就是给模型的「API 目录 + 遥控器」。 description 和 schema 是目录页(模型读它决定按哪个键),execute 是遥控器背后真正接线的电路。整章的工程含量都在一件事上:把模型说的那句「我要调 get_weather,参数是……」精确、可靠、类型安全地落到真实函数上。
2. 顶层全景(工具系统怎么转)
一张图:从「定义」到「被模型调用」
先看数据怎么流。方向从上到下,就是一个工具从「你写下」到「模型调用、拿到结果」的一生:
你写代码 Agent 装配期 一次生成中
┌───────────┐ ┌──────────────────┐ ┌──────────────────┐
│createTool │──放进──▶ │ ToolManager │──拍平─▶ │ 传给 LLM 的 │
│ (Tool) │ tools[] │ · 独立工具 │ +查重 │ tools 字典 │
├───────────┤ │ · Toolkit │ │ (name→schema) │
│createTool │──────────▶ │ · provider 工具 │ └────────┬─────────┘
│createTool │ └──────────────────┘ │ 模型决定
├───────────┤ ▼「调 X」
│createToolkit│ ┌──────────────────┐
│ (Toolkit) │──────────────────────────────────────▶ │ createToolExecute │
└───────────┘ │ · onStart 钩子 │
│ · execute() │
│ · onEnd 钩子 │
└────────┬─────────┘
▼ 结果回喂模型
部件一句话职责
| 部件 | 干什么 | 在哪个文件 |
|---|---|---|
Tool / createTool | 单个工具的定义与包装 | tool/index.ts |
Toolkit / createToolkit | 把相关工具打包 + 共享指令 | tool/toolkit.ts |
BaseToolManager | 增删查、查重、拍平的通用逻辑 | tool/manager/BaseToolManager.ts |
ToolManager | 顶层管理器,能装工具包,prepareToolsForExecution | tool/manager/ToolManager.ts |
ToolkitManager | 工具包内部的管理器(不能再套工具包) | tool/manager/ToolkitManager.ts |
| 推理工具 | 内建 think / analyze | tool/reasoning/ |
| 路由策略 | 按语义嵌入挑工具 | tool/routing/embedding.ts |
| MCP | 接入外部工具 server | mcp/ |
| Retriever | RAG 检索包成工具 | retriever/ |
主线走一遍(不进代码)
- 你用
createTool/createToolkit定义工具,放进Agent的tools数组。 - Agent 构造时把它们全塞进
ToolManager,后者按类型分桶(独立工具 / provider 工具 / 工具包)并查重。 - 每次生成前,
ToolManager.prepareToolsForExecution把所有工具拍平成一个{name → {description, schema, execute}}字典,交给底层 AI SDK,再送给模型。 - 模型输出「我要调
get_weather」,运行时找到对应工具,依次 跑onStart钩子 →execute→onEnd钩子,把结果回喂模型。
下面逐层拆。
3. 一个 Tool 到底是什么(核心机制)
它要解决的小问题
「把一个函数变成模型能安全调用的东西」需要四样:一个名字(模型按名字点)、一段描述(模型据此决定用不用)、一份参数 schema(约束模型只能传合法参数)、一个实现(真正干活)。Tool 类就是这四样的容器。
思路:Zod schema 一石二鸟
VoltAgent 用 Zod(TypeScript 的运行时 schema 校验库)描述参数。这一个 schema 同时干两件事:
- 给模型看:底层 AI SDK 把 Zod schema 转成 JSON Schema,告诉模型「参数长这样」。
- 给代码看:
z.infer<T>让execute的参数在编译期就有精确类型——你在execute里写args.city有自动补全,写错字段名直接编译报错。
真实实现
createTool 只是 new Tool(...) 的语法糖,真正的定义在 Tool 类:
tool/index.ts:311createTool—— 工厂函数,带两个重载(有无outputSchema)。tool/index.ts:198class Tool<T, O>—— 泛型T是输入 schema、O是可选输出 schema。tool/index.ts:282构造函数做三条校验:没name直接抛错;没description只告警(tool/index.ts:287);没parameters抛错。id缺省时回退成name(tool/index.ts:294)。
关键字段(ToolOptions,tool/index.ts:100):
| 字段 | 作用 |
|---|---|
name / description | 模型看到的名字与用途说明 |
parameters | Zod 输入 schema(必填) |
outputSchema | 可选输出 schema,用于校验返回值 |
execute | 实现函数;省略它 = 客户端工具 |
needsApproval | 是否需要执行前审批(可传函数动态判断) |
hooks | 工具级 onStart / onEnd 钩子 |
toModelOutput | 把输出转成多模态内容(图片等) |
providerOptions | provider 专属选项(如 Anthropic 缓存控制) |