课程目标
掌握 LangChain.js 的工具系统:StructuredTool 基类、tool() 工厂函数、DynamicTool / DynamicStructuredTool 动态工具、Zod schema 与 JSON Schema 的转换、工具调用的完整链路。
14.1 工具系统概览
"工具"是让 LLM 能够执行具体操作(搜索、计算、API 调用等)的机制。LangChain.js 的工具系统有三层:
StructuredTool (抽象基类)
├── Tool // 简单字符串输入的工具
│ └── DynamicTool // 运行时动态创建
└── DynamicStructuredTool // 运行时动态创建,支持复杂 schema
tool() 函数 // 推荐的创建方式(自动选择合适的类)
工具也是 Runnable——继承自 BaseLangChain(间接继承 Runnable),可以参与 pipe() 组合。
源码位置: libs/langchain-core/src/tools/index.ts
14.2 tool() 函数 — 推荐的创建方式
tool() 是创建工具的首选方式。它接受一个函数和配置对象,自动根据 schema 类型选择创建 DynamicTool 还是 DynamicStructuredTool。
14.2.1 基本用法
import { tool } from "@langchain/core/tools";
import { z } from "zod";
// 创建一个带 Zod schema 的结构化工具
const weatherTool = tool(
async ({ city, unit }) => {
// 参数类型由 Zod schema 自动推断
return `${city}: 25${unit === "celsius" ? "°C" : "°F"}, Sunny`;
},
{
name: "get_weather",
description: "Get the current weather for a city",
schema: z.object({
city: z.string().describe("The city name"),
unit: z.enum(["celsius", "fahrenheit"]).describe("Temperature unit"),
}),
}
);
// 直接调用
const result = await weatherTool.invoke({ city: "Beijing", unit: "celsius" });
// "Beijing: 25°C, Sunny"
14.2.2 简单字符串工具
如果不提供 schema 或 schema 是 z.string(),tool() 会创建 DynamicTool(接受字符串输入):
const searchTool = tool(
async (query) => {
return `Search results for: ${query}`;
},
{
name: "search",
description: "Search the web",
// 不提供 schema → 自动创建 DynamicTool
}
);
const result = await searchTool.invoke("LangChain.js tutorial");
14.2.3 schema 路由逻辑
tool() 函数内部的路由逻辑(index.ts 第 914-1025 行):
// 简化的路由决策
if (!fields.schema || isSimpleStringSchema || isStringJSONSchema) {
return new DynamicTool({ ... }); // 字符串输入
} else {
return new DynamicStructuredTool({ ... }); // 结构化输入
}
14.2.4 支持 JSON Schema
除了 Zod,tool() 也支持 JSON Schema:
const calcTool = tool(
async (input) => {
return String(eval(input.expression));
},
{
name: "calculator",
description: "Evaluate a math expression",
schema: {
type: "object" as const,
properties: {
expression: { type: "string", description: "Math expression to evaluate" },
},
required: ["expression"],
},
}
);
14.3 StructuredTool — 面向对象的工具定义
源码位置: libs/langchain-core/src/tools/index.ts 第 95-351 行
StructuredTool 是所有工具的抽象基类。需要子类实现三个抽象属性和一个抽象方法:
export abstract class StructuredTool<SchemaT = ToolInputSchemaBase, ...> extends BaseLangChain {
abstract name: string; // 工具名称
abstract description: string; // 工具描述(LLM 据此决定是否使用)
abstract schema: SchemaT; // 输入 schema(Zod 或 JSON Schema)
// 子类必须实现的核心方法
protected abstract _call(
arg: SchemaOutputT,
runManager?: CallbackManagerForToolRun,
parentConfig?: ToolRunnableConfig
): Promise<ToolOutputT> | AsyncGenerator<ToolEventT, ToolOutputT>;
}
14.3.1 自定义 StructuredTool
import { StructuredTool } from "@langchain/core/tools";
import { z } from "zod";
class CalculatorTool extends StructuredTool {
name = "calculator";
description = "Performs basic math calculations";
schema = z.object({
expression: z.string().describe("A math expression like '2 + 3'"),
});
async _call({ expression }: { expression: string }) {
try {
// 注意:实际项目中不要用 eval,这里仅为演示
return String(eval(expression));
} catch {
return "Invalid expression";
}
}
}
const calc = new CalculatorTool();
const result = await calc.invoke({ expression: "2 + 3 * 4" });
// "14"
14.3.2 invoke 方法的内部流程
invoke() → call() 是工具执行的核心路径(第 175-350 行):
- 输入解析:判断是
ToolCall还是直接输入,提取实际参数 - Schema 校验:用 Zod 或 JSON Schema 校验输入
- 回调管理:触发
handleToolStart事件 - 执行函数:调用
_call(),支持 Promise 和 AsyncGenerator - 输出格式化:根据
responseFormat和toolCallId决定返回 ToolMessage 还是原始值 - 触发回调:
handleToolEnd或handleToolError
14.3.3 关键配置项
// responseFormat: 控制输出格式
responseFormat?: "content" | "content_and_artifact";
// returnDirect: 设为 true 时,Agent 执行此工具后立即返回,不再循环
returnDirect = false;
// verboseParsingErrors: 输入校验失败时是否显示详细错误
verboseParsingErrors = false;
14.4 DynamicTool 与 DynamicStructuredTool
14.4.1 DynamicTool
接受字符串输入的动态工具,schema 自动设为 z.object({ input: z.string().optional() }):
import { DynamicTool } from "@langchain/core/tools";
const myTool = new DynamicTool({
name: "greeting",
description: "Generate a greeting",
func: async (input) => `Hello, ${input}!`,
});
14.4.2 DynamicStructuredTool
接受结构化输入的动态工具:
import { DynamicStructuredTool } from "@langchain/core/tools";
import { z } from "zod";
const myTool = new DynamicStructuredTool({
name: "format_date",
description: "Format a date in the specified format",
schema: z.object({
date: z.string().describe("ISO date string"),
format: z.enum(["short", "long"]).describe("Output format"),
}),
func: async ({ date, format }) => {
const d = new Date(date);
return format === "short"
? d.toLocaleDateString()
: d.toLocaleDateString("en-US", { weekday: "long", year: "numeric", month: "long", day: "numeric" });
},
});
14.5 Zod Schema 与工具描述的转换
LLM 在决定调用哪个工具时,需要看到工具的 JSON Schema 描述。框架会将 Zod schema 自动转换:
// Zod schema
z.object({
city: z.string().describe("The city name"),
unit: z.enum(["celsius", "fahrenheit"]).describe("Temperature unit"),
})
// 转换后的 JSON Schema(LLM 看到的)
{
"type": "object",
"properties": {
"city": { "type": "string", "description": "The city name" },
"unit": { "type": "string", "enum": ["celsius", "fahrenheit"], "description": "Temperature unit" }
},
"required": ["city", "unit"]
}
Zod v3/v4 双版本支持:框架通过 interopParseAsync 和相关工具类型(libs/langchain-core/src/utils/types/zod.ts)实现 Zod v3 和 v4 的兼容。
14.6 工具调用的完整链路
从 LLM 返回工具调用到最终得到结果,完整链路如下:
1. 用户消息传入 ChatModel(已 bindTools)
↓
2. LLM 返回 AIMessage,包含 tool_calls 字段
AIMessage { tool_calls: [{ name: "get_weather", args: { city: "Beijing" }, id: "call_123" }] }
↓
3. 框架解析 tool_calls,找到对应的工具实例
↓
4. 调用 tool.invoke(toolCall)
- invoke 内部检测输入是 ToolCall,提取 args 和 id
- 用 schema 校验 args
- 执行 _call(parsedArgs)
↓
5. 工具返回结果,框架包装为 ToolMessage
ToolMessage { content: "Beijing: 25°C", tool_call_id: "call_123", name: "get_weather" }
↓
6. ToolMessage 加入消息列表,再次传入 LLM
14.6.1 ToolCall 处理
工具支持直接传入 ToolCall 对象:
// 模拟 LLM 返回的 ToolCall
const toolCall = {
name: "get_weather",
args: { city: "Beijing", unit: "celsius" },
id: "call_123",
type: "tool_call" as const,
};
// 直接传给工具
const result = await weatherTool.invoke(toolCall);
// 返回 ToolMessage,自动带上 tool_call_id
14.6.2 输出格式化逻辑
_formatToolOutput 函数(第 1027-1062 行)决定返回值类型:
- 有
toolCallId→ 返回ToolMessage(含tool_call_id) - 无
toolCallId→ 返回原始值
14.6.3 content_and_artifact 模式
const toolWithArtifact = tool(
async ({ query }) => {
const data = { results: ["item1", "item2"], total: 2 };
// 返回 [content, artifact] 元组
return [`Found ${data.total} results`, data];
},
{
name: "search",
description: "Search for items",
schema: z.object({ query: z.string() }),
responseFormat: "content_and_artifact",
}
);
14.7 bindTools — 将工具绑定到模型
import { ChatOpenAI } from "@langchain/openai";
const model = new ChatOpenAI({ model: "gpt-4o-mini" });
// 将工具绑定到模型
const modelWithTools = model.bindTools([weatherTool, calcTool]);
// 调用时,LLM 可以选择使用这些工具
const response = await modelWithTools.invoke("What's the weather in Beijing?");
// response.tool_calls → [{ name: "get_weather", args: { city: "Beijing", ... } }]
bindTools() 在 BaseChatModel 上定义(第 11 课),它将工具的 schema 描述附加到 API 请求中。
14.8 ToolInputParsingException
源码位置: libs/langchain-core/src/tools/utils.ts
当工具输入不匹配 schema 时抛出的专用异常:
export class ToolInputParsingException extends Error {
output?: string;
constructor(message: string, output?: string) {
super(message);
this.output = output; // 保留原始输入,方便 Agent 重试
}
}
这个异常与普通 Error 的区别在于:Agent 可以识别它,将错误信息反馈给 LLM 让其修正参数后重试。
14.9 BaseToolkit — 工具集合
export abstract class BaseToolkit {
abstract tools: StructuredToolInterface[];
getTools(): StructuredToolInterface[] {
return this.tools;
}
}
Toolkit 是工具的容器,用于将相关工具分组。例如数据库 Toolkit 可以包含查询、插入、更新等工具。
14.10 实战练习
练习 1:创建三个工具并绑定到模型
import { tool } from "@langchain/core/tools";
import { z } from "zod";
const weatherTool = tool(
async ({ city }) => `${city}: 25°C, Sunny`,
{
name: "get_weather",
description: "Get current weather for a city",
schema: z.object({ city: z.string() }),
}
);
const calculatorTool = tool(
async ({ expression }) => String(eval(expression)),
{
name: "calculator",
description: "Calculate a math expression",
schema: z.object({ expression: z.string() }),
}
);
const searchTool = tool(
async (query) => `Results for: ${query}`,
{
name: "web_search",
description: "Search the web for information",
}
);
// 绑定到模型
const modelWithTools = model.bindTools([weatherTool, calculatorTool, searchTool]);
练习 2:处理完整的工具调用流程
import { HumanMessage, AIMessage, ToolMessage } from "@langchain/core/messages";
// 第一轮:发送问题
const response = await modelWithTools.invoke([
new HumanMessage("What is the weather in Tokyo and what is 15 * 23?"),
]);
// 第二轮:执行工具调用,收集结果
const toolMessages: ToolMessage[] = [];
for (const toolCall of response.tool_calls ?? []) {
const selectedTool = [weatherTool, calculatorTool, searchTool]
.find((t) => t.name === toolCall.name);
if (selectedTool) {
const result = await selectedTool.invoke(toolCall);
toolMessages.push(result);
}
}
// 第三轮:将工具结果传回模型
const finalResponse = await modelWithTools.invoke([
new HumanMessage("What is the weather in Tokyo and what is 15 * 23?"),
response,
...toolMessages,
]);
14.11 源码精读路线
| 优先级 | 文件 | 关注点 |
|---|---|---|
| P0 | tools/index.ts | StructuredTool 类、tool() 函数、DynamicStructuredTool |
| P0 | tools/types.ts | ToolParams, StructuredToolInterface, 类型定义 |
| P1 | tools/utils.ts | ToolInputParsingException, _isToolCall 判断逻辑 |
| P1 | messages/tool.ts | ToolMessage, ToolCall 类型 |
| P2 | utils/types/zod.ts | Zod v3/v4 兼容层 |
| P2 | utils/json_schema.ts | JSON Schema 相关工具函数 |
本课收获总结
| 级别 | 你应该掌握的 |
|---|---|
| 🟢 基础 | 学会用 tool() 函数和 StructuredTool 类创建工具 |
| 🔵 中阶 | 理解 Zod schema 如何转化为 LLM 可理解的工具描述 |
| 🟡 高阶 | 掌握 DynamicTool 和 DynamicStructuredTool 的动态工具创建 |
| 🟠 资深 | 分析工具调用的完整链路:schema 校验 → LLM tool_calls → 执行 → ToolMessage → 反馈 |
| 🔴 架构 | 设计工具注册与发现机制、BaseToolkit 分组、权限控制与调用审计策略 |
下一课预告
第 15 课讲 Output Parsers 结构化输出解析——StringOutputParser、JsonOutputParser、StructuredOutputParser,以及流式输出的 transform 机制。