第 14 课: Tools — 工具系统

0 阅读4分钟

课程目标

掌握 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 行):

  1. 输入解析:判断是 ToolCall 还是直接输入,提取实际参数
  2. Schema 校验:用 Zod 或 JSON Schema 校验输入
  3. 回调管理:触发 handleToolStart 事件
  4. 执行函数:调用 _call(),支持 Promise 和 AsyncGenerator
  5. 输出格式化:根据 responseFormattoolCallId 决定返回 ToolMessage 还是原始值
  6. 触发回调handleToolEndhandleToolError

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 源码精读路线

优先级文件关注点
P0tools/index.tsStructuredTool 类、tool() 函数、DynamicStructuredTool
P0tools/types.tsToolParams, StructuredToolInterface, 类型定义
P1tools/utils.tsToolInputParsingException, _isToolCall 判断逻辑
P1messages/tool.tsToolMessage, ToolCall 类型
P2utils/types/zod.tsZod v3/v4 兼容层
P2utils/json_schema.tsJSON Schema 相关工具函数

本课收获总结

级别你应该掌握的
🟢 基础学会用 tool() 函数和 StructuredTool 类创建工具
🔵 中阶理解 Zod schema 如何转化为 LLM 可理解的工具描述
🟡 高阶掌握 DynamicToolDynamicStructuredTool 的动态工具创建
🟠 资深分析工具调用的完整链路:schema 校验 → LLM tool_calls → 执行 → ToolMessage → 反馈
🔴 架构设计工具注册与发现机制、BaseToolkit 分组、权限控制与调用审计策略

下一课预告

第 15 课讲 Output Parsers 结构化输出解析——StringOutputParserJsonOutputParserStructuredOutputParser,以及流式输出的 transform 机制。