跳到主要内容

工具系统与 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顶层管理器,能装工具包,prepareToolsForExecutiontool/manager/ToolManager.ts
ToolkitManager工具包内部的管理器(不能再套工具包)tool/manager/ToolkitManager.ts
推理工具内建 think / analyzetool/reasoning/
路由策略按语义嵌入挑工具tool/routing/embedding.ts
MCP接入外部工具 servermcp/
RetrieverRAG 检索包成工具retriever/

主线走一遍(不进代码)

  1. 你用 createTool / createToolkit 定义工具,放进 Agenttools 数组。
  2. Agent 构造时把它们全塞进 ToolManager,后者按类型分桶(独立工具 / provider 工具 / 工具包)并查重
  3. 每次生成前,ToolManager.prepareToolsForExecution 把所有工具拍平成一个 {name → {description, schema, execute}} 字典,交给底层 AI SDK,再送给模型。
  4. 模型输出「我要调 get_weather」,运行时找到对应工具,依次跑 onStart 钩子 → executeonEnd 钩子,把结果回喂模型。

下面逐层拆。


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:311 createTool —— 工厂函数,带两个重载(有无 outputSchema)。
  • tool/index.ts:198 class 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模型看到的名字与用途说明
parametersZod 输入 schema(必填)
outputSchema可选输出 schema,用于校验返回值
execute实现函数;省略它 = 客户端工具
needsApproval是否需要执行前审批(可传函数动态判断)
hooks工具级 onStart / onEnd 钩子
toModelOutput把输出转成多模态内容(图片等)
providerOptionsprovider 专属选项(如 Anthropic 缓存控制)

关键细节:客户端工具 = 没有 execute

tool/index.ts:275isClientSide() 只判断一件事:有没有 execute 函数。没有就是客户端工具——服务端不执行,而是把调用请求透传给前端,由浏览器/客户端去跑(比如「打开一个文件选择框」这种只能在前端做的动作)。这个判断在 prepareToolsForExecution 里用到(见 §5)。

关键细节:输出可以是流

execute 的返回类型是 ToolExecutionResult<T>(tool/index.ts:12):

export type ToolExecutionResult<T> = PromiseLike<T> | AsyncIterable<T> | T;

即工具可以同步返回、返回 Promise、或返回一个 async 迭代器(流式,最后一个值为最终结果)。

关键细节:多模态输出 toModelOutput

普通工具返回文本或 JSON。但如果工具要给模型返回图片(比如截图工具),就用 toModelOutput(tool/index.ts:174)把输出转成 ToolResultOutput。这个类型(tool/index.ts:48)覆盖了 AI SDK 的多模态返回格式:

type含义
text / json普通文本 / 结构化结果
error-text / error-json错误结果
content混合数组:文本 + media(base64 图片/媒体)

只有 Anthropic、OpenAI 等支持多模态的 provider 会用到它。


4. 工具的生命周期钩子(onStart / onEnd)

它要解决的小问题

有时你想在工具执行前后插一脚:记日志、改参数、篡改输出、统计耗时。VoltAgent 给每个工具一对可选钩子。

三个关键类型

  • tool/index.ts:39 ToolHooks = { onStart?, onEnd? }
  • tool/index.ts:14 ToolHookOnStartArgs —— 拿到 toolargsoptions
  • tool/index.ts:20 ToolHookOnEndArgs —— 额外拿到 output(成功时)或 error(失败时),二者互斥。

妙处:onEnd 可以覆盖输出

ToolHookOnEndResult(tool/index.ts:30)允许 onEnd 返回一个 { output } 来替换工具的真实输出。这是「后处理」的钩子:

// 示意,非源码:onEnd 把敏感字段抹掉再喂给模型
hooks: {
onEnd: async ({ output }) => {
return { output: redactSecrets(output) }; // 返回覆盖值
},
}

真实调用点

钩子不是工具自己调的,而是 Agent 在执行工具时调的。在 agent/agent.ts 的工具执行逻辑里:

  • agent/agent.ts:6447 先跑 tool.hooks?.onStart,再跑 agent 级的 hooks.onToolStart
  • agent/agent.ts:6483 resolveToolEndOutputtool.hooks?.onEnd,若返回带 output 就覆盖(agent/agent.ts:6490hasOutputOverride 判断),然后 agent 级 onToolEnd 还能再覆盖一次;被覆盖过的输出会重新过一遍 validateToolOutput(agent/agent.ts:6509)。

