为什么需要自定义 Tool?
从通用到专属
LangChain 内置的工具(如搜索、计算器)能满足基本需求,但在实际业务中,我们往往需要 AI 调用专属于自己业务场景的能力:
| 场景 | 内置工具 | 自定义工具 |
|---|---|---|
| 前端开发 | 无 | 代码格式化、组件生成、样式转换 |
| 数据分析 | 简单计算 | 读取本地 CSV、生成图表配置 |
| 项目管理 | 无 | 创建 Jira 任务、发送钉钉通知 |
| 内容创作 | 无 | 文章排版、SEO 检查、配图生成 |
Tool 组件的核心价值
- 标准化接口:统一的输入输出规范,AI 无需关心内部实现
- 可复用性:一次封装,多处调用
- 业务解耦:工具独立于对话流程,易于测试和维护
- 能力扩展:让 AI 能够操作外部系统(文件、数据库、API)
Tool 核心原理与设计规范
Tool 的本质是什么?
Tool(工具)本质上是一个被标准化包装的函数,它需要满足三个条件才能被 AI 理解和调用:
- 有明确的名称:AI 通过名称选择要调用的工具
- 有清晰的描述:AI 根据描述判断何时该调用此工具
- 有规范的参数定义:AI 从用户输入中提取参数
typescript
// 普通函数 vs Tool
// ❌ 普通函数 - 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 可通过描述理解用途 |
前端 TS 封装自定义 Tool 完整步骤
基础 Tool 模板
typescript
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<typeof MyToolSchema>) => {
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 实战案例
案例一:前端代码格式化工具
typescript
// code-formatter.ts
import { tool } from "@langchain/core/tools";
import { z } from "zod";
// 模拟代码格式化(实际项目可集成 prettier)
function formatJavaScript(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 "javascript":
case "typescript":
formattedCode = formatJavaScript(code, indentSize);
break;
case "json":
formattedCode = JSON.stringify(JSON.parse(code), null, indentSize);
break;
default:
return `暂不支持的语言: ${language},当前支持:javascript, 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: `格式化前端代码,使代码更美观易读。
使用场景:用户提供未格式化的代码、粘贴的代码排版混乱、需要统一代码风格时。
支持的语言:javascript, typescript, json`,
schema: z.object({
code: z.string().describe("需要格式化的原始代码"),
language: z.enum(["javascript", "typescript", "json"]).describe("代码语言类型"),
indentSize: z.number().min(2).max(8).default(2).describe("缩进空格数,默认2"),
}),
}
);
案例二:本地文件读取工具
typescript
// 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) + `\n\n... 内容已截断(总长度 ${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"),
}),
}
);
案例三:网页内容抓取工具
typescript
// web-fetcher.ts
import { tool } from "@langchain/core/tools";
import { z } from "zod";
// 模拟网页抓取(实际项目可使用 axios + cheerio)
async function fetchWebContent(url: string): Promise<string> {
// 模拟请求延迟
await new Promise(resolve => setTimeout(resolve, 500));
// 模拟返回内容
return `
<title>示例网页</title>
<h1>欢迎访问示例网站</h1>
<p>这是一个模拟的网页内容,实际项目中应该使用 axios/fetch 请求真实 URL。</p>
<div class="content">
主要内容包括:前端开发教程、AI 应用案例、LangChain 学习笔记等。
</div>
`;
}
// 简单的 HTML 文本提取
function extractText(html: string): string {
return html
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "")
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "")
.replace(/<[^>]+>/g, " ")
.replace(/\s+/g, " ")
.trim();
}
export const webFetcher = tool(
async ({ url, extractMainContent = true }) => {
try {
// URL 格式验证
try {
new URL(url);
} catch {
return `错误:无效的 URL 格式:${url}`;
}
// 限制域名范围(可选,防止访问危险网站)
const allowedDomains = process.env.ALLOWED_DOMAINS?.split(",") || [];
const urlObj = new URL(url);
if (allowedDomains.length > 0 && !allowedDomains.includes(urlObj.hostname)) {
return `错误:不允许访问域名 ${urlObj.hostname},仅支持:${allowedDomains.join(", ")}`;
}
// 抓取网页内容
const html = await fetchWebContent(url);
// 提取纯文本
let content = extractMainContent ? extractText(html) : html;
// 截断过长内容
const maxLength = 5000;
if (content.length > maxLength) {
content = content.slice(0, maxLength) + `\n\n... 内容已截断`;
}
return content;
} catch (error) {
return `抓取网页失败:${error instanceof Error ? error.message : String(error)}`;
}
},
{
name: "web_fetcher",
description: `抓取网页内容并提取主要文本。当用户需要了解某个网页的内容、获取在线文档信息时使用。
注意:这是一个模拟实现,生产环境需要配置实际请求能力。`,
schema: z.object({
url: z.string().describe("需要抓取的网页 URL,格式如 https://example.com"),
extractMainContent: z.boolean().default(true).describe("是否提取主要文本内容(去除HTML标签),默认 true"),
}),
}
);
案例四:数据转换工具(CSV/JSON 互转)
typescript
// data-converter.ts
import { tool } from "@langchain/core/tools";
import { z } from "zod";
export const dataConverter = tool(
async ({ input, fromFormat, toFormat }) => {
try {
let result: string;
// JSON 转 CSV
if (fromFormat === "json" && toFormat === "csv") {
const data = JSON.parse(input);
if (!Array.isArray(data) || data.length === 0) {
return "错误:JSON 必须是非空数组";
}
const headers = Object.keys(data[0]);
const rows = data.map(obj =>
headers.map(header => {
let value = obj[header];
if (value === undefined) return "";
if (typeof value === "object") return JSON.stringify(value);
return String(value).includes(",") ? `"${value}"` : value;
}).join(",")
);
result = [headers.join(","), ...rows].join("\n");
}
// CSV 转 JSON
else if (fromFormat === "csv" && toFormat === "json") {
const lines = input.trim().split("\n");
const headers = lines[0].split(",").map(h => h.trim());
const records = lines.slice(1).map(line => {
const values = line.split(",").map(v => v.trim().replace(/^"|"$/g, ""));
return headers.reduce((obj, header, idx) => {
obj[header] = values[idx] || "";
return obj;
}, {} as Record<string, string>);
});
result = JSON.stringify(records, null, 2);
}
else {
return `错误:不支持的转换类型 ${fromFormat} -> ${toFormat},支持:json->csv 或 csv->json`;
}
return result;
} catch (error) {
if (error instanceof SyntaxError) {
return `错误:JSON 格式解析失败 - ${error.message}`;
}
return `转换失败:${error instanceof Error ? error.message : String(error)}`;
}
},
{
name: "data_converter",
description: `数据格式转换工具,支持 JSON 和 CSV 之间的互相转换。
使用场景:用户需要将 JSON 数据导出为表格格式,或将 CSV 数据转换为可读的 JSON 结构。`,
schema: z.object({
input: z.string().describe("需要转换的原始数据内容"),
fromFormat: z.enum(["json", "csv"]).describe("源数据格式"),
toFormat: z.enum(["json", "csv"]).describe("目标数据格式"),
}),
}
);
Tool 调用异常处理方案
完整的异常处理框架
typescript
// robust-tool.ts
import { tool } from "@langchain/core/tools";
import { z } from "zod";
// 带超时的 Promise 包装器
function withTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T> {
return Promise.race([
promise,
new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error(`操作超时(${timeoutMs}ms)`)), timeoutMs)
),
]);
}
// 定义返回类型(包含执行状态)
interface ToolResult {
success: boolean;
data?: any;
error?: string;
executionTime?: number;
}
// 示例:带完整异常处理的工具
export const robustTool = tool(
async (args: any) => {
const startTime = Date.now();
try {
// 1. 参数校验(Zod 已做,此处可补充业务校验)
// 2. 执行核心逻辑(带超时)
const result = await withTimeout(doBusinessLogic(args), 30000);
// 3. 返回成功结果
const toolResult: ToolResult = {
success: true,
data: result,
executionTime: Date.now() - startTime,
};
return JSON.stringify(toolResult);
} catch (error) {
// 4. 分类错误处理
let errorMessage: string;
if (error instanceof Error) {
if (error.message.includes("超时")) {
errorMessage = `工具执行超时:${error.message}`;
} else if (error.message.includes("ECONNREFUSED")) {
errorMessage = "网络连接失败,请检查网络后重试";
} else {
errorMessage = `执行失败:${error.message}`;
}
} else {
errorMessage = `未知错误:${String(error)}`;
}
const toolResult: ToolResult = {
success: false,
error: errorMessage,
executionTime: Date.now() - startTime,
};
return JSON.stringify(toolResult);
}
},
{
name: "robust_tool",
description: "带完整异常处理的工具示例",
schema: z.object({}),
}
);
async function doBusinessLogic(args: any): Promise<any> {
// 模拟业务逻辑
return { status: "ok", data: "处理完成" };
}
异常类型与处理策略
| 异常类型 | 常见原因 | 处理策略 | 返回信息 |
|---|---|---|---|
| 参数校验错误 | 缺少必填参数、类型错误 | Zod 自动校验 + 友好提示 | "参数错误:xxx 是必填字段" |
| 网络超时 | 请求耗时过长 | 设置超时、重试机制 | "请求超时,请稍后重试" |
| 资源不存在 | 文件/API 找不到 | 检查路径、提供建议 | "文件不存在:xxx,请检查路径" |
| 权限不足 | 无权访问资源 | 返回明确错误 | "权限不足,无法访问 xxx" |
| 数据格式错误 | JSON 解析失败 | 提示正确格式 | "JSON 格式错误,请检查:xxx" |
| 限流/配额 | 调用次数过多 | 返回限流信息 | "调用频率过高,请稍后再试" |
工具集封装思路
工具分类管理
typescript
// tools/index.ts
import { codeFormatter } from "./code-formatter";
import { fileReader } from "./file-reader";
import { webFetcher } from "./web-fetcher";
import { dataConverter } from "./data-converter";
// 按功能分类
export const devTools = {
codeFormatter,
// gitHelper, // Git 操作工具
// npmSearch, // NPM 包搜索
};
export const dataTools = {
dataConverter,
// csvParser,
// jsonValidator,
};
export const networkTools = {
webFetcher,
// httpRequest,
// apiCaller,
};
export const fileTools = {
fileReader,
// fileWriter,
// directoryLister,
};
// 统一获取所有工具
export function getAllTools() {
return [
...Object.values(devTools),
...Object.values(dataTools),
...Object.values(networkTools),
...Object.values(fileTools),
];
}
// 按场景获取工具
export function getToolsForScenario(scenario: "frontend-dev" | "data-processing" | "web-scraping") {
switch (scenario) {
case "frontend-dev":
return Object.values(devTools);
case "data-processing":
return Object.values(dataTools);
case "web-scraping":
return Object.values(networkTools);
default:
return getAllTools();
}
}
动态工具注册
typescript
// tools/registry.ts
interface ToolRegistry {
[key: string]: ReturnType<typeof tool>;
}
class ToolManager {
private tools: ToolRegistry = {};
// 注册工具
register(tool: ReturnType<typeof tool>): void {
const name = tool.name;
if (this.tools[name]) {
console.warn(`工具 ${name} 已存在,将被覆盖`);
}
this.tools[name] = tool;
}
// 批量注册
registerAll(tools: ReturnType<typeof tool>[]): void {
tools.forEach(t => this.register(t));
}
// 获取单个工具
get(name: string): ReturnType<typeof tool> | undefined {
return this.tools[name];
}
// 获取所有工具
getAll(): ReturnType<typeof tool>[] {
return Object.values(this.tools);
}
// 按名称模式匹配
search(pattern: RegExp): ReturnType<typeof tool>[] {
return Object.entries(this.tools)
.filter(([name]) => pattern.test(name))
.map(([, tool]) => tool);
}
}
export const toolManager = new ToolManager();
可扩展的工具基类
typescript
// tools/base-tool.ts
import { tool } from "@langchain/core/tools";
import { z, ZodObject } from "zod";
// 工具配置接口
interface ToolConfig {
name: string;
description: string;
schema: ZodObject<any>;
timeout?: number;
retries?: number;
}
// 抽象工具类
export abstract class BaseTool {
protected config: ToolConfig;
constructor(config: ToolConfig) {
this.config = config;
}
// 子类需要实现的核心逻辑
protected abstract execute(args: any): Promise<string>;
// 前置钩子
protected beforeExecute(args: any): void {
console.log(`[Tool] 开始执行 ${this.config.name},参数:`, args);
}
// 后置钩子
protected afterExecute(result: string): void {
console.log(`[Tool] 执行完成 ${this.config.name},结果长度:${result.length}`);
}
// 错误处理钩子
protected onError(error: Error, args: any): string {
return `${this.config.name} 执行失败:${error.message}`;
}
// 构建 LangChain Tool
build() {
const self = this;
return tool(
async (args: any) => {
self.beforeExecute(args);
try {
const result = await self.execute(args);
self.afterExecute(result);
return result;
} catch (error) {
return self.onError(error as Error, args);
}
},
{
name: this.config.name,
description: this.config.description,
schema: this.config.schema,
}
);
}
}
// 使用示例
class CalculatorTool extends BaseTool {
constructor() {
super({
name: "calculator",
description: "数学计算工具",
schema: z.object({
expression: z.string(),
}),
});
}
protected async execute(args: { expression: string }): Promise<string> {
// 安全计算
const result = Function('"use strict";return (' + args.expression + ')')();
return result.toString();
}
}
// 注册使用
const calculator = new CalculatorTool().build();
完整实战 - 智能代码助手
typescript
import { ChatOpenAI } from "@langchain/openai";
import { StateGraph, MessagesAnnotation, START, END } from "@langchain/langgraph";
import { HumanMessage, ToolMessage } from "@langchain/core/messages";
import dotenv from "dotenv";
import { codeFormatter } from "./code-formatter.ts";
import { fileReader } from "./file-reader.ts";
import { dataConverter } from "./data-converter.ts";
dotenv.config();
async function smartCodeAssistant() {
// 1. 初始化模型
const model = new ChatOpenAI({
apiKey: process.env.DASHSCOPE_API_KEY,
configuration: {
baseURL: process.env.DASHSCOPE_API_URL,
},
model: "qwen-plus",
temperature: 0.3,
});
// 工具列表
const tools = [codeFormatter, fileReader, dataConverter];
const modelWithTools = model.bindTools(tools);
// -----------------------
// 节点1:AI 思考是否调用工具
// -----------------------
async function callModel(state: typeof MessagesAnnotation.State) {
const response = await modelWithTools.invoke(state.messages);
return { messages: [response] };
}
// -----------------------
// 节点2:执行工具(核心修复)
// -----------------------
async function executeTools(state: typeof MessagesAnnotation.State) {
const lastMessage = state.messages[state.messages.length - 1];
// 如果没有工具调用,直接返回
if (!("tool_calls" in lastMessage) || !lastMessage.tool_calls) {
return { messages: [] };
}
// 执行所有工具调用
const toolResults = [];
for (const toolCall of lastMessage.tool_calls) {
console.log(`\n🔧 正在执行工具:${toolCall.name}`);
console.log(`📥 参数:${JSON.stringify(toolCall.args)}`);
// 找到对应的工具
const tool = tools.find((t) => t.name === toolCall.name);
let result;
if (tool) {
try {
result = await tool.invoke(toolCall.args);
} catch (e) {
result = `工具执行失败:${e}`;
}
} else {
result = "未知工具";
}
console.log(`✅ 工具返回:${result}`);
toolResults.push(
new ToolMessage({
content: result,
tool_call_id: toolCall.id,
})
);
}
return { messages: toolResults };
}
// -----------------------
// 构建工作流(支持自动工具调用)
// -----------------------
const workflow = new StateGraph(MessagesAnnotation)
.addNode("model", callModel)
.addNode("executeTools", executeTools) // 添加工具执行节点
.addEdge(START, "model")
.addEdge("model", "executeTools") // AI → 执行工具
.addEdge("executeTools", "model") // 执行完 → 让AI总结结果
.addEdge("executeTools", END); // 结束
const app = workflow.compile();
// =======================
// 用户提问
// =======================
const userRequest = `
请帮我做以下事情:
1. 读取当前目录下的 package.json 文件
2. 提取其中的依赖信息
3. 将依赖信息转换为表格格式
`;
console.log(`👤 用户: ${userRequest}\n`);
console.log("🔄 执行中...\n");
// 运行
const result = await app.invoke({
messages: [new HumanMessage(userRequest)],
});
// 输出最终回答
const finalAnswer = result.messages.at(-1);
console.log("\n🤖 最终回答:\n", finalAnswer?.content);
}
smartCodeAssistant();
结语
通过这篇教程,我们深入学习了 LangChain 中自定义 Tool 的完整开发流程。Tool 是让 AI 具备"动手能力"的关键,掌握它就能将 AI 与你的业务系统深度集成。
对于文章中错误的地方或有任何疑问,欢迎在评论区留言讨论!