为什么自定义 Tool 是必须的?
问一个问题:LangChain 内置的搜索、计算器等工具,真的能满足实际业务需求吗?答案通常是否定的。AI 真正需要调用的,是那些贴合你业务场景的“专属能力”。

举例来说,以下几个常见场景即可说明问题:
| 场景 | 内置工具 | 自定义工具 |
|---|---|---|
| 前端开发 | 无 | 代码格式化、组件自动生成、样式转换 |
| 数据分析 | 简单计算 | 读取本地 CSV、生成图表配置 |
| 项目管理 | 无 | 创建 Jira 任务、发送钉钉通知 |
| 内容创作 | 无 | 文章排版、SEO 检查、配图生成 |
可以看出,真正的价值往往隐藏在“内置工具所不具备”的领域。而 Tool 组件,正是将这种能力赋予 AI 的关键桥梁。
其核心价值十分明确:标准化接口使 AI 无需关注内部实现细节;可复用性支持一次封装到处使用;业务解耦让工具独立于对话流程,简化测试与维护;能力扩展则让 AI 能直接操作文件、数据库、外部 API,这正是 Agent 的精髓所在。
Tool 核心原理与设计规范详解
Tool 的本质究竟是什么?
简单来说,Tool 就是一个经过“标准化封装”的函数。然而,它与普通函数的最大区别在于,必须满足三个条件,才能让 AI 理解并主动调用:
- 明确的名称——AI 通过名称识别并选择要调用的工具。
- 清晰的描述——AI 根据描述判断何时应调用该工具。
- 规范的参数定义——AI 从用户输入中准确提取所需参数。
对比一下就明白了:
// 普通函数 —— AI 无法直接调用
function formatCode(code: string, indent: number) {
return code.trim();
}
// Tool —— 可以被 AI 调用
const codeFormatter = tool(
async ({ code, indentSize }) => formatCode(code, indentSize),
{
name: "code_formatter",
description: "格式化前端代码,支持 JS/TS/Vue",
schema: z.object({
code: z.string().describe("需要格式化的代码"),
indentSize: z.number().default(2).describe("缩进空格数"),
}),
}
);
Tool 封装规范指南
以下关键规范项建议牢记:
| 规范项 | 要求 | 示例 |
|---|---|---|
| 名称命名 | 小写字母加下划线,以动词开头 | get_user_info、format_date |
| 描述完整性 | 明确功能、适用场景及触发条件 | "当用户需要...时使用此工具" |
| 参数类型 | 使用 Zod 进行严格的类型定义,避免使用 any |
z.object({ id: z.string() }) |
| 参数描述 | 每个参数都应包含 .describe() 方法 |
z.string().describe("用户ID") |
| 返回值规范 | 统一返回字符串格式,复杂数据使用 JSON 序列化 | JSON.stringify(data) |
| 错误处理 | 使用 try-catch 包裹异常,返回用户友好的错误信息 | return "错误:xxx" |
Tool 与普通函数的根本区别
下表可助你快速理解两者的本质差异:
| 维度 | 普通函数 | Tool |
|---|---|---|
| 调用者 | 由开发者直接调用 | 由 AI 自主决定是否调用 |
| 输入 | 任意参数格式均可 | 必须满足 Zod Schema 定义 |
| 输出 | 可为任意类型 | 建议统一返回字符串 |
| 文档 | 注释信息,可选 | 必须包含 name 和 description |
| 错误处理 | 直接抛出异常 | 捕获异常后返回错误信息 |
| 可发现性 | 无 | AI 可通过描述了解其用途 |
前端 TypeScript 封装自定义 Tool 的完整步骤
基础 Tool 模板结构
直接给出代码,这是最通用的模板框架:
import { tool } from "@langchain/core/tools";
import { z } from "zod";
// 步骤1:定义参数 Schema
const MyToolSchema = z.object({
param1: z.string().describe("参数1的说明"),
param2: z.number().optional().describe("参数2的说明(可选)"),
});
// 步骤2:定义工具函数
const myTool = tool(
async (args: z.infer) => {
try {
// 步骤3:实现业务逻辑
const { param1, param2 } = args;
const result = await doSomething(param1, param2);
// 步骤4:返回结果(字符串格式)
return typeof result === "string" ? result : JSON.stringify(result);
} catch (error) {
// 步骤5:异常处理
return `工具执行失败:${error instanceof Error ? error.message : String(error)}`;
}
},
{
name: "my_tool_name", // 工具唯一标识
description: "工具功能描述,说明何时使用", // AI 理解依据
schema: MyToolSchema, // 参数定义
}
);
Tool 开发完成检查清单
开发完成后,建议对照以下清单逐一核查:
- 工具名称是否清晰体现其功能?
- 描述是否明确了触发条件?
- 参数 Schema 是否完整定义了各参数的类型与说明?
- 异常是否被正确捕获并返回用户友好的信息?
- 返回值是否为字符串类型?
- 是否已考虑空值与边界情况?
多场景下实用 Tool 实战案例
案例一:前端代码格式化工具的实现
// code-formatter.ts
import { tool } from "@langchain/core/tools";
import { z } from "zod";
// 模拟代码格式化(实际项目可集成 prettier)
function formatJa vaScript(code: string, indentSize: number = 2): string {
const indent = " ".repeat(indentSize);
// 简化的格式化逻辑(实际应使用 prettier.format)
return code
.split("n")
.map(line => line.trim())
.map(line => {
if (line.startsWith("}") || line.startsWith("]") || line.startsWith(")")) {
return line;
}
return indent + line;
})
.join("n");
}
export const codeFormatter = tool(
async ({ code, language, indentSize }) => {
try {
if (!code || code.trim().length === 0) {
return "错误:代码内容不能为空";
}
let formattedCode = code;
switch (language) {
case "ja vascript":
case "typescript":
formattedCode = formatJa vaScript(code, indentSize);
break;
case "json":
formattedCode = JSON.stringify(JSON.parse(code), null, indentSize);
break;
default:
return `暂不支持的语言: ${language},当前支持:ja vascript, typescript, json`;
}
return formattedCode;
} catch (error) {
if (error instanceof SyntaxError) {
return `代码语法错误:${error.message}`;
}
return `格式化失败:${error instanceof Error ? error.message : String(error)}`;
}
},
{
name: "code_formatter",
description: `格式化前端代码,使代码更美观易读。使用场景:用户提供未格式化的代码、粘贴的代码排版混乱、需要统一代码风格时。支持的语言:ja vascript, typescript, json`,
schema: z.object({
code: z.string().describe("需要格式化的原始代码"),
language: z
.enum(["ja vascript", "typescript", "json"])
.describe("代码语言类型"),
indentSize: z
.number()
.min(2)
.max(8)
.default(2)
.describe("缩进空格数,默认2"),
}),
}
);
案例二:本地文件读取工具的封装
// file-reader.ts
import { tool } from "@langchain/core/tools";
import { z } from "zod";
import fs from "fs/promises";
import path from "path";
export const fileReader = tool(
async ({ filePath, encoding = "utf-8" }) => {
try {
// 安全检查:防止路径遍历攻击
const resolvedPath = path.resolve(filePath);
if (!resolvedPath.startsWith(process.cwd())) {
return `错误:无法访问项目目录外的文件:${filePath}`;
}
// 检查文件是否存在
const stats = await fs.stat(resolvedPath).catch(() => null);
if (!stats) {
return `错误:文件不存在:${filePath}`;
}
// 限制文件大小(最大 1MB)
if (stats.size > 1024 * 1024) {
return `错误:文件过大(${(stats.size / 1024).toFixed(2)} KB),超过 1MB 限制`;
}
// 读取文件
const content = await fs.readFile(resolvedPath, encoding as BufferEncoding);
// 截断过长的内容
const maxLength = 10000;
if (content.length > maxLength) {
return content.slice(0, maxLength) + `nn... 内容已截断(总长度 ${content.length} 字符)`;
}
return content;
} catch (error) {
return `读取文件失败:${error instanceof Error ? error.message : String(error)}`;
}
},
{
name: "file_reader",
description: `读取本地文件内容。当用户需要查看文件内容、分析代码文件时使用。支持的文件类型:.txt, .js, .ts, .json, .md, .vue, .css, .html安全限制:只能读取项目目录内的文件,单文件最大 1MB`,
schema: z.object({
filePath: z.string().describe("文件路径,支持相对路径(如 ./src/index.ts)或绝对路径"),
encoding: z
.enum(["utf-8", "ascii"])
.default("utf-8")
.describe("文件编码,默认 utf-8"),
}),
}
);
案例三:网页内容抓取工具的设计
// web-fetcher.ts
import { tool } from "@langchain/core/tools";
import { z } from "zod";
// 模拟网页抓取(实际项目可使用 axios + cheerio)
async function fetchWebContent(url: string): Promise {
// 模拟请求延迟
await new Promise(resolve => setTimeout(resolve, 500));
// 模拟返回内容
return `示例网页 欢迎访问示例网站
这是一个模拟的网页内容,实际项目中应该使用 axios/fetch 请求真实 URL。
主要内容包括:前端开发教程、AI 应用案例、LangChain 学习笔记等。
`;
}
// 简单的 HTML 文本提取
function extractText(html: string): string {
return html
.replace(/