所以覆盖顺序是:工具输出 → 工具 onEnd → agent onToolEnd → 校验。执行的完整编排属于上一章的运行时职责,这里只看工具这一侧的接口。


5. 工具的组织:Toolkit 与三个 Manager

工具多了要分组、要防重名、要能一次性拍平交给模型。这层是 Toolkit + 三个 Manager 的活。

5.1 Toolkit:带「共享指令」的工具包

tool/toolkit.ts:8Toolkit 类型是「一组相关工具 + 可选的共享说明」。比 Tool[] 多出来的关键字段是 instructionsaddInstructions:

  • instructions(tool/toolkit.ts:25)—— 给模型的「怎么用这组工具」的说明。
  • addInstructions(tool/toolkit.ts:33)—— 若为 true,这段指令会被加进 agent 的 system prompt;此时工具包内单个工具的说明可能被忽略以避免冗余。

createToolkit(tool/toolkit.ts:48)做默认值填充:没 name 抛错、空工具只告警、addInstructions 缺省 false

5.2 三个 Manager 的分工

BaseToolManager (抽象:增删查/查重/拍平的通用逻辑)
│ abstract addToolkit()
┌───────┴────────┐
▼ ▼
ToolManager ToolkitManager
(顶层,能装工具包) (工具包内部,addToolkit 是 no-op)
  • tool/manager/BaseToolManager.ts:31 BaseToolManager —— 通用基类,内部三个 Map:baseTools(用户工具)、providerTools(provider 定义的工具)、toolkits(工具包)。
  • tool/manager/ToolManager.ts:10 ToolManager —— 顶层,接受独立工具、provider 工具、工具包三种。
  • tool/manager/ToolkitManager.ts:6 ToolkitManager —— 工具包内部的管理器,它的 addToolkit(tool/manager/ToolkitManager.ts:41)是故意的 no-op:工具包里不能再套工具包,调用它只告警不崩。

这就是为什么层级只有两层:顶层管理器 → 工具包 → 工具。

5.3 分类靠「类型守卫」

Manager 怎么知道传进来的是工具、provider 工具、还是工具包?靠三个 duck-typing 守卫(tool/manager/BaseToolManager.ts):

守卫判据
isBaseTooltool.type === "user-defined":12
isProviderTooltool.type === "provider":19
isToolkittools 数组属性:26

Tool 类上那个 readonly type = "user-defined"(tool/index.ts:258)就是为跨模块可靠识别而设的稳定标记——避免 instanceof 在不同打包边界下失效。

5.4 查重:同名工具的处理规则

addStandaloneTool(tool/manager/BaseToolManager.ts:89)和 addToolkit(tool/manager/ToolManager.ts:25)都会调 hasToolInAny(tool/manager/BaseToolManager.ts:243)递归检查重名。规则值得记:

场景行为
独立工具重名告警但覆盖(Map.set 直接盖掉)
工具包重名告警,然后替换(ToolManager.ts:34)
工具包内工具与已有工具冲突告警并整包跳过,addToolkit 返回 false(ToolManager.ts:37)

注意不对称:独立工具冲突是「盖掉」,工具包冲突是「拒绝」——因为工具包是原子的,不能只装一半。

5.5 拍平:prepareToolsForExecution

ToolManager.ts:48prepareToolsForExecution 是「交给模型前的最后一步」。它:

  1. getAllBaseTools(BaseToolManager.ts:173)把独立工具 + 所有工具包内的工具拍平成一个列表。
  2. 对每个工具建一条记录 {description, inputSchema, needsApproval, providerOptions, toModelOutput}
  3. 客户端工具(isClientSide() 为真,ToolManager.ts:75)不挂 execute,直接跳过——服务端不执行它。
  4. provider 工具原样透传(ToolManager.ts:87)。

产出就是喂给 AI SDK 的 tools 字典。注意 inputSchema: tool.parameters 传的是 Zod schema,AI SDK 内部再转 JSON Schema。


6. 内建推理工具:think 与 analyze

它要解决的小问题

想让模型「先想清楚再动手」,而不是拿到问题就瞎调工具。VoltAgent 内建了一个推理工具包,给模型一个「草稿纸」。

