LangChain 自定义 Tool 封装:打造专属 AI 能力工具集

为什么需要自定义 Tool?

从通用到专属

LangChain 内置的工具(如搜索、计算器)能满足基本需求,但在实际业务中,我们往往需要 AI 调用专属于自己业务场景的能力:

场景 内置工具 自定义工具
前端开发 代码格式化、组件生成、样式转换
数据分析 简单计算 读取本地 CSV、生成图表配置
项目管理 创建 Jira 任务、发送钉钉通知
内容创作 文章排版、SEO 检查、配图生成

Tool 组件的核心价值

  • 标准化接口:统一的输入输出规范,AI 无需关心内部实现
  • 可复用性:一次封装,多处调用
  • 业务解耦:工具独立于对话流程,易于测试和维护
  • 能力扩展:让 AI 能够操作外部系统(文件、数据库、API)

Tool 核心原理与设计规范

Tool 的本质是什么?

Tool(工具)本质上是一个被标准化包装的函数,它需要满足三个条件才能被 AI 理解和调用:

  1. 有明确的名称:AI 通过名称选择要调用的工具
  2. 有清晰的描述:AI 根据描述判断何时该调用此工具
  3. 有规范的参数定义: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_infoformat_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 与你的业务系统深度集成。

对于文章中错误的地方或有任何疑问,欢迎在评论区留言讨论!

相关推荐
鱼人1 小时前
Vue 3 组合式 API 最佳实践:如何写出可维护的代码
前端
长大19881 小时前
彻底搞懂 JavaScript 事件循环
前端
橘猫走江湖1 小时前
Web 前端本地存储:localStorage 与 IndexedDB
前端·javascript·indexeddb
小强19881 小时前
CSS 布局进化史:从 Float 到 Flexbox 再到 Grid
前端
AKA__老方丈1 小时前
删除确认 Hook - 统一管理单删/批量删除的确认弹窗与执行
前端·javascript·vue.js
假如让我当三天老蒯1 小时前
React+TS 项目结构(自学项目用)
前端·react.js
yingyima1 小时前
Celery 分布式任务队列:我差点被这行代码坑死
前端
用户125758524361 小时前
XYGo Admin 即时通讯模块解析:基于 WebSocket 的企业级消息架构实践
前端
铁皮饭盒2 小时前
彩色命令行,Node21自带函数1行实现 ,Bun也兼容, 附Bun.color实现渐变色的代码
前端·后端