1. 项目概述为什么LangChain.js的工具调用是AI应用落地的关键一步最近在折腾LangChain.js发现很多朋友在把玩完基础的聊天机器人后就卡在了“如何让AI真正帮我做事”这个坎上。比如你费劲写了个Agent它能跟你聊得天花乱坠但当你让它查一下明天的天气、或者帮你往数据库里存条数据时它往往就只会说“抱歉我无法访问实时数据”或者“我没有执行这个操作的能力”。这感觉就像你雇了个知识渊博的管家但他只会动嘴皮子连帮你开个灯都不会。问题的核心就在于“工具”Tools的使用。LangChain.js中的“工具”本质上就是赋予大语言模型LLM调用外部函数、访问外部资源的能力让AI从“思想家”变成“实干家”。这不仅仅是技术实现更是决定一个AI应用能否从Demo走向实用的分水岭。无论是处理实时信息、操作数据库、调用第三方API还是执行本地计算都需要通过工具来实现。今天我们就抛开理论直接进入实战手把手拆解在LangChain.js中如何定义、集成并使用工具打造一个真正能“动手”的智能体。2. 工具Tools的本质连接LLM与外部世界的桥梁在深入代码之前我们必须先搞清楚“工具”在LangChain.js架构里到底扮演什么角色。这有助于我们在后面做出正确的设计和选型。2.1 工具的核心概念与工作流程你可以把工具理解为一个标准的、LLM能够理解和调用的“函数接口”。这个接口包含几个关键部分名称name一个清晰、简短的标识符LLM会根据这个名称来决定在什么情况下调用它。比如get_weather、search_database。描述description这是最重要的部分。你需要用自然语言清晰地描述这个工具是干什么的、输入什么、输出什么。LLM尤其是那些不专门针对代码训练的模型主要靠这段描述来理解工具的用途。描述的质量直接决定了工具被正确调用的概率。执行函数func一个实际的JavaScript/TypeScript函数包含了真正的业务逻辑。当LLM决定调用某个工具时LangChain会执行这个函数并将结果返回给LLM。其工作流程是一个典型的“规划-执行-观察”循环规划用户提出请求如“上海明天天气怎么样”。LLM分析请求结合当前对话上下文和所有可用工具的描述判断是否需要调用工具、以及调用哪个工具。调用LLM生成一个结构化的调用指令包含工具名和输入参数。执行LangChain框架解析该指令找到对应的工具函数并执行。这个函数可能会去调用一个天气API、查询数据库或者执行一段计算。观察工具执行的结果成功的数据或错误信息被返回给LLM。整合与回复LLM接收到工具返回的结果将其整合到自己的思考中生成最终面向用户的自然语言回复。这个循环可能会迭代多次。例如用户问“帮我找一下关于LangChain的最新文章然后总结成三点”。LLM可能先调用一个搜索工具获取文章列表再调用一个阅读或总结工具来处理具体内容。2.2 内置工具 vs. 自定义工具如何选择LangChain.js 提供了丰富的内置工具同时也支持高度灵活的自定义工具。选择哪种方式取决于你的具体场景。内置工具开箱即用通常是封装了常见、稳定的第三方服务。优势集成快速无需关注底层API细节通常有较好的错误处理和类型定义。例如SerpAPI工具用于搜索引擎检索Calculator工具用于数学计算。劣势灵活性受限可能无法满足特定业务需求部分工具可能需要API密钥和付费。适用场景原型验证、快速搭建具备通用能力如搜索、计算的Agent或者作为你工具集的一个补充。自定义工具完全由你定义函数逻辑。优势无限灵活可以连接任何内部系统、数据库、API或执行任何复杂逻辑。这是将AI能力嵌入到你现有业务系统的唯一途径。劣势需要自行实现所有逻辑包括错误处理、参数验证、安全控制等开发成本较高。适用场景绝大多数企业级应用、需要操作内部数据或业务流程的场景。对于严肃的项目我的经验是以自定义工具为主内置工具为辅。核心业务逻辑必须掌握在自己手里用自定义工具封装而对于信息检索等辅助性功能可以酌情使用成熟的内置工具提升开发效率。3. 实战从零开始创建并使用自定义工具理论说再多不如一行代码。我们以一个实际场景为例构建一个“智能待办事项助手”。这个助手能帮用户添加任务、查询任务甚至根据内容自动分类。3.1 环境准备与基础结构搭建首先确保你的项目环境已经就绪。我们使用TypeScript来获得更好的类型提示。# 初始化项目并安装核心依赖 npm init -y npm install langchain langchain/core npm install -D typescript ts-node types/node # 初始化tsconfig.json npx tsc --init我们用一个简单的内存数组来模拟数据库实际项目中你会连接真实的数据库。// 模拟一个简单的内存数据库 interface TodoItem { id: number; title: string; description?: string; category?: string; completed: boolean; createdAt: Date; } class TodoStore { private todos: TodoItem[] []; private idCounter 1; addTodo(title: string, description?: string): TodoItem { const newTodo: TodoItem { id: this.idCounter, title, description, completed: false, createdAt: new Date(), }; this.todos.push(newTodo); return newTodo; } getTodos(filter?: { category?: string; completed?: boolean }): TodoItem[] { let result this.todos; if (filter?.category) { result result.filter(todo todo.category filter.category); } if (filter?.completed ! undefined) { result result.filter(todo todo.completed filter.completed); } return result; } // 其他方法如updateTodo, deleteTodo等... } export const todoStore new TodoStore();3.2 定义第一个自定义工具添加待办事项现在我们来创建第一个也是最核心的工具——add_todo。import { DynamicStructuredTool } from langchain/core/tools; import { z } from zod; // 用于参数验证 import { todoStore } from ./todoStore; // 使用 DynamicStructuredTool它支持基于 Zod Schema 的强类型参数定义 const addTodoTool new DynamicStructuredTool({ name: add_todo, description: 添加一个新的待办事项。输入需要标题描述是可选的。, schema: z.object({ title: z.string().describe(待办事项的标题必须清晰简短。), description: z.string().optional().describe(待办事项的详细描述。), }), func: async ({ title, description }) { // 这里是真正的业务逻辑 try { const newTodo todoStore.addTodo(title, description); return 成功添加待办事项ID: ${newTodo.id}, 标题: ${newTodo.title}。; } catch (error) { return 添加待办事项失败${error instanceof Error ? error.message : 未知错误}; } }, });关键点解析与避坑经验为什么用DynamicStructuredTool而不是基础的ToolDynamicStructuredTool集成了参数验证通过Zod能向LLM提供更精确的参数类型和描述极大提高了工具调用的准确率。这是官方推荐的方式。描述description是灵魂注意看描述它明确说明了工具的功能和输入要求。LLM就是靠这个来理解的。写得模糊调用就会出错。错误处理必不可少工具函数必须包含健壮的错误处理try-catch。因为工具可能被以意想不到的方式调用或者底层服务可能失败。永远不要让一个未处理的异常直接抛给LLM这会导致整个Agent崩溃。应该返回一个描述性的错误信息字符串。返回字符串工具函数必须返回一个字符串或Promise 。这个字符串会被直接塞回给LLM作为观察结果。因此返回的信息应该是对LLM“友好”的自然语言描述同时包含关键数据。3.3 定义第二个工具查询待办事项一个只能添加不能查看的待办助手是没用的。我们再创建一个查询工具。import { DynamicStructuredTool } from langchain/core/tools; import { z } from zod; import { todoStore } from ./todoStore; const getTodosTool new DynamicStructuredTool({ name: get_todos, description: 查询待办事项列表。可以按分类筛选也可以查看全部。, schema: z.object({ category: z.string().optional().describe(用于筛选的分类名称例如 工作、个人。如果不提供则返回所有事项。), showCompleted: z.boolean().optional().default(false).describe(是否显示已完成的事项默认为false只显示未完成。), }), func: async ({ category, showCompleted }) { try { const todos todoStore.getTodos({ category, completed: showCompleted ? true : undefined, }); if (todos.length 0) { const filterDesc [category 分类${category}, showCompleted 已完成].filter(Boolean).join(且); return 没有找到${filterDesc ? 符合${filterDesc}条件 : }的待办事项。; } // 将数据格式化成易于LLM理解的文本 const todoList todos.map(todo - ID:${todo.id} [${todo.completed ? ✓ : ○}] ${todo.title}${todo.description ? - ${todo.description} : }${todo.category ? (#${todo.category}) : } ).join(\n); return 找到 ${todos.length} 个待办事项\n${todoList}; } catch (error) { return 查询待办事项失败${error instanceof Error ? error.message : 未知错误}; } }, });经验之谈格式化返回信息工具返回给LLM的字符串其格式非常重要。你应该返回结构清晰、信息完整的自然语言。避免返回原始的JSON或过于简短的语句如“查询成功”。好的返回格式能让LLM更容易提取关键信息并组织成给用户的回复。例如上面返回的列表格式LLM可以轻松地将其转化为“我找到了您的X个待办事项分别是...”这样的句子。4. 组装智能体Agent让工具真正运转起来有了工具我们需要一个能调度它们的“大脑”——智能体Agent。这里我们使用最常用的ReAct代理框架它模仿人类“思考-行动”的过程效果非常稳定。4.1 配置LLM与创建代理执行器首先你需要一个LLM。这里以OpenAI的模型为例你也可以替换为Azure OpenAI、Anthropic等LangChain支持的其他模型。import { ChatOpenAI } from langchain/openai; import { AgentExecutor, createReactAgent } from langchain/agents; import { addTodoTool, getTodosTool } from ./tools; // 假设我们把工具放在tools.ts里 // 1. 初始化LLM。请将你的API密钥放在环境变量中不要硬编码在代码里 const llm new ChatOpenAI({ modelName: gpt-4o, // 对于工具调用更强大的模型如gpt-4、gpt-4o、claude-3效果更好 temperature: 0, // 对于执行类任务低温度0-0.3保证输出稳定性和可重复性 openAIApiKey: process.env.OPENAI_API_KEY, }); // 2. 定义工具数组 const tools [addTodoTool, getTodosTool]; // 3. 创建ReAct代理 const agent await createReactAgent({ llm, tools, // promptTemplate 可以自定义这里使用默认的ReAct模板 }); // 4. 创建代理执行器它是运行代理的主要接口 const agentExecutor new AgentExecutor({ agent, tools, // 以下是一些重要配置 verbose: true, // 开发时设为true可以看到LLM的思考链和工具调用详情便于调试 maxIterations: 5, // 限制最大迭代次数防止陷入死循环 handleParsingErrors: true, // 优雅处理LLM输出解析错误 });4.2 运行你的第一个工具调用代理现在让我们来运行它看看AI如何协调使用我们定义的工具。async function runAgent() { const input1 帮我记一下明天下午三点和团队开项目评审会。; console.log(用户: ${input1}); const result1 await agentExecutor.invoke({ input: input1 }); console.log(助手: ${result1.output}\n); // 等待一下模拟另一个对话轮次 const input2 我刚才让你记了什么会议来着; console.log(用户: ${input2}); const result2 await agentExecutor.invoke({ input: input2 }); console.log(助手: ${result2.output}); } runAgent().catch(console.error);当verbose: true时你会在控制台看到类似以下的详细输出这是理解Agent工作过程的绝佳材料用户: 帮我记一下明天下午三点和团队开项目评审会。 [Agent Thought] 用户要求添加一个待办事项。我需要使用 add_todo 工具。工具需要的参数是 title 和可选的 description。从用户输入中title 可以是“明天下午三点和团队开项目评审会”。description 可能不需要或者可以用输入本身。我先调用工具。 [Agent Action] 调用工具 add_todo参数{title: 明天下午三点和团队开项目评审会} [Tool Output] 成功添加待办事项ID: 1, 标题: 明天下午三点和团队开项目评审会。 [Agent Thought] 工具调用成功事项已添加。我可以把这个结果告诉用户。 助手: 好的已经为您添加了待办事项“明天下午三点和团队开项目评审会”。事项ID是1。 用户: 我刚才让你记了什么会议来着 [Agent Thought] 用户询问之前记录的会议。我需要查询待办事项列表。可以使用 get_todos 工具不添加筛选条件来查看所有事项。 [Agent Action] 调用工具 get_todos参数{} [Tool Output] 找到 1 个待办事项 - ID:1 [○] 明天下午三点和团队开项目评审会 [Agent Thought] 查询结果显示有一个未完成的待办事项正是之前添加的会议。我可以将这个信息回复给用户。 助手: 您之前让我记录的是“明天下午三点和团队开项目评审会”目前该事项尚未完成。这个过程清晰地展示了ReAct代理的“思考-行动-观察”循环。它自己分析问题、选择工具、解析参数、理解结果并生成回复。5. 高级技巧与生产环境避坑指南当你掌握了基础的工具调用后下面这些进阶知识和踩坑经验能帮你构建更健壮、更强大的应用。5.1 工具描述的优化艺术工具的description是LLM选择工具的唯一依据。写得好坏天差地别。差描述“一个工具。”或“处理数据。”好描述“根据用户提供的城市名称查询该城市未来三天的天气预报包括温度、天气状况和降水概率。输入应为单个城市名字符串。”优化原则明确功能用动词开头清晰说明做什么。“查询...”、“计算...”、“存储...”定义输入详细说明每个参数是什么、格式如何。“城市名称例如‘上海’”、“一个数学表达式字符串”说明输出告诉LLM会得到什么。“返回一个包含温度、湿度的字符串”、“返回操作成功或失败的消息”限定范围说明在什么情况下使用。“当用户询问天气时使用”、“当需要进行算术运算时使用”你可以为同一个工具准备多个不同详细程度的描述在不同复杂度的Agent中切换使用。5.2 处理复杂参数与多步骤操作有时用户请求很复杂比如“帮我查一下北京和上海的天气然后对比一下”。一个工具可能搞不定或者需要LLM进行多步规划。方案一设计组合工具创建一个高级工具内部封装多个步骤。例如创建一个compare_weather工具它在内部先调用两次天气查询API再进行对比分析。这样对LLM来说它只做了一次简单的工具调用。方案二依靠Agent的迭代能力这正是ReAct等框架的优势。LLM会先调用get_weather查北京拿到结果后再调用get_weather查上海最后自己整合两个结果进行对比回复。你需要确保每个基础工具都设计良好并且为Agent设置足够的maxIterations。关键点对于复杂逻辑我通常倾向于方案二。它更符合LLM的推理特性也更具灵活性。方案一虽然将复杂性隐藏了起来但工具描述会变得非常复杂且不易维护。5.3 错误处理与稳定性保障在生产环境中工具调用失败是常态。网络超时、API限流、无效输入等等。工具层捕获如前所述每个工具函数内部必须有try-catch返回错误信息字符串而不是抛出异常。Agent执行器配置利用handleParsingErrors配置。当LLM的输出无法被解析为有效的工具调用时可以定义一个回调函数向LLM返回一个定制化的错误提示让它“重试”或“换一种方式思考”。const agentExecutor new AgentExecutor({ agent, tools, verbose: true, maxIterations: 5, handleParsingErrors: (error) { // 这里可以记录日志 console.error(解析Agent输出时出错:, error); // 返回一个指导性的信息给LLM return 我未能正确理解你的指令。请更清晰地说明你想让我做什么或者直接告诉我你想使用哪个工具如添加任务、查询任务。; }, });设置超时与重试对于调用外部API的工具应该在函数内部使用axios等库设置请求超时并考虑实现简单的重试逻辑注意幂等性。输入验证前置在Zod Schema中尽可能定义严格的验证规则如字符串格式、枚举值、数字范围这能在工具执行前就过滤掉大量无效输入。5.4 上下文管理Memory与工具调用我们的例子中两次invoke是独立的。但在真实对话中我们需要Agent记住之前说过的话和做过的事。这就需要引入记忆Memory。import { ChatMessageHistory } from langchain/stores/message/in_memory; import { MessagesPlaceholder } from langchain/core/prompts; import { RunnableWithMessageHistory } from langchain/core/runnables; // 1. 在创建Agent时在Prompt中预留记忆变量的位置 const agent await createReactAgent({ llm, tools, promptTemplate: ..., // 通常需要自定义Prompt加入 chat_history 变量 }); // 2. 使用 RunnableWithMessageHistory 包装执行器 const messageHistory new ChatMessageHistory(); const agentWithMemory new RunnableWithMessageHistory({ runnable: agentExecutor, getMessageHistory: (_sessionId) messageHistory, // 根据会话ID获取历史这里简单处理 inputMessagesKey: input, historyMessagesKey: chat_history, }); // 3. 调用时传入配置 const result await agentWithMemory.invoke( { input: 我刚才让你记了什么会议来着 }, { configurable: { sessionId: user-123 } } // 通过sessionId区分不同用户的对话历史 );引入记忆后Agent就能进行连贯的多轮对话并基于历史上下文做出更准确的决策比如知道“我刚才让你记的”指的是哪件事。6. 调试与效能优化让开发过程更顺畅开发工具调用应用大部分时间都在调试。以下是我总结的高效调试方法。1. 开启Verbose模式这是最重要的第一步。将AgentExecutor的verbose设为true所有思考过程、工具调用和结果都一览无余。2. 模拟工具Mocking在开发初期或者当外部API不稳定时可以先创建一个工具的“模拟版本”返回固定的测试数据。这能让你快速验证Agent的逻辑流是否正确而不用等待真实的API响应。3. 单元测试工具函数将你的工具函数当作普通的异步函数进行单元测试。确保在各种边界输入下空值、错误格式、超长字符串都能正确处理并返回预期的字符串。4. 使用LLM调试输出如果Agent的行为不符合预期把verbose日志中LLM的“思考”Thought和准备调用的“动作”Action复制出来单独扔给同一个LLM比如通过OpenAI Playground问它“基于这个工具描述和用户输入你认为应该调用哪个工具参数是什么”这能帮你判断是工具描述的问题还是LLM本身推理的问题。5. 迭代优化描述调试往往是一个“修改工具描述 - 运行测试 - 观察结果”的循环。不要指望一次就能写出完美的描述。在效能上有两点需要注意工具数量一次性给Agent提供太多工具比如超过10个可能会降低其选择准确率并增加Token消耗。可以考虑根据上下文动态加载工具集。Token消耗每个工具的描述、每次工具调用的输入输出都会计入Token。优化描述的长度并让工具返回简洁但信息丰富的结果有助于控制成本。工具调用是LangChain.js将大语言模型从“聊天玩具”变为“生产力应用”的核心枢纽。它要求开发者不仅要有LLM的知识更要有扎实的软件工程思维如何设计清晰的接口、如何处理异常、如何管理状态。当你熟练掌握了工具的创建、组合与调试你就真正打开了构建复杂AI应用的大门。剩下的就是将你的业务逻辑一个个封装成工具然后看着AI智能体像一位熟练的员工一样将它们串联起来解决实际问题。这个过程充满挑战但当你看到第一个能自动处理工作流的Agent跑通时那种成就感是无与伦比的。