两个工具

  • tool/reasoning/tools.ts:23 thinkTool(name think)—— 让模型记录一步思考:title + thought + 可选 action + confidence。描述里明确要求「在调其它工具或给最终答案之前用」。
  • tool/reasoning/tools.ts:88 analyzeTool(name analyze)—— 让模型评估上一步的结果,并给出 next_action:continue / validate / final_answer

这俩工具的 execute 其实不干实事——它只是把这一步构造成一个 ReasoningStep 用 Zod 校验一下(tools.ts:55),然后返回「已记录」。它们的价值在于逼模型把推理显式写出来,变成对话里可观测、可追溯的一步。

打包:createReasoningTools

tool/reasoning/index.ts:147createReasoningTools 返回一个 Toolkit,并且默认 addInstructions: true——把一大段 DEFAULT_INSTRUCTIONS(index.ts:8)注入 system prompt,教模型「先 think、循环 think→analyze、只在 final_answer 时给结论」。还可选带 few-shot 示例(index.ts:28 FEW_SHOT_EXAMPLES)。

这是「工具包 + 共享指令」模式的最佳范例:工具本身很轻,真正塑造行为的是随包附带的指令。


7. 工具太多怎么办:语义路由

它要解决的小问题

一个 agent 挂 200 个工具,全塞进 prompt 会:1) 撑爆上下文;2) 模型在一堆工具里挑花眼、挑错。工具路由(tool routing) 的思路:平时只给模型两个「元工具」——先搜、再调

思路:searchTools → callTool 两段式

模型想干活


┌──────────────┐ "帮我查天气" ┌────────────────────┐
│ searchTools │◀────query────────│ 模型只看得到这两个 │
│ 语义挑 topK 个│ │ 元工具(+ expose 的) │
└──────┬───────┘ └────────────────────┘
│ 返回相关工具的 name+schema

┌──────────────┐ name+args
│ callTool │──────────────▶ 真正执行 pool 里那个工具
│ 按名字调 │ (可强制:没搜过不准调)
└──────────────┘
  • pool = 所有候选工具(模型平时看不到);
  • expose = 少数始终可见的工具;
  • searchTools 从 pool 里按语义挑出 topK 个,把它们的 schema 返回给模型;
  • callTool 按名字执行——可选强制「必须先搜过才能调」。

语义挑选:createEmbeddingToolSearchStrategy

tool/routing/embedding.ts:69createEmbeddingToolSearchStrategy 是默认的挑选策略。原理是向量相似度:

  1. 把每个工具序列化成一段文本(defaultToolText,embedding.ts:53:拼 name + description + tags + 参数 schema)。
  2. 用 embedding 模型把工具文本和用户 query 都转成向量。
  3. 算 query 向量与每个工具向量的 余弦相似度(embedding.ts:177 cosineSimilarity),排序取 topK。

关键优化——缓存(embedding.ts:80):工具文本没变就不重算向量。getToolEmbeddings(embedding.ts:82)先清掉已不存在的工具的缓存,再只对「新的或文本变了的」工具批量算向量(embedding.ts:105),命中/未命中数还会记进 span 属性(embedding.ts:194)。

策略接口很小,tool/routing/types.ts:49ToolSearchStrategy 只要求一个 select({query, tools, topK, context})——你可以换成任何自定义挑选逻辑,不一定用 embedding。

配置:ToolRoutingConfig

tool/routing/types.ts:71:

字段含义
pool候选工具池(不给则自动用全部工具)
expose始终对模型可见的工具
embedding挑选用的 embedding 配置(不给则退化到简单排序)
topK每次搜返回几个
enforceSearchBeforeCall是否强制「先搜再调」,默认 true

装配与执行(agent 侧)

  • agent/agent.ts:8498 applyToolRoutingConfig —— 装配期:建 searchTools / callTool 两个内建工具并塞进 manager(agent.ts:8514);把 expose 的工具加进可见集、pool 的工具加进池;若没显式给 pool,就自动把所有普通工具当池(agent.ts:8531)。
  • agent/agent.ts:6906 createToolRoutingSearchTool —— searchTools 的实现:算 effectiveTopK(agent.ts:6927)、取候选、调策略挑选,并给整个过程建 observability span。两个内建工具的名字是常量 searchTools / callTool(tool/routing/constants.ts:1)。
  • agent/agent.ts:7033 callTool 的实现:先防递归(不能拿 callTool 调 callTool,agent.ts:7042);从池里取目标工具;若 enforceSearchBeforeCall,检查这个工具是否被搜过,没搜过就抛错(agent.ts:7058);再校验参数、执行。

