来源:互联网 更新时间:2026-06-05 07:23
先问个问题:LangChain 内置的那些搜索、计算器工具,真的够用吗?在真实业务里,答案往往是不够。AI 需要调用的,应该是那些真正属于你业务场景的“专属能力”。

举个例子,几条常见的需求场景就能说明问题:
| 场景 | 内置工具 | 自定义工具 |
|---|---|---|
| 前端开发 | 无 | 代码格式化、组件生成、样式转换 |
| 数据分析 | 简单计算 | 读取本地 CSV、生成图表配置 |
| 项目管理 | 无 | 创建 Jira 任务、发送钉钉通知 |
| 内容创作 | 无 | 文章排版、SEO 检查、配图生成 |
看到了吧,真正的价值往往藏在那些“内置工具没有”的地方。而 Tool 组件,正是把这种能力交到 AI 手里的桥梁。
它的核心价值其实很清晰:标准化接口,让 AI 完全不用关心内部怎么实现的;可复用性,封装一次到处能用;业务解耦,工具独立于对话流程,测试和维护都变得简单;还有能力扩展——让 AI 直接操作文件、数据库、外部 API,这才是 Agent 的灵魂。
说穿了,Tool 就是一个被“标准化包装”的函数。但它跟普通函数最大的区别在于,它需要满足三个条件,才能让 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("缩进空格数"),
}),
}
);
几个关键的规范项,最好记一下:
| 规范项 | 要求 | 示例 |
|---|---|---|
| 名称命名 | 小写+下划线,动词开头 | 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 |
|---|---|---|
| 调用者 | 开发者直接调用 | AI 自主决定调用 |
| 输入 | 任意参数格式 | 必须符合 Zod Schema |
| 输出 | 任意类型 | 建议返回字符串 |
| 文档 | 注释(可选) | 必须有 name + description |
| 错误处理 | 抛出异常 | 捕获异常,返回错误信息 |
| 可发现性 | 无 | AI 可通过描述理解用途 |
直接上代码,这是最通用的模板:
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, // 参数定义
}
);
开发完成后,不妨对照这个清单自查一遍:
// 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(/<script[^>]*>[sS]*?<\/script>/gi, "")
.replace(/