1. 为什么需要封装工具集?
在AI应用开发中,我们经常需要让AI系统能够执行各种具体任务,比如读写文件、查询数据库、调用API等。传统的手工编码方式虽然可行,但随着任务复杂度的提升,这种方式会暴露出诸多问题。
1.1 手工编码方式的痛点
假设我们要开发一个文件管理AI助手,传统实现方式可能是这样的:
javascript复制// 每个工具都要重复写验证逻辑
const readFile = async (path: string) => {
if (!path) throw new Error("缺少路径");
if (typeof path !== "string") throw new Error("路径必须是字符串");
try {
return await fs.readFile(path, "utf-8");
} catch (err) {
return "读取失败";
}
};
const writeFile = async (path: string, content: string) => {
if (!path) throw new Error("缺少路径");
if (typeof content !== "string") throw new Error("内容必须是字符串");
// ... 重复的代码
};
这种方式存在几个明显问题:
- 代码重复:每个工具都需要自己实现参数验证、错误处理等基础逻辑
- 接口不一致:不同开发者实现的工具可能有不同的调用方式和返回格式
- AI集成困难:需要手动适配AI模型的输入输出格式
- 维护成本高:当需要修改验证规则或错误处理逻辑时,需要修改多处代码
1.2 LangChain Tools的价值主张
LangChain Tools提供了一套标准化的工具封装方案,可以完美解决上述问题。通过统一的接口规范,开发者可以:
- 专注业务逻辑:只需实现核心功能,基础验证由框架处理
- 统一接口:所有工具遵循相同的调用规范
- 自动适配AI:工具描述和参数会自动转换为AI可理解的格式
- 内置安全机制:提供参数验证、错误处理等基础能力
javascript复制const readFileTool = new DynamicStructuredTool({
name: "read_file",
description: "读取文件内容",
schema: z.object({ path: z.string() }),
func: async ({ path }) => fs.readFile(path, "utf-8")
});
1.3 传统方案与LangChain方案对比
| 问题点 | 传统手工方案 | LangChain方案 |
|---|---|---|
| 参数验证 | 每个工具重复实现验证逻辑 | 使用Zod Schema统一验证 |
| 错误处理 | 格式不统一,AI难以理解 | 自动格式化错误信息 |
| AI集成 | 需要手动解析AI输出和格式化返回 | 自动适配AI输入输出 |
| 类型安全 | 需要手动维护TypeScript类型定义 | 基于Zod Schema自动推导类型 |
| 可测试性 | 需要mock复杂上下文 | 标准接口易于测试 |
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Tool基础:从零开始封装工具
2.1 Tool的核心结构
LangChain中的工具由三个核心要素组成:
- name:工具的唯一标识符,AI通过它来选择调用哪个工具
- description:工具的功能描述,AI通过它来判断何时使用该工具
- func:实际执行的函数,包含工具的核心业务逻辑
2.2 最简单的工具:DynamicTool
DynamicTool是最基础的工具类型,适合不需要复杂参数的工具:
javascript复制import { DynamicTool } from "@langchain/core/tools";
const simpleTool = new DynamicTool({
name: "get_time",
description: "获取当前时间",
func: async () => {
return new Date().toLocaleString();
}
});
// 使用示例
const result = await simpleTool.invoke("");
console.log("当前时间:", result);
注意:即使不需要参数,DynamicTool的invoke方法也必须接收一个字符串参数,这是LangChain的接口规范。
2.3 带参数的工具
对于需要接收用户输入的工具,可以使用带参数的DynamicTool:
javascript复制const echoTool = new DynamicTool({
name: "echo",
description: "返回用户输入的内容",
func: async (input: string) => {
return `你说的是: ${input}`;
}
});
// 使用示例
const result = await echoTool.invoke("Hello World!");
console.log(result); // 输出: 你说的是: Hello World!
这种方式的局限性在于:
- 只能接收单个字符串参数
- 缺乏参数验证能力
- 不适合需要多个结构化参数的场景
2.4 带参数验证的工具:DynamicStructuredTool
对于需要多个结构化参数的工具,推荐使用DynamicStructuredTool配合Zod进行参数验证:
javascript复制import { DynamicStructuredTool } from "@langchain/core/tools";
import { z } from "zod";
const weatherTool = new DynamicStructuredTool({
name: "get_weather",
description: "获取指定城市的天气信息,返回温度、天气状况",
schema: z.object({
city: z.string().describe("城市名称,如:北京、上海"),
unit: z.enum(["celsius", "fahrenheit"]).optional().describe("温度单位")
}),
func: async ({ city, unit = "celsius" }) => {
// 模拟天气数据
const weatherData: Record<string, any> = {
"北京": { temp: 22, condition: "晴" },
"上海": { temp: 18, condition: "雨" },
"武汉": { temp: 25, condition: "阴" }
};
const data = weatherData[city];
if (!data) {
return `未找到城市 "${city}" 的天气信息`;
}
const temp = unit === "celsius" ? `${data.temp}°C` : `${data.temp * 9/5 + 32}°F`;
return `${city}今天${data.condition},温度${temp}`;
}
});
// 使用示例
const result = await weatherTool.invoke({ city: "北京" });
console.log(result); // 输出: 北京今天晴,温度22°C
2.5 为什么需要参数验证?
参数验证是工具可靠性的重要保障,对比有无验证的情况:
| 场景 | 无验证的后果 | 有验证的优势 |
|---|---|---|
| 参数类型错误 | 工具内部抛出难以理解的异常 | 自动拦截并提供友好错误信息 |
| 必填参数缺失 | 需要工具内部手动检查 | Zod自动验证必填字段 |
| 参数格式不规范 | 可能导致后续处理出错 | 统一格式化后再处理 |
| AI传参错误 | AI难以理解失败原因 | AI能根据验证错误调整参数 |
3. 深入DynamicStructuredTool
3.1 Zod Schema详解
Zod是一个强大的TypeScript-first的schema验证库,LangChain使用它来定义工具参数的结构和验证规则。
3.1.1 基础类型定义
javascript复制const basicSchema = z.object({
name: z.string(), // 字符串
age: z.number(), // 数字
isActive: z.boolean(), // 布尔值
tags: z.arr