8. MCP:接入外部工具服务器

它要解决的小问题

工具不一定得写在你的代码里。Model Context Protocol(MCP) 是一套标准协议,让工具能力跑在独立的 server 进程里(可能是别人写的、别的语言写的)。VoltAgent 要做的是:连上这些 server,把它们暴露的工具包装成和本地 Tool 一模一样的东西,让 agent 无感使用。

一句话直觉

MCP server 就像「工具的 USB 设备」,MCPClient 是「驱动」。 插上(连接)、枚举设备提供的功能(listTools)、把每个功能包成本地 Tool(getAgentTools),agent 用起来跟自带工具没区别。

全景图

┌──────────────────┐ servers:{...} ┌─────────────┐ transport ┌──────────┐
│ MCPConfiguration │──── 每个 server ─▶│ MCPClient │◀───────────▶│ MCP │
│ (多 server 编排) │ 懒连接+缓存 │ (一个连接) │ stdio/http/ │ server │
│ · getTools() │◀── Tool[] ────────│ getAgentTools│ sse │ (进程) │
│ · authorization │ │ callTool │ └──────────┘
└──────────────────┘ │ elicitation │
│ └─────┬────────┘
│ 每个远端工具包成 │ server 反问用户?
│ Tool(名字加 server 前缀) ▼
▼ ┌──────────────┐
塞进 agent.tools │UserInputBridge│ 把问题转给你的 handler
└──────────────┘

8.1 MCPConfiguration:多 server 编排

mcp/registry/index.ts:43 MCPConfiguration<TServerKeys> 是「一组 MCP server 的配置管理器」。你给它一张 servers 表,它负责连接、缓存、取工具:

  • mcp/registry/index.ts:101 getTools(authContext?) —— 从所有配置的 server 并发取工具,拍平成一个 Tool[]。单个 server 出错只打日志、返回空,不拖垮其它(registry/index.ts:110)。
  • mcp/registry/index.ts:377 getConnectedClient —— 懒连接 + 缓存:缓存里有就复用(并顺手 connect 一次验活),连接坏了就删缓存重建。
  • 还有 getToolsets(按 server 分组)、getRawTools(拿原始定义)、getClient(s)(直接拿连接)等变体。

8.2 MCPClient:一个连接的封装

mcp/client/index.ts:46 MCPClient 包装官方 MCP SDK 的 Client,处理连接、传输、工具调用。

四种传输方式,构造时按 server 配置的 type 选(mcp/client/index.ts:160):

type传输说明
httpStreamable HTTP默认带 SSE 回退
sseSSE显式 SSE
streamable-httpStreamable HTTP显式,无回退
stdio子进程标准输入输出本地跑一个命令当 server

SSE 回退是个韧性设计:http 类型先试 Streamable HTTP,失败了自动降级重连 SSE(mcp/client/index.ts:271 attemptSSEFallback)——旧 server 可能只支持 SSE,这样不用改配置也能连上。

把远端工具包成本地 Tool——核心在 getAgentTools(mcp/client/index.ts:376):

  1. listTools(client/index.ts:348)拿远端工具定义(name/description/inputSchema)。
  2. 把远端的 JSON Schema 转回 Zod(client/index.ts:390,用 zod-from-json-schema)——因为本地 Tool 要求 Zod。
  3. createTool 包一个本地工具,名字加 server 前缀:`${clientInfo.name}_${toolDef.name}`(client/index.ts:395)——避免多个 server 的同名工具撞车。
  4. 这个本地工具的 execute(client/index.ts:408)其实就是回头调 this.callTool(...)(client/index.ts:474),把调用通过传输层发给远端 server,返回 result.content

于是:远端工具与本地工具在 agent 眼里完全一样,都是 Tool

8.3 UserInputBridge:server 反问用户(elicitation)

