工具扩展了 Agent(Agent我们下一小节介绍)的功能------使它们能够获取实时数据、执行代码、查询外部数据库并在现实世界中采取行动。在底层,工具是具有明确定义输入和输出的可调用函数,这些函数会被传递给聊天模型。模型会根据对话上下文决定何时调用工具以及需要提供哪些输入参数。Tool 是让 Agent 能够与外部世界交互的核心组件,本质上是有明确输入输出定义的可调用函数。模型根据对话上下文决定何时调用工具以及传入什么参数。Tool 由三部分组成:名称、描述和参数 schema(使用 Zod 定义)。模型读取这些信息来理解工具的用途并正确调用。在 LLM 应用开发中,Tool 是实现 Agent(智能体)的核心组件。
核心概念
在 LangChain 中,Tool 本质上就是一个函数,它包含三个核心要素:
名称:大模型用来识别和调用该工具的唯一标识。 描述:告诉大模型这个工具是做什么的,什么时候应该调用它。(描述的质量直接决定了模型是否会准确调用) 执行逻辑:当大模型决定调用该工具时,实际运行的代码逻辑。 工作流:用户输入 -> Agent 判断是否需要使用 Tool -> LLM 返回 Tool 名称和参数 -> LangChain 执行 Tool -> 将执行结果返回给 LLM -> LLM 生成最终回答。
创建工具
基本工具定义
创建工具最简单的方法是从包中导入tool函数langchain。可以使用zod定义工具的输入模式,使用 tool 函数(🌟 最推荐)这是目前 LangChain.js 最推荐的声明式写法,结合 Zod 进行参数类型校验,清晰且安全。
js
import * as z from "zod"
import { tool } from "langchain"
// 1. 定义 Schema (约束大模型传入的参数)
const weatherSchema = z.object({
city: z.string().describe("需要查询天气的城市名称"),
unit: z.enum(["celsius", "fahrenheit"]).optional().describe("温度单位")
});
// 2. 创建 Tool
const getWeatherTool = tool(async ({ city, unit }) => {
// 这里的逻辑在实际应用中是调用外部天气 API
if (city === "北京") {
return `${city}今天晴朗,温度 25${unit === "celsius" ? "°C" : "°F"}`;
}
return `${city}今天多云`;
}, {
name: "get_weather", // 工具名称
description: "获取指定城市的当前天气情况", // 工具描述
schema: weatherSchema, // 绑定 Schema
});
console.log(await getWeatherTool.invoke({ city: "北京", unit: "celsius" }));
// 输出: 北京今天晴朗,温度 25°C
北京今天晴朗,温度 25°C
Tool 的核心属性解析
写好 Tool 的关键在于描述和Schema。描述的编写艺术,大模型完全依赖 description 来决定是否调用该工具。一个好的描述应该: 清晰说明功能:不要写"处理数据",要写"根据城市名称获取实时天气数据"。 说明适用场景:例如"当用户询问天气、气温、下雨情况时使用此工具"。 说明不适用场景:例如"不要用此工具查询新闻"。
Zod Schema 的作用
z.string(), z.number() 等类型约束可以防止大模型传入错误类型的数据。 .describe() 极其重要!它是对单个参数的解释,帮助大模型理解应该传什么值进去。
错误处理
如果工具调用出错了怎么办?不应该让程序崩溃,而应该将错误信息返回给 LLM,让它尝试自我修正。当 LLM 收到"除数不能为0"的返回时,它可能会换一个参数重新调用,或者向用户解释不能除以0。
js
const safeCalculator = tool(async (input) => {
try {
// 模拟可能出错的逻辑
if (input.b === 0) throw new Error("除数不能为0");
return input.a / input.b;
} catch (error: any) {
// 🌟 关键:将错误信息作为字符串返回,而不是 throw
return `工具调用出错: ${error.message}`;
}
}, {
name: "safe_divide",
description: "除法计算",
schema: z.object({ a: z.number(), b: z.number() })
});
使用内置工具
LangChain 社区提供了大量现成的工具,如网页搜索、数据库查询等。通过包 @langchain/community 引入。
bash
npm install @langchain/community
js
import { SerpAPI } from "@langchain/community/tools/serpapi";
// 使用 Google 搜索工具 (需配置 SERPAPI_API_KEY)
const searchTool = new SerpAPI(process.env.SERPAPI_API_KEY, {
hl: "cn",
gl: "cn",
});
vbnet
Stack trace:
Error: SerpAPI API key not set. You can set it as SERPAPI_API_KEY in your .env file, or pass it to SerpAPI.
at new SerpAPI (file:///Users/cheney/Documents/trae_projects/js/langchain-demo/node_modules/@langchain/community/dist/tools/serpapi.js:315:13)
at <anonymous>:3:20
在 Tool 中访问运行时状态
有时工具在执行时需要知道当前的用户 ID 或请求上下文。可以通过 RunnableConfig 传递。
js
import { RunnableConfig } from "@langchain/core/runnables";
const queryDatabaseTool = tool(async (input, config: RunnableConfig) => {
// 从 config 中获取元数据
const userId = config?.configurable?.userId;
return `查询到了用户 ${userId} 的数据: ...`;
}, {
name: "query_user_db",
description: "查询当前用户的数据库信息",
schema: z.object({ query: z.string() }),
});
// 调用时传入:
// agentExecutor.invoke({ input: "..." }, { configurable: { userId: "12345" } });
- 服务器端工具使用:某些聊天模型内置了在服务器端执行的工具(例如网页搜索、代码解释器)。详情请参阅"服务器端工具使用"部分。
- 工具名称最好使用字母数字下划线分割(例如,web_search而不是空格Web Search)。某些模型提供商对包含空格或特殊字符的名称存在兼容性问题,甚至会报错。坚持使用字母数字字符、下划线和连字符有助于提高不同提供商之间的兼容性。
让Tool更聪明:访问上下文
工具在能够访问运行时信息(例如对话历史记录、用户数据和持久内存)时,其功能最为强大。只有能够访问到这些信息,工具才能根据上下文进行决策和执行。就像给Tool配一个"秘书",让它知道是谁在调用、在什么场景下调用:
js
import * as z from "zod";
import { ChatOpenAI } from "@langchain/openai";
import { createAgent, tool } from "langchain";
const getUserName = tool(
(user_me, config) => {
// 从配置中读取用户名
const userName = config.context.user_name; // 从上下文获取用户名
if(user_me==userName){
return userName;
}
return "I don't know your name.";
},
{
name: "get_user_name",
description: "Get the current user's name.",
schema: z.object({}),
},
);
// 定义上下文结构
const contextSchema = z.object({
user_name: z.string(),
});
const agent = createAgent({
model: new ChatOpenAI({ model: "google-genai:gemini-3.5-flash" }),
tools: [getUserName],
contextSchema, // 告诉Agent上下文长什么样
});
// 调用时传入上下文
const result = await agent.invoke(
{
messages: [{ role: "user", content: "What is my name?" }],
},
{
configurable: { thread_id: crypto.randomUUID() }, // 会话ID
context: { user_name: "John Smith" }, // 上下文数据
},
);
长期记忆(Store)
让Tool拥有"记忆",跨会话记住信息:它BaseStore提供持久存储,数据可在会话之间保留。与状态(短期记忆)不同,保存到存储中的数据在以后的会话中仍然可用。 通过以下方式访问存储库config.store。存储库使用命名空间/键模式来组织数据:
js
import * as z from "zod";
import { createAgent, tool } from "langchain";
import { InMemoryStore } from "@langchain/langgraph";
import { ChatOpenAI } from "@langchain/openai";
const store = new InMemoryStore(); // 内存存储(生产环境可用数据库)
// 写入记忆
const saveUserInfo = tool(
async ({ user_id, name, age, email }) => {
await store.put(["users"], user_id, { name, age, email });
return "Successfully saved user info.";
},
{
name: "save_user_info",
description: "Save user info.",
schema: z.object({
user_id: z.string(),
name: z.string(),
age: z.number(),
email: z.string(),
}),
},
);
// 读取记忆
const getUserInfo = tool(
async ({ user_id }) => {
const value = await store.get(["users"], user_id);
return value;
},
{
name: "get_user_info",
description: "Look up user info.",
schema: z.object({ user_id: z.string() }),
},
);
const agent = createAgent({
model: new ChatOpenAI({ model: "gpt-5.4" }),
tools: [getUserInfo, saveUserInfo],
store, // 把存储交给Agent管理
});
// 第一次会话:保存用户信息
await agent.invoke({
messages: [
{ role: "user", content: "Save the following user: userid: abc123, name: Foo, age: 25, email: foo@langchain.dev" },
],
});
// 第二次会话:查询用户信息(记忆跨会话保留)
const result = await agent.invoke({
messages: [
{ role: "user", content: "Get user info for user with id 'abc123'" },
],
});
流式输出(Stream Writer)
给Tool加个"进度条",实时告诉用户它在干什么:在工具执行过程中,实时传输工具的更新信息。这对于在长时间运行的操作期间向用户提供进度反馈非常有用。 用于config.writer发出自定义更新:
js
import * as z from "zod";
import { tool, ToolRuntime } from "langchain";
const getWeather = tool(
({ city }, config: ToolRuntime) => {
const writer = config.writer;
// 像打字一样输出进度
if (writer) {
writer(`Looking up data for city: ${city}`); // 打印查找数据的进度
writer(`Acquired data for city: ${city}`); // 打印数据获取完成的进度
}
return `It's always sunny in ${city}!`;
},
{
name: "get_weather",
description: "Get weather for a given city.",
schema: z.object({ city: z.string() }),
},
);
执行信息
通过以下方式从工具内部访问线程 ID、运行 ID 和重试状态runtime.execution_info:
js
import { tool } from "langchain";
import * as z from "zod";
const logExecutionContext = tool(
async (_input, runtime) => {
const info = runtime.executionInfo;
console.log(`Thread: ${info.threadId}, Run: ${info.runId}`);
console.log(`Attempt: ${info.nodeAttempt}`);
return "done";
},
{
name: "log_execution_context",
description: "Log execution identity information.",
schema: z.object({}),
}
);
Tool返回值:不只是字符串
Tool不仅能返回字符串,还能返回结构化数据,甚至直接控制流程:
js
const weatherTool = tool(
({ city }) => {
// 返回对象而不是字符串
return {
city: city,
temperature: 25,
condition: "晴朗",
humidity: 45,
};
},
{
name: "get_weather_detailed",
description: "获取详细天气信息",
schema: z.object({ city: z.string() }),
},
);
实战组合:Tool + Agent 完整流程
js
import { ChatOpenAI } from "@langchain/openai";
import { createAgent, tool } from "langchain";
import * as z from "zod";
// 1. 创建工具
const searchTool = tool(
({ query }) => `搜索结果:关于${query}的信息...`,
{
name: "web_search",
description: "搜索互联网获取实时信息",
schema: z.object({ query: z.string().describe("搜索关键词") }),
},
);
const calculatorTool = tool(
({ expression }) => {
// 实际应用中这里用eval或数学库
return `计算结果:${expression}`;
},
{
name: "calculator",
description: "执行数学计算",
schema: z.object({ expression: z.string().describe("数学表达式") }),
},
);
// 2. 创建Agent
const agent = createAgent({
model: new ChatOpenAI({ temperature: 0 }),
tools: [searchTool, calculatorTool],
});
// 3. 运行Agent
const result = await agent.invoke({
messages: [
{ role: "user", content: "搜索一下LangChain最新版本,然后计算2024+2025等于多少" },
],
});
console.log(result);