1. 五种工具定义方式到底在解决什么问题LangChain 里让大模型调用外部能力绕不开 Tool 这个概念。但很多人第一次接触时会被各种写法搞晕有tool装饰器、有StructuredTool、有Tool类、有convert_to_openai_tool、还有直接传函数给 Agent 的写法。每种写法都能跑但踩的坑完全不一样。我最初做的一个模拟项目需要让模型查本地数据库、调内部接口、做格式转换。一开始随手用Tool类硬编码结果参数一多就崩换成tool装饰器后类型推断又出问题后来试了StructuredTool才把参数校验稳住。整个过程踩了至少五种不同的坑今天把这些整理出来给正在选型的同行省点时间。这篇文章适合已经跑通过 LangChain 基础链路、准备把工具接入生产环境的开发者。如果你还在纠结要不要用 LangChain那可以先跳过但如果你已经决定用并且被工具定义的各种写法搞得头大下面的内容应该能直接抄作业。核心关键词就一个工具定义方式。我会把五种方式的适用场景、参数写法、常见报错、排查思路全部拆开讲最后给一张选型对照表。所有代码都是可复现的最小示例不依赖任何特定平台。2. 五种工具定义方式逐项拆解2.1 方式一tool 装饰器——最顺手但最容易踩类型坑tool装饰器是 LangChain 里最直观的写法。你写一个普通函数上面加一行tool它就变成了一个 Tool 对象。函数名默认成为工具名docstring 成为工具描述类型注解用来生成参数 schema。from langchain_core.tools import tool tool def search_order(order_id: str) - str: 根据订单号查询订单状态 return f订单 {order_id} 已发货看起来很简单但坑就在类型注解上。LangChain 会解析函数的类型注解来生成 JSON Schema如果你用了Optional、Union、List[Dict]这类复杂类型解析器可能直接报错或者生成错误的 schema。我实测下来str、int、float、bool这四种基础类型最稳复杂类型建议用StructuredTool配合 Pydantic 模型。另一个坑是 docstring 的格式。LangChain 会从 docstring 里提取参数描述如果你写的是 Google 风格或者 NumPy 风格的 docstring提取结果可能不完整。最稳妥的做法是把参数描述直接写在函数签名旁边的注释里或者用args_schema显式指定。注意tool装饰器默认会把函数名作为工具名如果函数名有下划线或者大写字母某些模型可能识别不稳定。建议工具名用纯小写字母加下划线比如search_order而不是SearchOrder。还有一个隐藏坑tool装饰的函数如果抛出异常异常信息会直接传给模型。如果你不想暴露内部错误细节需要在函数内部 try-except 后返回友好提示。我见过有人直接把数据库连接错误抛出去模型收到一堆堆栈信息后完全懵了。2.2 方式二Tool 类硬编码——最灵活但参数一多就失控Tool类是 LangChain 最早期的工具定义方式需要手动指定name、func、description三个核心参数。from langchain_core.tools import Tool def query_weather(city: str) - str: return f{city} 今天晴25度 weather_tool Tool( namequery_weather, funcquery_weather, description查询指定城市的天气输入城市名称 )这种写法的好处是你可以完全控制工具名和描述不受函数名限制。但问题也很明显func接收的是原始字符串输入LangChain 不会自动做参数解析。如果你的函数需要多个参数模型必须把参数拼成一个字符串传进来然后你在函数内部自己 split。这就很容易出错。我试过用Tool类定义一个需要三个参数的工具模型有时候传北京,晴,25有时候传北京 晴 25还有时候传 JSON 字符串。你需要在函数内部写一堆兼容逻辑非常痛苦。所以Tool类只适合单参数、输入格式固定的场景比如输入一个关键词返回搜索结果。提示如果你非要用Tool类处理多参数建议在 description 里明确写出输入格式比如输入格式为城市名|日期并在函数内部做严格校验。但即便如此模型仍然可能不按格式来。2.3 方式三StructuredTool——参数校验最稳但写起来最啰嗦StructuredTool是 LangChain 里参数处理能力最强的工具类。它支持 Pydantic 模型作为args_schema可以定义复杂的嵌套参数、默认值、字段描述。from langchain_core.tools import StructuredTool from pydantic import BaseModel, Field class OrderQueryInput(BaseModel): order_id: str Field(description订单号纯数字) detail_level: int Field(default1, description详情级别1基础 2完整) def query_order(order_id: str, detail_level: int 1) - str: return f订单 {order_id} 详情级别 {detail_level} order_tool StructuredTool.from_function( funcquery_order, namequery_order, description查询订单详情, args_schemaOrderQueryInput )这种写法的优势非常明显参数校验在进入函数之前就完成了模型传错类型会直接报错而不是把脏数据传进业务逻辑。而且 Pydantic 的Field可以写详细的参数描述模型理解起来更准确。但坑在于StructuredTool.from_function要求函数的参数名和 Pydantic 模型的字段名完全一致。如果你函数里写的是order_id模型里写的是orderId运行时会直接报ValidationError。我踩过这个坑排查了半小时才发现是命名不一致。另一个坑是默认值的处理。Pydantic 模型里设置了default1但函数签名里也必须设置同样的默认值否则调用时可能缺少参数。LangChain 不会自动把 Pydantic 的默认值填充到函数调用里这一点文档里写得很模糊。2.4 方式四convert_to_openai_tool——格式转换的桥梁但容易丢信息convert_to_openai_tool是一个工具函数作用是把 LangChain 的 Tool 对象转换成 OpenAI 兼容的 function calling 格式。如果你用的是支持 function calling 的模型这个转换是必须的。from langchain_core.utils.function_calling import convert_to_openai_tool openai_tool convert_to_openai_tool(order_tool)这个函数本身没什么坑但问题在于转换过程中可能会丢失一些信息。比如StructuredTool里 Pydantic 模型的嵌套结构转换后可能变成扁平的 JSON Schema。如果你的参数有复杂的嵌套对象转换后模型可能无法正确理解。我实测下来convert_to_openai_tool对基础类型和简单对象支持最好嵌套超过两层的结构建议拆成多个工具而不是硬塞进一个工具里。另外转换后的 schema 里required字段的生成逻辑和 Pydantic 的Optional有关如果你不希望某个参数必填一定要在 Pydantic 里设置默认值。注意不同模型对 function calling 的 schema 支持程度不一样。有些模型不支持anyOf、oneOf这类 JSON Schema 关键字转换后可能直接报错。建议在转换后打印一下 schema确认没有模型不支持的字段。2.5 方式五直接传函数给 Agent——最省事但最不可控LangChain 的 Agent 初始化时可以直接传一个函数列表框架会自动把函数转换成 Tool 对象。from langchain.agents import initialize_agent def get_time() - str: 获取当前时间 return 2025-01-01 12:00:00 agent initialize_agent( tools[get_time], llmllm, agentzero-shot-react-description )这种写法最省事但坑也最多。首先函数必须有 docstring否则工具描述为空模型完全不知道这个工具是干什么的。其次函数不能有除了self之外的必填参数否则初始化时直接报错。我试过传一个需要两个参数的函数进去结果 Agent 初始化就失败了报错信息还很不直观。另外这种写法下你无法控制工具名。LangChain 会用函数名作为工具名如果函数名有特殊字符或者太长模型可能识别不稳定。而且你无法给工具加额外的元数据比如return_direct、handle_tool_error这些参数。所以这种写法只适合快速原型验证生产环境建议还是用tool或者StructuredTool显式定义。3. 实操过程从零搭建一个多工具 Agent3.1 环境准备与依赖安装先明确版本。LangChain 的 API 变动非常频繁不同版本的工具定义方式差异很大。我用的组合是langchain-core0.3.x 加langchain0.3.x这个组合比较稳定。pip install langchain-core0.3.0 langchain0.3.0 pydantic2.0如果你用的是更早的版本比如 0.1.xStructuredTool.from_function的参数名可能不一样建议先确认版本再抄代码。提示LangChain 的包拆分很细langchain-core里是核心抽象langchain里是 Agent 和 Chain 的实现。如果你只装langchain不装langchain-core可能会遇到导入错误。3.2 定义三个典型工具覆盖不同场景我设计了一个模拟场景一个客服助手需要查订单、查物流、发通知。三个工具分别用三种不同的方式定义覆盖单参数、多参数、无参数三种情况。工具一查订单用tool装饰器单参数from langchain_core.tools import tool tool def query_order(order_id: str) - str: 根据订单号查询订单状态。输入必须是纯数字订单号。 if not order_id.isdigit(): return 订单号格式错误请提供纯数字订单号 return f订单 {order_id} 状态已发货这个工具用tool装饰器参数只有一个str类型推断最稳定。我在函数内部加了格式校验避免模型传入非数字字符串。工具二查物流用StructuredTool多参数带默认值from langchain_core.tools import StructuredTool from pydantic import BaseModel, Field class LogisticsInput(BaseModel): order_id: str Field(description订单号) carrier: str Field(defaultauto, description快递公司默认自动识别) def query_logistics(order_id: str, carrier: str auto) - str: return f订单 {order_id} 由 {carrier} 承运预计明天送达 logistics_tool StructuredTool.from_function( funcquery_logistics, namequery_logistics, description查询订单物流信息, args_schemaLogisticsInput )这个工具用StructuredTool两个参数其中一个有默认值。注意函数签名和 Pydantic 模型的字段名必须完全一致默认值也要一致。工具三发通知用Tool类无参数from langchain_core.tools import Tool def send_notification() - str: return 通知已发送 notify_tool Tool( namesend_notification, funclambda _: send_notification(), description发送通知给用户无需输入参数 )无参数的工具用Tool类反而最方便因为func接收一个字符串参数用 lambda 忽略掉就行。如果用tool装饰器定义无参数函数LangChain 会生成一个空的参数 schema某些模型可能不兼容。3.3 组装 Agent 并测试工具调用三个工具定义好后组装成一个 Agent。from langchain.agents import initialize_agent, AgentType from langchain_openai import ChatOpenAI llm ChatOpenAI(modelgpt-4o-mini, temperature0) tools [query_order, logistics_tool, notify_tool] agent initialize_agent( toolstools, llmllm, agentAgentType.OPENAI_FUNCTIONS, verboseTrue ) result agent.invoke({input: 帮我查一下订单 12345 的物流}) print(result)这里用的是OPENAI_FUNCTIONS类型的 Agent它会自动把 Tool 对象转换成 function calling 格式。如果你用的是其他模型可能需要换成ZERO_SHOT_REACT_DESCRIPTION但那种模式下工具调用的稳定性会差一些。实测下来OPENAI_FUNCTIONS模式下三个工具都能被正确调用。但有一个细节query_order和query_logistics都需要order_id参数模型有时候会混淆把订单号传给物流工具后又传给订单工具。解决办法是在 description 里写清楚每个工具的用途差异比如查订单状态用 query_order查物流轨迹用 query_logistics。3.4 参数计算与选择过程工具定义里涉及几个关键参数选择我逐个说明理由。temperature0Agent 调用工具时需要确定性输出温度必须设为 0。如果温度太高模型可能随机选择工具或者生成错误的参数格式。AgentType.OPENAI_FUNCTIONS这个类型依赖模型的 function calling 能力工具调用的准确率最高。如果模型不支持 function calling只能降级到 ReAct 模式但 ReAct 模式下工具描述的质量直接影响调用成功率。verboseTrue调试阶段一定要打开可以看到模型每一步的思考过程和工具调用参数。生产环境可以关掉减少日志量。工具数量控制在 5 个以内我试过给 Agent 传 10 个工具模型的选择准确率明显下降。如果业务需要更多工具建议按场景分组每组一个 Agent。4. 常见问题与排查技巧实录4.1 工具调用报错速查表报错信息可能原因排查方法ValidationError: field requiredPydantic 模型字段名和函数参数名不一致逐字对比字段名包括大小写和下划线TypeError: func() missing 1 required positional argument函数签名和 args_schema 不匹配检查函数参数是否有默认值Pydantic 字段是否必填KeyError: nameTool 对象缺少 name 属性确认用的是tool或StructuredTool而不是裸函数openai.BadRequestError: Invalid schema转换后的 JSON Schema 模型不支持打印convert_to_openai_tool的结果检查是否有anyOf等关键字Agent stopped due to iteration limit模型反复调用工具但无法得出答案检查工具返回值是否包含模型需要的信息增加max_iterations4.2 三个独家避坑技巧技巧一工具描述里加不要用于的说明。模型有时候会过度使用某个工具。比如查订单的工具模型可能拿它去查物流。在 description 里加一句此工具仅用于查询订单状态不用于查询物流能显著降低误调用率。技巧二返回值统一用字符串。LangChain 的工具返回值最终会拼接到模型的上下文里。如果你返回一个字典或者列表某些模型可能解析不了。最稳妥的做法是返回格式化好的字符串比如订单状态已发货物流单号SF123456。技巧三给工具加超时和重试。如果工具内部调用了外部接口一定要加超时。我见过有人工具内部请求一个慢接口模型等了 30 秒后直接超时整个 Agent 链路崩溃。建议在工具函数内部用requests的timeout参数或者用tenacity做重试。注意LangChain 本身不提供工具级别的超时控制你需要在工具函数内部自己实现。如果工具执行时间超过模型的最大等待时间Agent 会直接报错。4.3 工具选型对照表定义方式适用场景参数复杂度推荐指数tool装饰器单参数、基础类型低高Tool类无参数或单字符串参数低中StructuredTool多参数、需要校验高高convert_to_openai_tool配合 function calling 模型中必用直接传函数快速原型验证低低这张表是我踩了无数坑之后总结的。如果你刚开始用 LangChain建议从tool装饰器入手遇到多参数场景再换StructuredTool。Tool类只适合无参数工具直接传函数只适合临时测试。5. 工具定义之外的几个关键决策5.1 工具粒度怎么控制工具粒度是一个很容易被忽视的问题。粒度太细模型需要调用多次才能完成一个任务粒度太粗模型无法灵活组合。我的经验是一个工具只做一件事但这件事的边界要清晰。比如查订单和查物流是两个工具而不是一个查订单及物流的工具。因为模型可能只需要查订单不需要查物流拆开更灵活。但也不能拆得太细。比如查订单号和查订单状态就没必要拆成两个工具因为查订单号的目的就是为了查状态。拆得太细会导致模型调用链路过长增加出错概率。5.2 工具命名和描述的规范工具名建议用动词_名词的格式比如query_order、send_notification、calculate_discount。不要用缩写不要用拼音不要用驼峰命名。工具描述要写清楚三件事这个工具做什么、什么时候用、输入格式是什么。比如根据订单号查询订单状态。当用户询问订单进度、发货状态时使用此工具。 输入必须是纯数字订单号例如 12345。这样的描述模型理解起来最准确。我试过只写查询订单模型经常把物流查询也路由到这个工具上。5.3 错误处理与降级策略工具执行失败时不要让异常直接抛给模型。正确的做法是在工具内部捕获异常返回一个友好的错误提示。tool def query_order(order_id: str) - str: 根据订单号查询订单状态 try: result db.query(order_id) return f订单 {order_id} 状态{result.status} except Exception as e: return f查询订单 {order_id} 失败请稍后重试这样模型收到的是查询失败请稍后重试而不是一堆堆栈信息。模型可以根据这个提示决定是重试还是告知用户。如果某个工具经常失败可以考虑在 Agent 层面做降级。比如物流查询工具不可用时直接返回物流信息暂时无法查询而不是让模型反复重试。5.4 性能优化的几个实操点工具调用的性能瓶颈通常在外部接口请求上。几个优化点缓存对于查询类工具相同参数的请求可以缓存结果。用functools.lru_cache或者 Redis 都行。并发如果模型一次需要调用多个工具LangChain 支持并发执行。但要注意工具之间是否有依赖关系。超时每个工具都要设超时建议 5-10 秒。超过这个时间模型等待成本太高。返回值精简工具返回的内容会拼接到模型上下文里返回内容越长token 消耗越大。只返回模型需要的信息不要返回整个数据库记录。我实测下来一个工具返回 500 字和返回 50 字模型的理解准确率差不多但 token 消耗差 10 倍。所以返回值一定要精简。6. 我个人的选型建议如果你正在做 LangChain 项目工具定义方式的选择顺序建议是优先用tool装饰器参数复杂时换StructuredTool无参数工具用Tool类快速验证时直接传函数。convert_to_openai_tool不是可选项只要用 function calling 模型就必须经过这一步。踩过几次坑之后我现在的习惯是所有工具都用StructuredTool定义哪怕只有一个参数。因为 Pydantic 的校验能力太香了能提前拦截 80% 的参数错误。虽然写起来啰嗦一点但省下的排查时间远超这点成本。最后再分享一个小技巧定义完工具后先单独测试每个工具的调用确认参数传递和返回值都符合预期再组装到 Agent 里。我见过太多人直接把一堆工具扔给 Agent然后花几个小时排查为什么模型不调用某个工具。单独测试五分钟能省下几小时的调试时间。