MCP 有个能力叫 elicitation:工具执行到一半,server 可以反过来问用户(「你确定要删这条吗?」)。VoltAgent 用 UserInputBridge(mcp/client/user-input-bridge.ts:43)桥接。

  • 客户端构造时永远声明 elicitation 能力(client/index.ts:138),并注册一个请求处理器(client/index.ts:211),把 server 的 elicit 请求转给 bridge 的 processRequest(user-input-bridge.ts:149)。
  • 你通过 mcpClient.elicitation.setHandler(...) 注册怎么应答;还有 once()(一次性 handler,user-input-bridge.ts:115)和 removeHandler()
  • 没注册 handler 时,请求被自动取消(user-input-bridge.ts:150 返回 {action: "cancel"})——安全默认:server 问不到人就不阻塞。

单次工具调用还能临时挂 handler:getAgentTools 生成的 execute 支持从 execOptions.elicitation 拿一次性 handler,用完在 finally 里恢复原状态(client/index.ts:413:440)。

8.4 授权:can 函数

mcp/authorization/types.ts:82 MCPAuthorizationConfig 让你对 MCP 工具做访问控制。核心是一个 can 函数(authorization/types.ts:59 MCPCanFunction),它在两个时机被问:

action时机开关
discovery列举工具时(getTools)filterOnDiscovery,默认 false
execution执行工具时(callTool)checkOnExecution,默认 true

can 拿到 {toolName, serverName, action, arguments, userId, context},返回 true/false{allowed, reason}

  • 发现期过滤:MCPConfiguration.filterToolsByAuthorization(registry/index.ts:157)对每个工具问一次 can,不允许的直接从列表里剔除。注意它会先剥掉 server 前缀再问(registry/index.ts:178),因为 can 关心的是原始工具名。
  • 执行期检查:MCPClient.callTool(client/index.ts:478)在真正调用前问 can,不允许就抛 MCPAuthorizationError(client/index.ts:496)。

一个真实的 can(源码注释里的示例,authorization/types.ts:49):按 context 里的角色判断,非 admin 不准调 delete_item。这就是「基于角色/属性的工具访问控制」。


9. RAG 检索作为一种工具

它要解决的小问题

RAG(检索增强生成) 说白了就是:回答前先去知识库搜一段相关资料塞进上下文。在 VoltAgent 里,「搜知识库」不是特殊机制,而就是一个普通工具——模型决定何时检索。

BaseRetriever:抽象检索器

retriever/retriever.ts:13 BaseRetriever 是所有检索器的基类。你继承它、实现一个抽象方法 retrieve(input, options)(retriever.ts:92)返回一段字符串,就有了一个检索器。

它的巧妙处是构造时就自带一个现成工具:readonly tool 字段(retriever.ts:42)。构造函数(retriever.ts:48)里调 createRetrieverToolthis 包成工具,于是你能直接解构:

// 示意,非源码
const { tool } = new MyRetriever();
const agent = new Agent({ /* ... */, tools: [tool] });

createRetrieverTool:包装成工具

retriever/tools/index.ts:33 createRetrieverTool 就是一个 createTool 调用:参数固定为 {query: string}(tools/index.ts:48),execute 里调 retriever.retrieve(query, options)(tools/index.ts:101),把选项透传给检索器(这样检索器能拿到 userIdconversationId 等上下文),并给检索加日志和 observability 属性。

工具默认名字 search_knowledge、默认描述「在知识库里搜相关信息」(tools/index.ts:40),都可覆盖。

所以 RAG 在这个框架里没有独立的「检索管线」概念——检索就是模型工具箱里的一把工具,和 get_weather 平级。


10. 巧妙之处(可以带走的技术)

  • 一个 Zod schema 服务两端。 同一份 parameters 既转成 JSON Schema 给模型看,又用 z.inferexecute 编译期类型。定义一次,类型安全和模型提示都有了(tool/index.ts:184)。

  • 客户端工具 = 没有 execute。 用「有没有实现函数」这一个信号区分服务端/客户端工具,不需要额外的 flag(tool/index.ts:275)。

  • stable type 标记胜过 instanceof。 Tooltype = "user-defined" 常量,跨打包边界识别可靠,避免 instanceof 在多份 bundle 下失效(tool/index.ts:258、守卫在 BaseToolManager.ts:12)。

  • 工具包冲突整包拒绝、独立工具冲突覆盖。 不对称的查重规则,因为工具包是原子的(ToolManager.ts:37 vs BaseToolManager.ts:118)。

  • 嵌套工具包用 no-op 挡住。 ToolkitManager.addToolkit 不是抛错而是告警返回 false,把「两层结构」这个约束做成软约束,调用者不会崩(ToolkitManager.ts:41)。

  • 工具向量缓存按文本比对失效。 只有工具描述真变了才重算 embedding,还能顺便淘汰已删工具的缓存(embedding.ts:99)。

  • MCP 远端工具 = 本地工具。 JSON Schema→Zod 转换 + server 前缀命名,让远端工具在 agent 眼里和自带工具零差异(client/index.ts:390:395)。

  • SSE 回退。 http 传输失败自动降级 SSE,兼容老 server 不用改配置(client/index.ts:271)。

  • RAG 不是管线,是工具。 检索器构造时自包装成工具,const { tool } = retriever 直接可用(retriever.ts:42)。


