你正在构建一个多工具智能体它能在回答“A仓库还有多少库存B仓库呢明天要发50件够不够”这类问题时不是凭空编造一个数字而是真的去调用接口查数据、做计算最后给你一个可验证的答案。这就是 LangChain 里 Tool 与 Toolkit 体系的典型价值把模型的“想象”变成可执行的“操作”。这篇内容就是一套从零到一的完整实践指南适合已经跑通 LangChain 基础流程、但还没系统理解工具体系的开发者也适合刚接触智能体、想知道“工具到底怎么被模型调用”的初学者。1. 工具不是“插件”是给模型装的传感器和执行器1.1 模型再聪明也有够不到的地方先说句大实话大模型本身的能力边界非常硬核。知识有截止时间新发生的事情它不知道没有实时数据权限问它今天的天气、某个仓库的实时库存它只能靠训练数据里的统计规律去“猜”算个稍微复杂的数字比如某仓库剩余库存减去明天要发的50件它也可能一本正经地算错。更关键的是它没有执行能力没法替你更新一条数据库记录、创建一个文件、调用一个外部系统。这些边界不是“再训练一个更强的模型就能解决的”而是“模型与世界之间缺少连接层”的问题。工具就是这一层连接。你给了模型一个查询函数它就等于多了一只可以伸到数据库里的手你给了它一个计算函数它就等于多了一个不会算错的计算器。LangChain 的工具体系干的就是这件事。1.2 工具调用的本质模型动嘴代码动手很多人第一次接触 LangChain 的 Tool 时会疑惑工具到底是模型在执行还是模型自己写代码都不是。工具调用的本质是模型在对话过程中基于你的问题主动“声明”想调用某个函数并给出参数真正去执行这个函数的是你的代码执行完的结果再传回给模型模型继续推理并给出最终回答。整个过程很像餐厅里的点餐顾客用户向服务员模型说要吃什么服务员不会做菜但他知道后厨工具有哪些菜、怎么下单他把菜端回来后再由他向你汇报。后厨永远是服务员在指挥但做菜的从来不是服务员本人。这个类比能帮你绕开一个最常见的误区——不是模型变聪明了而是工具让它“够得着了”。1.3 多工具智能体的最小工作流一个多工具智能体的运行循环其实很朴素但每一步都不能少用户输入被送到大模型模型根据系统提示和工具描述判断“要不要调用工具、调用哪个、参数填什么”你的执行器拿着模型给出的参数真的去调用工具工具的返回值被拼接成一段文本重新送进模型模型看到工具结果继续思考直到它认为信息足够再给出最终回答这个循环就是智能体的基本心跳。LangChain 里的 Agent 负责把第 2 到第 5 步串起来。理解这个循环之后你就会明白整个系统里最容易被忽略、也最值得花心思的地方其实是“工具描述”——因为模型唯一能看到的不是你工具内部的代码而是你写的那几行说明文字。2. 动手前先吃透三件事Tool、Toolkit、Agent 的分工2.1 Tool一个“会说话的 Python 函数”在 LangChain 里Tool 是对普通函数的标准化包装。它有三个字段决定了模型能不能正确使用它字段作用谁来看name工具的唯一标识模型通过这个名称调用工具模型description工具的能力说明书描述这个工具能做什么、什么场景用模型args_schema参数格式定义告诉模型参数名、参数类型、是否必填模型 你的代码这三样东西name 必须短而唯一description 必须清楚到“一个没看过你代码的人能准确调用”args_schema 则要精确到“参数填错就会报错”。从最朴素的角度理解Tool 让一个函数变成了“会说话”的服务——它把自己的能力和约束都通过描述告诉模型然后等着模型来调用。2.2 Toolkit把散装工具打包成一个工具箱Toolkit 是比 Tool 高一级的抽象。它的本质是“一组语义相关的工具的集合”。我用一个比较贴切的比喻Tool 是螺丝刀Toolkit 是工具箱Agent 是那个拿着工具箱干活的工人。工人不会每次手里攥着二十把螺丝刀来回跑他要的是一个箱子打开就知道里面有哪些工具、分别能干什么。在代码层面Toolkit 通常是一个类的实例这个类提供一个get_tools()方法返回一个工具列表。Agent 真正拿到的就是这个列表。Toolkit 并不神秘它更多解决的是组织问题当你的工具数量多到一定程度或者你要在不同项目里复用同一组工具时把它打包成 Toolkit代码结构会更清晰复用也会更容易。2.3 Agent调度大脑与循环控制器Agent 是整个多工具智能体的调度核心。它的职责是读取所有工具描述理解用户问题自主规划该调用哪个工具、调用顺序是什么、参数怎么填。然后它执行工具、观察结果、再规划直到获得足够的信息。LangChain 里两种常见的 Agent 模式建议一开始就分清楚模式工作机制适用场景tool-calling 模式模型原生支持工具调用通过结构化的 tool_calls 输出直接决定调用哪个工具模型支持 function calling 时优先选这种ReAct 模式模型通过输出文本格式Action / Action Input表达调用意图再由解析器读取模型不支持函数调用或者你想自己控制解析逻辑如果模型支持工具调用尽量别用 ReAct。ReAct 的文字格式解析链路更长也更容易在解析环节出问题。我在实测中最明显的体感是tool-calling 模式的调用成功率更高出错也更少。2.4 版本差异提醒别被旧教程带偏LangChain 更新快API 变动也不小。很多旧教程还在用initialize_agent这种老写法但新版本更推荐的做法是create_tool_calling_agent配合AgentExecutor。你如果在旧教程里看到一个函数去查阅你实际安装版本的官方文档确认它是否已经废弃再决定要不要用。这个提醒不是废话我见过太多的错误堆栈最后查来查去就是版本 API 不匹配导致的。3. 从零写第一个工具把普通函数变成模型可用的“手”3.1 环境准备这一步很简单。安装langchain和langchain-core作为基础依赖再用你自己习惯的方式创建模型客户端。在下面的代码示例里我会用llm变量表示你已经创建好的模型实例它需要支持 tool calling / function calling。怎样初始化模型客户端每种模型都不一样这里就不展开了你只要保证手头有一个能正常调用的模型实例即可。pip install langchain langchain-core如果你打算在本地调试模型调用效果建议把 verbose 打开这样可以看到模型到底有没有生成工具调用。这一步经常被忽略但它是后面排查一切问题的基础工具。3.2 最朴素的例子写一个查询库存的工具我先用一个模拟的仓库库存数据构造一个工具。这个工具接收warehouse_id和可选的item_id从一份本地数据里查询库存信息并返回一段格式化的文本。import json from langchain_core.tools import tool # 模拟一份仓库库存数据 INVENTORY { A01: {SKU001: 120, SKU002: 30}, B02: {SKU001: 80, SKU002: 45}, } tool def query_inventory(warehouse_id: str, item_id: str | None None) - str: 查询指定仓库的库存数量。仓库编码如 A01、B02。如果提供商品编码返回该商品的库存否则返回仓库内所有商品库存。 if warehouse_id not in INVENTORY: return f错误仓库 {warehouse_id} 不存在 data INVENTORY[warehouse_id] if item_id: count data.get(item_id, 0) return f仓库 {warehouse_id} 中商品 {item_id} 的库存为 {count} 件 return f仓库 {warehouse_id} 的库存信息{json.dumps(data, ensure_asciiFalse)} 件tool装饰器会读取这个函数的名称作为工具名读取函数的 docstring 作为 description读取类型注解生成参数 schema。注意warehouse_id: str和item_id: str | None None这两个注解少了类型注解工具的参数 schema 往往就无法正确生成模型也就不知道该怎么填参。3.3 好描述与坏描述的差距工具描述是直接拼接进提示词、被模型逐字读取的。它决定了模型在正确的场景会不会想起这个工具、会不会填出正确的参数。我见过太多人写完工具的 docstring 就再也不管它等到线上出问题才发现模型根本不知道工具能干什么。举个坏例子查询库存。模型看完没有任何线索。仓库编码是什么格式item_id 参数要不要填返回什么内容全是黑盒。改成一个稍微像话的版本查询指定仓库的库存数量。仓库编码如 A01、B02。如果提供商品编码返回该商品的库存否则返回仓库内所有商品库存。这个描述里给出了仓库编码的示例也说明了参数空与不空时的行为差异模型知道怎么填参了也知道了返回结果的形态。如果你希望模型在某些情况下不要调用这个工具可以直接写进 description例如“仅用于查询不要用于补货建议。”这种“用途 参数格式 返回值格式 限制条件”的结构是我目前用过最稳定的写法。3.4 正式接进 Agent 之前先单测很多问题在集成前就能拦下来。工具写完先单独调用一次print(query_inventory.invoke({warehouse_id: A01})) # 输出仓库 A01 的库存信息{SKU001: 120, SKU002: 30} 件再检查一下自动生成的参数 schemafrom langchain_core.tools import tool # 通过工具的 args_schema 可以导出 JSON Schema print(query_inventory.args_schema.schema())看到输出里有warehouse_id、item_id的字段定义你就知道这个工具已经可以被模型理解了。如果 schema 是一片空白优先检查你的函数是不是漏了类型注解。这种单测习惯能帮你把“Agent 为什么乱调用”的问题从集成阶段提前拆解到工具编写阶段。4. 把工具打包成 Toolkit批量注册、命名冲突与内置包速览4.1 什么时候该从 Tool 升级到 Toolkit如果你只有一两个工具直接往 Agent 里塞列表就完了完全不需要 Toolkit。但当你遇到下面几种情况就可以考虑打包了一组工具在多个项目或多个 Agent 里重复使用工具数量超过五个Agent 初始化代码变得越来越长、越来越难维护你想把一个领域的能力完整封装起来比如“库存管理”“订单处理”“文档解析”Toolkit 的本质还是返回工具列表它不会给工具本身增加一个“额外的包装层”。Agent 最终拿到的仍然是list[BaseTool]。所以不用担心 Toolkit 会拖慢执行效率它只是个组织工具。4.2 手写一个自定义 Toolkit以库存管理为例我这次准备造一个InventoryToolkit里面放两个工具一个是查询库存的query_inventory一个是计算两个日期之间天数的calculate_days_between。后者看起来跟库存没关系但真实场景里智能体经常需要“算出距交货日还有几天”然后跟库存一起做判断。from langchain_core.tools import BaseToolkit, BaseTool, tool tool def calculate_days_between(start_date: str, end_date: str) - int: 计算两个日期之间的间隔天数日期格式为 YYYY-MM-DD。 from datetime import datetime s datetime.strptime(start_date, %Y-%m-%d) e datetime.strptime(end_date, %Y-%m-%d) return abs((e - s).days) class InventoryToolkit(BaseToolkit): 库存管理与日期计算工具包 def get_tools(self) - list[BaseTool]: return [query_inventory, calculate_days_between]这个自定义 Toolkit 的核心就是get_tools()方法。你可以在里面按需生成工具甚至根据不同的运行环境返回不同的工具列表。用的时候很简单toolkit InventoryToolkit() tools toolkit.get_tools() print([t.name for t in tools]) # [query_inventory, calculate_days_between]4.3 内置 Toolkit能用现成的就别自己造LangChain 的社区和官方都沉淀了一些内置 Toolkit例如数据库查询类、文件系统类、文档加载类等。这些内置工具包的优势是经过大量场景验证代码质量通常比你自己临时写的要高也更少踩坑。缺点是它们背后的服务不同初始化时往往需要自己提供连接参数不同版本的类名和导入路径也经常变化。我的建议是先用官方文档搜一下如果有正在维护的解决方案直接用它没有合适的再按上一小节的方式自定义。4.4 命名冲突两个工具叫了同一个名字工具列表中不能出现两个name相同的工具否则 Agent 在调用时会出现歧义轻则调用到错误实现重则直接报错。这是一个很隐蔽的坑发生在你从一个 Toolkit 里拿工具、又手动往 Agent 里塞另一个同名工具时。定位方法很简单初始化前先把所有工具名打出来检查重复项tools toolkit.get_tools() [query_inventory] # 可能出问题的写法 names [t.name for t in tools] duplicated {n for n in names if names.count(n) 1} if duplicated: print(发现重名工具, duplicated)发现重复后改掉其中一个工具的name即可。给工具命名时最好带上业务前缀比如inventory_query、inventory_update而不是笼统的query、update——这样既降低重名概率也方便模型识别工具归属。5. 串成第一个多工具智能体一次真实任务的完整链路5.1 组装 AgentExecutorAgent 的组装比想象中简单。关键是在一个 Prompt 模板里把用户的输入和模型内部的工具调用记录位置预留好。create_tool_calling_agent会帮我们完成大部分调度工作。from langchain.agents import create_tool_calling_agent, AgentExecutor from langchain_core.prompts import ChatPromptTemplate prompt ChatPromptTemplate.from_messages([ (system, 你是一名仓库助理善于调用工具查询库存并做数量计算。), (placeholder, {chat_history}), (human, {input}), (placeholder, {agent_scratchpad}), ]) agent create_tool_calling_agent(llmllm, toolstools, promptprompt) agent_executor AgentExecutor(agentagent, toolstools, verboseTrue)agent_scratchpad这个占位符很关键它承载的是模型在推理过程中已经生成的工具调用和工具返回结果。没有它模型就没有“记忆”去继续推理。5.2 实测让 Agent 替你做一次跨仓库存比较组装完之后直接赋予一个真实的业务问题result agent_executor.invoke({ input: A01 仓库和 B02 仓库的 SKU001 库存各是多少两个仓库加起来够明天发 50 件吗 })如果把verboseTrue打开你能在当前终端看到类似这样的中间过程 Entering new AgentExecutor chain... Invoking: query_inventory with {warehouse_id: A01, item_id: SKU001} ... Invoking: query_inventory with {warehouse_id: B02, item_id: SKU001} ...Agent 先是调了两次query_inventory拿到两个数字然后自己在最终回答里做加法并给出结论。整个过程完全由模型自己规划没有人在代码里写“先查 A 再查 B 再比较”的硬逻辑。这就是多工具智能体帮我们解放双手的地方——它自动完成了任务编排。5.3 中间日志怎么读判断 Agent 选路是否正常日志里最值得关注的是Invoking那一行。它告诉我们模型认为此刻应该调用哪个工具、传入什么参数。如果日志里出现了不合理的调用比如模型把warehouse_id填成了“一号仓库”说明你的描述或 schema 没写清楚而不是模型“变笨了”。把 Agent 的推理链路暴露到 verbose 日志里是最直接的排障手段。5.4 工具返回值的姿势结构化文本比裸字典更稳工具有一个极其影响后续推理的细节返回值格式。如果你的工具返回一个 Python 字典有时候模型确实也能读但它更容易在格式化字符串时搞出歧义。我踩过几次坑之后强制自己在工具里json.dumps(ensure_asciiFalse)或直接用“仓库 X 中商品 Y 的库存为 Z 件”这样的文本模板。例如# 不建议 return {warehouse: warehouse_id, sku: item_id, count: 100} # 建议 return f仓库 {warehouse_id} 中商品 {item_id} 的库存为 {count} 件当你看到模型对工具结果理解错位时先别怪模型回去改工具的返回值格式往往就有奇效。工具返回的信息对模型而言就是一个字符串字符串越规整模型越容易从里面对应出最终回答所需的要素。6. 让 Agent 少犯错的调优手段以及一次典型踩坑复盘6.1 描述里写清“不要做什么”工具描述不止要说它能做什么更要说清边界。比如我们要做一个“补货建议工具”但如果库存不够直接把“缺货”这件事说清楚不要顺手给出建议因为建议应该由另一个“补货建议工具”来管。把边界写进 description模型就不会把一个工具同时干了两件事也不会在工具返回缺货结果后依然瞎猜一个补货数量。好描述里可以加一句“如果库存充足无需调用此工具”或者“此工具不负责生成补货建议”。这听起来像是在对模型说话但工具描述本质上就是要说给模型听的。你越是把限制写清楚模型就越不会自由发挥。6.2 工具内部防御让错误信息成为推理依据工具调用过程中的异常最好不要直接抛给 AgentExecutor。一旦抛出未捕获的异常整个链路会变得很难看模型也会完全失去“自己修正”的机会。更稳妥的做法是工具内部 try-except把异常转成一句可读的错误信息返回tool def query_inventory(warehouse_id: str, item_id: str | None None) - str: 描述同前 try: if warehouse_id not in INVENTORY: return f错误仓库 {warehouse_id} 不存在 ... except Exception as e: return f查询库存时发生错误{e}这样它不会中断 Agent 的循环模型读到“错误仓库 X 不存在”时会自己尝试修正参数、换一个仓库编码甚至直接向用户说明情况。错误信息从“崩溃”变成了“推理素材”。6.3 token 与性能优化别让描述撑爆上下文工具描述最终都会被拼进提示词。如果你给每个工具写一篇两千字的长篇描述模型每次调用都要把这些文字从头读一遍token 消耗自然增大模型注意力也会被稀释。我见过一个项目里的工具描述长达整页结果 Agent 反而更频繁调用错了工具。好的描述应该压缩在几行内把用途、参数格式、返回值、边界说明讲清楚就好。给工具额外加一个简单的入参缓存也能省掉大量重复调用比如用functools.lru_cache装饰底层函数业务场景里非常实用。6.4 踩坑复盘一个描述过于泛化的“查询工具”是怎么坑了整个 Agent 的写这篇文章的时候我真的复现过一个典型场景把两个工具分别命名为query和calculate描述都很简单。结果 Agent 在遇到“计算明天发货够不够”这种问题时不去调用calculate反而连续调用query传入完全错误的仓库编码。从 verbose 日志看它就像一个固执的客服反复拨同一个错误号码。定位过程是这样的打开 verbose 日志发现模型连续调用query参数warehouse_id有时候是“A01号仓”有时候是“上海仓”检查query工具的 schema发现只有warehouse_id: str没有任何说明仓库编码格式查看了两个工具 description发现calculate的描述只写了“计算”没写“用于计算日期差值或数值增减”模型根本不知道第二个工具是解决“够不够”问题的关键修复方案是重写两个工具的描述把query明确为“仅查询库存不接受中文名称仓库编码格式为 A01、B02”把calculate明确为“计算两个日期天数差或数值运算结果用于判断库存是否满足发货需求”。改完之后同一个任务日志变成了清晰的“先查库存、再调用计算工具比较”链路。这次踩坑让我养成了一个习惯每个新工具都先直接投喂给模型一个测试问题观察它调用路径是否与预期一致如果不一致先看日志里模型是怎么理解工具描述的再顺着描述去修改而不是去调模型参数。排查的优先级永远是工具描述和 schema 的问题先看Agent 配置的问题后看。6.5 沉淀一份工具上线前的自查清单我在多工具智能体上踩过足够多的坑之后现在每次写完一组工具都会对着下面的清单检查一遍工具名是否唯一、是否带业务前缀description 是否包含用途、参数格式、返回内容、边界限制参数类型注解是否完整可选参数是否有默认值工具内部是否做了异常兜底错误信息是否可读返回值是否为规整文本而不是裸字典是否先用单测确认工具能填充正确的参数再接入 Agent这六条看起来琐碎但每一条都在真实项目里用事故换过教训。工具一旦打磨到清单里每一项都通过Agent 基本上就不会再犯那些低级的“调用错了”类错误。我个人的体会是多工具智能体最难的部分从来不是怎么让模型“会调用工具”而是你愿不愿意先把每个工具本身打磨到可以被放心调用。工具是模型的传感器和执行器传感器的精度不达标大脑再聪明也判断不了世界。如果你最近也在搭类似的智能体遇到问题多往描述和参数 schema 那边看看大概率能找到最直接的解法。
阅读完成 · 觉得有帮助?