📖 本章学习目标
- ✅ 理解工具系统的核心机制(Tool Calling)
- ✅ 使用
tool()API 定义同步和异步工具- ✅ 掌握工具的错误处理策略
- ✅ 实现动态工具选择(基于权限或上下文)
- ✅ 集成 MCP 协议连接外部工具生态
- ✅ 设计高质量的工具描述和参数 Schema
- ✅ 避免常见的工具设计陷阱
一、工具的本质:赋予 Agent 行动能力
大语言模型本身是"封闭"的——它只能根据训练数据生成文本,没有办法主动获取信息或操作外部系统。
类比理解: LLM 就像一个被关在图书馆里的学者:
- 📚 他知道很多知识(训练数据)
- ❌ 但他无法上网查最新信息
- ❌ 也无法帮你发邮件或操作数据库
工具(Tools) 打破了这个封闭性,就像给学者配备了:
- 🌐 互联网接入(搜索工具)
- 📧 邮件客户端(通信工具)
- 💻 电脑终端(系统操作工具)
1、工具调用机制
工具调用的核心流程:
sequenceDiagram
participant U as 用户
participant A as Agent
participant L as LLM
participant T as Tool
U->>A: "帮我查北京今天的天气"
A->>L: 传入用户消息 + 工具定义
Note over L: LLM 分析任务<br/>决定需要查天气
L-->>A: tool_call(get_weather, {city: "北京"})
Note over A: LLM 给出工具名和参数<br/>但不实际执行
A->>T: 调用 get_weather("北京")
T-->>A: "北京今日晴,22°C"
A->>L: 传入工具结果
Note over L: LLM 基于真实数据回答
L-->>A: "北京今天天气晴朗..."
A-->>U: 返回最终回答
关键点:
- LLM 只决定调用哪个工具、传什么参数
- 程序负责实际执行工具调用
- 工具结果回传给 LLM,让它基于真实信息回答
2、常见工具类型
| 类型 | 具体示例 | 应用场景 |
|---|---|---|
| 信息检索 | 网络搜索、知识库查询、数据库读取 | 获取实时信息、查询私有数据 |
| 计算执行 | Python 代码解释器、计算器、SQL 执行 | 数学计算、数据分析 |
| 系统操作 | 文件读写、进程管理、终端命令 | 自动化工作流 |
| 外部集成 | 邮件发送、日历操作、第三方 API | 与外部系统交互 |
| 数据处理 | 图像识别、文档解析、格式转换 | 多媒体内容处理 |
二、定义工具:tool() API
LangChain.js 的 tool() 函数是定义工具的标准方式。
1、基础结构
每个工具由三部分组成:
- 要执行的函数
- 函数的元数据,包括名称、描述等
- 使用
tool工具函数包装
import { tool } from "@langchain/core/tools";
import { z } from "zod";
const myTool = tool(
// 第一部分:执行函数(做什么)
(params) => {
// 实际的业务逻辑
return "结果字符串";
},
// 第二部分:元数据(告诉 LLM 这是什么)
{
name: "my_tool", // 工具名称
description: "工具的功能描述", // LLM 根据描述决定是否调用
schema: z.object({...}), // 参数校验规则
}
);
2、简单示例:计算器工具
让我们从零开始构建一个计算器工具。
第一步:定义执行函数
// 简单的数学计算函数
function calculateExpression(expression: string): string {
try {
// 注意:eval 在生产环境有安全风险
// 实际应用要用安全的数学解析库(如 mathjs)
const result = eval(expression);
return `计算结果:${result}`;
} catch {
return `计算错误:无效的表达式 "${expression}"`;
}
}
代码解读:
- 接收字符串形式的数学表达式
- 使用
eval()计算结果(仅用于演示) - 捕获错误并返回友好的错误消息
- 返回值必须是字符串
第二步:定义参数 Schema
import { z } from "zod";
// 使用 Zod 定义参数结构
const calculatorSchema = z.object({
expression: z
.string()
.describe("要计算的数学表达式,如 '2 + 3 * 4' 或 'Math.sqrt(16)'"),
});
为什么需要 Schema?
- ✅ 确保 LLM 传入正确类型的参数
- ✅ 提供参数说明,帮助 LLM 理解如何使用
- ✅ 在运行时自动校验参数
第三步:组合成完整工具
import { tool } from "@langchain/core/tools";
const calculator = tool(
// 执行函数
({ expression }) => {
try {
const result = eval(expression);
return `计算结果:${result}`;
} catch {
return `计算错误:无效的表达式`;
}
},
// 元数据
{
name: "calculator",
description: "计算数学表达式,支持加减乘除和基本函数(sin, cos, sqrt 等)",
schema: calculatorSchema,
}
);
完整的工具定义要素:
| 要素 | 作用 | 重要性 |
|---|---|---|
name | 工具的唯一标识符 | ⭐⭐⭐ LLM 用它来引用工具 |
description | 功能描述和使用场景 | ⭐⭐⭐⭐⭐ 最关键,直接影响调用准确性 |
schema | 参数结构和校验规则 | ⭐⭐⭐⭐ 保证类型安全 |
💡 最佳实践:如何写好工具描述
好的描述应该回答三个问题:
- 这个工具做什么?(功能)
- 什么时候应该用它?(使用场景)
- 输出格式是什么?(返回内容)
❌ 模糊的描述:
description: "搜索信息"✅ 清晰的描述:
description: "使用 Google 搜索互联网上的最新信息,返回前 5 条搜索结果的标题、摘要和链接。适合查询最新事件、获取外部知识、验证事实信息。不适合查询本地文件或数据库内容。"
3、异步工具:处理 I/O 操作
大多数真实工具需要做 I/O 操作(网络请求、数据库查询等),需要用异步函数。让我们以构建一个真实的天气查询工具为例,使用免费的 Open-Meteo API查询某个城市的天气。
第一步:了解 API
Open-Meteo 提供两个接口:
- 地理编码 API:城市名 → 经纬度
- 天气 API:经纬度 → 天气数据
第二步:编写执行函数
import { tool } from "@langchain/core/tools";
import { z } from "zod";
const getWeather = tool(
async ({ city, country = "CN" }) => {
try {
// 第一步:地理编码(城市名 → 经纬度)
const geoRes = await fetch(
`https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(city)}&count=1`
);
const geoData = await geoRes.json();
if (!geoData.results?.length) {
return `找不到城市:${city}`;
}
const { latitude, longitude } = geoData.results[0];
// 第二步:获取天气数据
const weatherRes = await fetch(
`https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}¤t=temperature_2m,precipitation,wind_speed_10m`
);
const weatherData = await weatherRes.json();
const current = weatherData.current;
// 第三步:格式化返回结果
return JSON.stringify({
city,
temperature: `${current.temperature_2m}°C`,
precipitation: `${current.precipitation}mm`,
windSpeed: `${current.wind_speed_10m}km/h`,
});
} catch (error) {
return `天气查询失败:${error instanceof Error ? error.message : "未知错误"}`;
}
},
// 元数据
{
name: "get_weather",
description: "查询指定城市的当前实时天气,返回温度、降水量和风速。适合回答关于天气的问题。",
schema: z.object({
city: z.string().describe("城市名称,支持中英文,如 '北京' 或 'Beijing'"),
country: z.string().optional().describe("国家代码(ISO 3166),默认 CN"),
}),
}
);
代码分步解读:
-
第 5-15 行:地理编码
- 将城市名转换为经纬度
- 处理找不到城市的情况
-
第 17-20 行:获取天气
- 使用经纬度查询实时天气
- 获取温度、降水、风速等数据
-
第 22-28 行:格式化结果
- 将数据组织为 JSON 字符串
- 包含所有关键信息
-
第 29-31 行:错误处理
- 捕获网络错误或 API 异常
- 返回友好的错误消息
第三步:测试工具
import { createAgent } from "langchain";
const agent = createAgent({
model: "openai:gpt-4o",
tools: [getWeather],
});
const result = await agent.invoke({
messages: [{ role: "user", content: "北京今天天气怎么样?" }]
});
console.log(result.messages.at(-1)?.content);
// 输出:北京今天天气晴朗,气温 22°C,微风...
4、返回复杂内容
工具不仅可以返回字符串,还可以返回包含多个内容块的数组(如文本 + 图片)。
import { tool } from "@langchain/core/tools";
import { z } from "zod";
// 返回多媒体内容的工具
const generateChart = tool(
async ({ data, title }) => {
// 假设有一个生成图表的函数
const chartUrl = await createChartImage(data, title);
// 返回多个内容块
return [
{ type: "text", text: `图表已生成:${title}` },
{ type: "image_url", image_url: { url: chartUrl } },
];
},
{
name: "generate_chart",
description: "根据数据生成柱状图或折线图,返回图片 URL",
schema: z.object({
data: z.array(z.number()).describe("图表数据数组,如 [10, 20, 30]"),
title: z.string().describe("图表标题"),
}),
}
);
支持的 content 类型:
| 类型 | 用途 | 示例 |
|---|---|---|
text | 纯文本 | { type: "text", text: "结果" } |
image_url | 图片 | { type: "image_url", image_url: { url: "..." } } |
audio_url | 音频 | { type: "audio_url", audio_url: { url: "..." } } |
三、工具的错误处理
工具调用出错是常见情况(API 超时、参数错误、权限不足等)。如何优雅地处理错误,直接影响 Agent 的健壮性。
1、策略对比
| 策略 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 工具内部处理 | 简单直接,灵活控制 | 每个工具都要写错误处理 | 少量工具 |
| 中间件统一处理 | DRY 原则,统一管理 | 可能丢失特定工具的上下文 | 大量工具 |
| 混合策略 | 兼顾灵活性和一致性 | 复杂度稍高 | 生产环境推荐 |
2、工具内部处理(简单场景)
在工具函数里捕获错误,返回错误描述字符串:
const searchDatabase = tool(
async ({ query, table }) => {
try {
// 执行数据库查询
const results = await db.query(`SELECT * FROM ${table} WHERE ...`);
if (results.length === 0) {
return "查询没有找到结果,请尝试不同的搜索条件。";
}
// 只返回前 10 条,避免 Token 浪费
return JSON.stringify(results.slice(0, 10));
} catch (error) {
// 根据错误类型返回不同的消息
if (error instanceof DatabaseError) {
return `数据库查询失败:表 "${table}" 不存在或无权访问。`;
}
return `查询出错:${error instanceof Error ? error.message : "未知错误"}`;
}
},
{
name: "search_database",
description: "在数据库中搜索数据,返回匹配的记录",
schema: z.object({
query: z.string().describe("搜索关键词"),
table: z.string().describe("要搜索的表名,如 'users' 或 'orders'"),
}),
}
);
优势:
- ✅ 可以针对特定错误类型返回定制化消息
- ✅ LLM 可以根据错误信息调整策略(如换个表名再试)
3、中间件统一处理(推荐用于大量工具)
对于大量工具,在每个工具里写错误处理会有重复代码。更好的做法是在 Agent 级别统一处理。
import { createAgent, createMiddleware } from "langchain";
import { ToolMessage } from "@langchain/core/messages";
// 创建统一的工具错误处理中间件
const toolErrorHandler = createMiddleware({
name: "ToolErrorHandler",
// 拦截工具调用
wrapToolCall: async (request, handler) => {
try {
// 正常执行工具
return await handler(request);
} catch (error) {
// 统一返回友好的错误消息
console.error(`工具 ${request.toolCall.name} 调用失败:`, error);
return new ToolMessage({
content: `工具调用失败:${error instanceof Error ? error.message : "未知错误"}。请尝试其他方式或告知用户无法完成该操作。`,
tool_call_id: request.toolCall.id!,
});
}
},
});
// 注册中间件
const agent = createAgent({
model: "openai:gpt-4o",
tools: [searchDatabase, getWeather, calculator],
middleware: [toolErrorHandler], // 添加错误处理中间件
});
工作原理:
- 中间件拦截所有工具调用
- 如果工具抛出异常,捕获它
- 返回标准化的错误消息给 LLM
- LLM 根据错误消息决定下一步行动
优势:
- ✅ DRY 原则:错误处理逻辑只写一次
- ✅ 统一管理:所有工具的错误格式一致
- ✅ 日志记录:可以在中间件里统一记录错误日志
4、混合策略(生产环境推荐)
结合两种方式:
- 工具内部处理业务逻辑错误(如"找不到城市")
- 中间件处理系统级错误(如网络超时、权限不足)
// 工具内部:处理预期的业务错误
const getWeather = tool(
async ({ city }) => {
const geoData = await fetchGeoData(city);
if (!geoData.results?.length) {
// 这是预期的业务错误,返回友好提示
return `找不到城市:${city},请检查城市名称是否正确。`;
}
// ...继续处理
},
{ /* 元数据 */ }
);
// 中间件:处理未预期的系统错误
const toolErrorHandler = createMiddleware({
name: "ToolErrorHandler",
wrapToolCall: async (request, handler) => {
try {
return await handler(request);
} catch (error) {
// 这些是未预期的错误(网络故障、权限问题等)
logErrorToMonitoring(error); // 记录到监控系统
return new ToolMessage({
content: "系统暂时无法完成此操作,请稍后重试。",
tool_call_id: request.toolCall.id!,
});
}
},
});
四、工具注册与管理
1、静态工具列表(最常见)
创建 Agent 时直接传入工具数组:
import { createAgent } from "langchain";
const agent = createAgent({
model: "openai:gpt-4o",
tools: [
searchTool,
calculatorTool,
weatherTool,
emailTool,
],
});
适用场景: 工具列表固定,所有用户都能使用相同的工具。
2、动态工具选择(基于权限)
有时候你不希望把所有工具都暴露给 Agent(比如某些工具只有管理员才能用)。
实现思路
import { createAgent } from "langchain";
// 定义工具池
const allTools = {
basic: [searchTool, calculatorTool],
advanced: [weatherTool, codeExecutorTool],
admin: [deleteDataTool, systemConfigTool],
};
// 根据用户角色获取可用工具
function getToolsForUser(userRole: "admin" | "user" | "guest") {
switch (userRole) {
case "admin":
return [...allTools.basic, ...allTools.advanced, ...allTools.admin];
case "user":
return [...allTools.basic, ...allTools.advanced];
case "guest":
return allTools.basic;
default:
return allTools.basic;
}
}
// 创建 Agent(初始不传工具)
const agent = createAgent({
model: "openai:gpt-4o",
tools: []
});
// 调用时动态传入工具
const userRole = "user"; // 从会话中获取
const result = await agent.invoke(
{ messages: [{ role: "user", content: "帮我删除测试数据" }] },
{ configurable: { tools: getToolsForUser(userRole) } }
);
执行流程:
- 用户发起请求
- 根据用户角色筛选可用工具
- 将筛选后的工具传给 Agent
- Agent 只能看到和使用授权的工具
安全优势:
- ✅ 最小权限原则:用户只能访问必要的工具
- ✅ 防止越权操作:普通用户无法调用管理员工具
- ✅ 灵活控制:可以随时调整用户的工具权限
3、动态工具选择(基于上下文)
根据任务类型动态加载相关工具,减少无关工具的干扰。
// 按类别组织工具
const toolCategories = {
research: [searchTool, webpageFetcherTool, summarizerTool],
coding: [codeExecutorTool, fileReaderTool, linterTool],
communication: [emailTool, slackNotifierTool],
};
// 根据任务类型选择工具
function selectToolsByTask(taskType: keyof typeof toolCategories) {
return toolCategories[taskType] || toolCategories.research;
}
// 使用示例
const agent = createAgent({ model: "openai:gpt-4o", tools: [] });
// 编程任务:只加载编程相关工具
const result = await agent.invoke(
{ messages: [{ role: "user", content: "帮我写一个排序算法" }] },
{ configurable: { tools: selectToolsByTask("coding") } }
);
优势:
- ✅ 提高准确性:减少无关工具的干扰
- ✅ 节省 Token:工具定义会占用 Prompt 空间
- ✅ 加快响应:LLM 不需要在不相关的工具中做选择
五、MCP:连接外部工具生态
Model Context Protocol(MCP) 是 Anthropic 于 2024 年提出的开放协议,旨在标准化 LLM 与外部工具的连接方式。
1、什么是 MCP?
可以把 MCP 理解为 AI 工具领域的"USB 接口":
flowchart LR
subgraph LangChain["LangChain Agent"]
Agent["Agent"]
Client["MultiServerMCPClient"]
end
subgraph MCP_Servers["MCP 服务器(可来自任何地方)"]
S1["文件系统 MCP<br/>读写本地文件"]
S2["GitHub MCP<br/>操作代码仓库"]
S3["数据库 MCP<br/>查询数据库"]
S4["自定义 MCP<br/>你自己的服务"]
end
Client <-->|MCP 协议| S1
Client <-->|MCP 协议| S2
Client <-->|MCP 协议| S3
Client <-->|MCP 协议| S4
Agent --> Client
style Client fill:#f6ffed,stroke:#52c41a,stroke-width:3px
核心优势:
- 🔌 即插即用:任何遵循 MCP 协议的工具都可以直接使用
- 🌍 跨应用复用:一个 MCP 服务器可以被多个 AI 应用共享
- 🛠️ 社区生态:可以直接使用社区现有的 MCP 服务器(GitHub、Slack、PostgreSQL 等)
2、MCP vs 直接定义工具
| 维度 | 直接定义 tool() | 使用 MCP |
|---|---|---|
| 适用场景 | 项目内部的工具逻辑 | 独立的工具服务,需要在多个应用间复用 |
| 接入成本 | 低,直接写函数 | 较高,需要搭建 MCP 服务器 |
| 可复用性 | 仅限当前项目 | 任何 MCP 兼容的客户端都可以使用 |
| 工具生态 | 自己写 | 可接入社区现有的 MCP 服务器 |
| 维护成本 | 低 | 中(需要维护独立的服务) |
💡 选择建议:
- 项目内部的工具优先用
tool()直接定义- 需要跨应用复用、或接入社区工具生态时,使用 MCP
3、安装 MCP 适配器
pnpm add @langchain/mcp-adapters
4、连接 MCP 服务器
MCP 支持两种传输方式:
方式 1:stdio(本地子进程)
适合本地工具或命令行工具:
import { MultiServerMCPClient } from "@langchain/mcp-adapters";
import { createAgent } from "langchain";
// 创建 MCP 客户端
const client = new MultiServerMCPClient({
// 连接文件系统 MCP 服务器
filesystem: {
transport: "stdio",
command: "npx",
args: ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/directory"],
},
// 连接数学计算 MCP 服务器
math: {
transport: "stdio",
command: "node",
args: ["./mcp-servers/math-server.js"],
},
});
// 获取所有 MCP 工具(自动转换为 LangChain 工具格式)
const mcpTools = await client.getTools();
// 创建 Agent
const agent = createAgent({
model: "openai:gpt-4o",
tools: mcpTools,
});
// 使用
const result = await agent.invoke({
messages: [{ role: "user", content: "读取 README.md 文件的内容并总结" }],
});
代码解读:
-
第 6-12 行:配置文件系统 MCP 服务器
- 使用
npx运行官方的文件系统服务器 - 限制访问目录为
/path/to/directory
- 使用
-
第 14-18 行:配置自定义数学服务器
- 运行本地的 Node.js 脚本
-
第 21 行:获取所有工具
- 自动发现并转换 MCP 工具为 LangChain 格式
-
第 24-27 行:创建并使用 Agent
方式 2:HTTP/SSE(远程服务器)
适合部署在云端的工具服务:
const client = new MultiServerMCPClient({
// 连接远程天气服务 MCP 服务器
weather: {
transport: "sse", // Server-Sent Events
url: "https://your-mcp-server.com/sse",
headers: {
Authorization: `Bearer ${process.env.MCP_API_KEY}`,
},
},
});
const mcpTools = await client.getTools();
5、自己构建 MCP 服务器
如果你想把现有的服务能力暴露为 MCP 工具,可以用 MCP SDK 快速搭建。以一个数据库查询 MCP 服务器为例:
第一步:初始化项目
mkdir database-mcp-server && cd database-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk
第二步:编写服务器代码
// mcp-servers/database-server.ts
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
ListToolsRequestSchema,
CallToolRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
// 创建 MCP 服务器实例
const server = new Server(
{ name: "database-server", version: "1.0.0" },
{ capabilities: { tools: {} } }
);
// 声明工具列表
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: "query_users",
description: "根据条件查询用户列表",
inputSchema: {
type: "object",
properties: {
name: {
type: "string",
description: "用户姓名(模糊匹配)"
},
limit: {
type: "number",
description: "返回条数,默认 10"
},
},
},
},
],
}));
// 处理工具调用
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === "query_users") {
const { name, limit = 10 } = request.params.arguments as {
name?: string;
limit?: number;
};
// 执行数据库查询
const users = await db.query(
"SELECT id, name, email FROM users WHERE name LIKE ? LIMIT ?",
[`%${name || ""}%`, limit]
);
// 返回结果
return {
content: [{ type: "text", text: JSON.stringify(users) }],
};
}
throw new Error(`未知工具:${request.params.name}`);
});
// 启动服务器(stdio 模式)
const transport = new StdioServerTransport();
await server.connect(transport);
console.log("Database MCP Server started");
代码分步解读:
-
第 10-13 行:创建服务器实例
- 指定服务器名称和版本
- 声明支持 tools 能力
-
第 16-33 行:声明工具列表
- 定义工具名称、描述
- 定义输入参数的 JSON Schema
-
第 36-56 行:处理工具调用
- 根据工具名执行对应逻辑
- 返回标准格式的响应
-
第 59-61 行:启动服务器
- 使用 stdio 传输模式
- 等待连接
第三步:在 LangChain 中使用
const client = new MultiServerMCPClient({
database: {
transport: "stdio",
command: "node",
args: ["./mcp-servers/database-server.js"],
},
});
const mcpTools = await client.getTools();
const agent = createAgent({ model: "openai:gpt-4o", tools: mcpTools });
六、实用工具示例集
以下是几个在实际 Agent 开发中常用的工具模板。
1、网页内容抓取
import { tool } from "@langchain/core/tools";
import { z } from "zod";
const fetchWebpage = tool(
async ({ url }) => {
try {
// 设置超时和用户代理
const response = await fetch(url, {
headers: {
"User-Agent": "Mozilla/5.0 (compatible; LangChain Agent)"
},
signal: AbortSignal.timeout(10000), // 10 秒超时
});
if (!response.ok) {
return `请求失败:HTTP ${response.status}`;
}
const html = await response.text();
// 简单提取纯文本
// 生产环境建议用 cheerio 或 unfluff
const text = html
.replace(/<script[\s\S]*?<\/script>/gi, "")
.replace(/<style[\s\S]*?<\/style>/gi, "")
.replace(/<[^>]+>/g, " ")
.replace(/\s+/g, " ")
.trim()
.slice(0, 5000); // 限制返回长度,避免 Token 浪费
return text;
} catch (error) {
return `抓取失败:${error instanceof Error ? error.message : "网络错误"}`;
}
},
{
name: "fetch_webpage",
description: "获取指定 URL 的网页内容(纯文本),适合阅读文章、博客、新闻等内容。不支持 JavaScript 渲染的页面。",
schema: z.object({
url: z.string().url().describe("要访问的完整 URL,必须以 http:// 或 https:// 开头"),
}),
}
);
关键点:
- 设置超时防止长时间挂起
- 限制返回长度(5000 字符)避免 Token 浪费
- 简单的 HTML 清理(生产环境用专业库)
2、文件读写(带安全限制)
import { tool } from "@langchain/core/tools";
import { z } from "zod";
import { readFile, writeFile } from "fs/promises";
import { join, resolve } from "path";
// 限制工作目录,防止 Agent 访问不该访问的文件
const WORKSPACE_DIR = resolve("./workspace");
const readFileTool = tool(
async ({ filename }) => {
try {
// 安全检查:防止路径遍历攻击
const safePath = join(WORKSPACE_DIR, filename);
const resolvedPath = resolve(safePath);
if (!resolvedPath.startsWith(WORKSPACE_DIR)) {
return `安全错误:不允许访问工作目录外的文件`;
}
const content = await readFile(resolvedPath, "utf-8");
return content;
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
return `文件不存在:${filename}`;
}
return `文件读取失败:${error instanceof Error ? error.message : "未知错误"}`;
}
},
{
name: "read_file",
description: "读取工作目录中的文件内容。只能访问 ./workspace 目录下的文件。",
schema: z.object({
filename: z.string().describe("文件名(相对于工作目录),如 'data.txt' 或 'docs/readme.md'"),
}),
}
);
安全要点:
- ✅ 限制访问目录(
WORKSPACE_DIR) - ✅ 防止路径遍历攻击(
../逃逸) - ✅ 详细的错误提示
3、发送通知(钉钉)
import { tool } from "@langchain/core/tools";
import { z } from "zod";
const sendDingTalk = tool(
async ({ message, urgency = "normal" }) => {
const webhookUrl = process.env.DINGTALK_WEBHOOK!;
const body = {
msgtype: "text",
text: {
content: urgency === "urgent" ? `【紧急】${message}` : message,
},
at: urgency === "urgent" ? { isAtAll: true } : {},
};
const response = await fetch(webhookUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (response.ok) {
return "通知发送成功";
}
return `通知发送失败:HTTP ${response.status}`;
},
{
name: "send_dingtalk_notification",
description: "向钉钉群发送通知消息。紧急消息会 @所有人。",
schema: z.object({
message: z.string().describe("通知内容"),
urgency: z.enum(["normal", "urgent"])
.optional()
.describe("紧急程度,urgent 会 @所有人,慎用"),
}),
}
);
七、工具设计的最佳实践
掌握这些最佳实践,能让你的工具更加可靠和易用。
💡 实践 1:工具描述要精确
LLM 完全依赖 description 来判断什么时候该用这个工具。
❌ 模糊的描述:
description: "搜索信息"
✅ 清晰的描述:
description: "使用 Google 搜索互联网上的最新信息,返回前 5 条搜索结果的标题、摘要和链接。适合查询最新事件、获取外部知识、验证事实信息。不适合查询本地文件或数据库内容。"
好的描述应该包含:
- 功能说明(做什么)
- 使用场景(何时用)
- 输出格式(返回什么)
- 限制条件(不能做什么)
💡 实践 2:参数要有 describe
Zod Schema 的 .describe() 说明会出现在发给 LLM 的工具定义中,直接影响 LLM 能否正确填写参数。
❌ 没有描述:
schema: z.object({ q: z.string() })
✅ 有清晰描述:
schema: z.object({
query: z.string().describe("搜索关键词,使用具体的搜索词,避免过于宽泛。例如用 'TypeScript 泛型教程' 而不是 'TypeScript'"),
language: z.enum(["zh", "en"]).optional().describe("结果语言,默认 zh(中文)"),
})
⚠️ 实践 3:返回适量信息
工具返回的内容会占用 LLM 的上下文窗口(Context Window)。
问题:
- 返回内容过多 → 消耗大量 Token(增加成本)
- 可能超出上下文限制
- 稀释重要信息,导致 LLM 忽略关键细节
解决方案:
- 在工具层做截断和过滤
- 只返回与任务相关的核心信息
- 提供分页或摘要功能
// ❌ 返回全部结果(可能有上千条)
return JSON.stringify(allResults);
// ✅ 只返回前 10 条,并告知总数
return JSON.stringify({
total: allResults.length,
results: allResults.slice(0, 10),
note: "仅显示前 10 条结果,如需更多请使用分页参数"
});
⚠️ 实践 4:工具命名要语义化
工具名称会影响 LLM 的理解。
❌ 不好的命名:
name: "func1"
name: "do_something"
✅ 好的命名:
name: "search_web"
name: "get_weather"
name: "calculate_expression"
命名规范:
- 使用动词 + 名词结构
- 用小写字母和下划线
- 名称要能准确反映功能
⚠️ 实践 5:幂等性设计
工具调用应该是幂等的(多次调用产生相同结果),或者明确标注副作用。
✅ 幂等的工具:
// 查询操作是幂等的
name: "search_database"
description: "查询数据(只读操作,不会产生副作用)"
⚠️ 有副作用的工具要明确标注:
name: "send_email"
description: "发送邮件(会产生实际动作,每次调用都会发送一封邮件)"
八、本章小结
工具系统是 AI Agent 的核心能力所在。这一章我们学习了:
📝 核心知识点回顾
| 知识点 | 关键要点 |
|---|---|
| 工具的本质 | LLM 决定调用,程序负责执行,形成"思考-行动-观察"循环 |
tool() API | 执行函数 + 元数据(name、description、schema) |
| 异步工具 | 处理 I/O 操作(网络请求、数据库查询) |
| 错误处理 | 工具内部处理 vs 中间件统一处理 vs 混合策略 |
| 动态工具选择 | 基于权限或上下文动态提供工具 |
| MCP 协议 | 连接外部工具生态的标准方式 |
| 最佳实践 | 精确的描述、参数说明、返回内容控制、幂等性设计 |
🎯 动手练习
尝试完成以下练习,巩固所学知识:
练习 1:创建搜索工具 创建一个网络搜索工具,使用免费的搜索引擎 API(如 DuckDuckGo 或 SerpAPI),返回前 3 条搜索结果的标题、摘要和链接。
练习 2:改进错误处理 为天气查询工具添加更详细的错误处理:
- 城市不存在
- API 超时
- 网络连接失败
- API 返回异常数据
测试每种情况下 Agent 的行为。
练习 3:动态工具权限 实现一个简单的权限系统:
- Guest 用户:只能使用搜索工具
- User 用户:可以使用搜索、计算器、天气
- Admin 用户:可以使用所有工具
练习 4:MCP 初体验 安装并运行官方的文件系统 MCP 服务器,让 Agent 能够读取和写入文件。测试以下场景:
- 读取 README.md 并总结
- 创建一个新文件并写入内容
- 尝试访问工作目录外的文件(应该被阻止)