11. 边界与局限

  • 工具包只有两层。 工具包不能嵌套工具包(ToolkitManager.addToolkit 是 no-op)。要更深的层级得自己在业务层组织。

  • 客户端工具服务端不执行。 没有 execute 的工具,prepareToolsForExecution 会跳过挂载(ToolManager.ts:75),需要前端配合真正执行,否则调用无结果。

  • MCP 单 server 失败静默。 getTools 里单个 server 出错只 console.error 返回空数组(registry/index.ts:110),不会抛给调用方——好处是韧性,坏处是工具「悄悄少了」不易察觉。

  • 强制搜索前置默认开启。 开了 tool routing 后 enforceSearchBeforeCall 默认 true,模型没先 searchToolscallTool 会直接报错(agent.ts:7058)。这是刻意的护栏,但也意味着模型必须「守规矩」。

  • elicitation 无 handler 即取消。 server 反问用户时若没注册 handler,请求被自动 cancel(user-input-bridge.ts:150),不会阻塞——但也可能让依赖用户确认的工具「静默走默认路径」。


12. 横向对比

同 shelf 兄弟章的分工:

VoltAgent 在工具这块的取舍:Zod-first + AI SDK 底座——不自己发明工具协议,而是紧贴 Vercel AI SDK 的 tools 契约,把复杂度花在编排(Manager)、路由(embedding)、外部接入(MCP)这三层增值上。


13. 代码地图(导航索引)

主题文件路径关键符号
工具定义 / 工厂packages/core/src/tool/index.tscreateToolclass ToolisClientSide
生命周期钩子packages/core/src/tool/index.tsToolHooksToolHookOnEndResult
多模态输出packages/core/src/tool/index.tsToolResultOutputtoModelOutput
工具包packages/core/src/tool/toolkit.tsToolkitcreateToolkit
通用管理器packages/core/src/tool/manager/BaseToolManager.tsBaseToolManageraddStandaloneToolhasToolInAnyisBaseTool
顶层管理器packages/core/src/tool/manager/ToolManager.tsToolManageraddToolkitprepareToolsForExecution
工具包内管理器packages/core/src/tool/manager/ToolkitManager.tsToolkitManager(addToolkit no-op)
推理工具packages/core/src/tool/reasoning/tools.tsthinkToolanalyzeTool
推理工具包packages/core/src/tool/reasoning/index.tscreateReasoningToolsDEFAULT_INSTRUCTIONS
语义路由策略packages/core/src/tool/routing/embedding.tscreateEmbeddingToolSearchStrategycosineSimilarity
路由类型 / 配置packages/core/src/tool/routing/types.tsToolSearchStrategyToolRoutingConfig
路由常量packages/core/src/tool/routing/constants.tsTOOL_ROUTING_SEARCH_TOOL_NAME
路由装配(agent 侧)packages/core/src/agent/agent.tsapplyToolRoutingConfigcreateToolRoutingSearchToolcallTool
MCP 导出packages/core/src/mcp/index.tsMCPConfigurationMCPClientUserInputBridge
MCP 多 server 编排packages/core/src/mcp/registry/index.tsMCPConfigurationgetToolsfilterToolsByAuthorization
MCP 客户端packages/core/src/mcp/client/index.tsMCPClientgetAgentToolscallToolattemptSSEFallback
MCP 用户输入桥packages/core/src/mcp/client/user-input-bridge.tsUserInputBridgeprocessRequestonce
MCP 授权packages/core/src/mcp/authorization/types.tsMCPCanFunctionMCPAuthorizationConfig
RAG 检索器基类packages/core/src/retriever/retriever.tsBaseRetrievertoolretrieve
RAG 工具包装packages/core/src/retriever/tools/index.tscreateRetrieverTool