ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

LangChain工具定义五种方式详解:从@tool到StructuredTool的选型与避坑指南

LangChain工具定义五种方式详解:从@tool到StructuredTool的选型与避坑指南

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( name="query_weather", func=query_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(default=1, 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( func=query_order, name="query_order", description="查询订单详情", args_schema=OrderQueryInput )

这种写法的优势非常明显:参数校验在进入函数之前就完成了,模型传错类型会直接报错而不是把脏数据传进业务逻辑。而且 Pydantic 的Field可以写详细的参数描述,模型理解起来更准确。

但坑在于:StructuredTool.from_function要求函数的参数名和 Pydantic 模型的字段名完全一致。如果你函数里写的是order_id,模型里写的是orderId,运行时会直接报ValidationError。我踩过这个坑,排查了半小时才发现是命名不一致。

另一个坑是默认值的处理。Pydantic 模型里设置了default=1,但函数签名里也必须设置同样的默认值,否则调用时可能缺少参数。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], llm=llm, agent="zero-shot-react-description" )

这种写法最省事,但坑也最多。首先,函数必须有 docstring,否则工具描述为空,模型完全不知道这个工具是干什么的。其次,函数不能有除了self之外的必填参数,否则初始化时直接报错。我试过传一个需要两个参数的函数进去,结果 Agent 初始化就失败了,报错信息还很不直观。

另外,这种写法下你无法控制工具名。LangChain 会用函数名作为工具名,如果函数名有特殊字符或者太长,模型可能识别不稳定。而且你无法给工具加额外的元数据,比如return_direct、handle_tool_error这些参数。

所以这种写法只适合快速原型验证,生产环境建议还是用@tool或者StructuredTool显式定义。

3. 实操过程:从零搭建一个多工具 Agent

3.1 环境准备与依赖安装

先明确版本。LangChain 的 API 变动非常频繁,不同版本的工具定义方式差异很大。我用的组合是langchain-core0.3.x 加langchain0.3.x,这个组合比较稳定。

pip install langchain-core==0.3.0 langchain==0.3.0 pydantic==2.0

如果你用的是更早的版本,比如 0.1.x,StructuredTool.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(default="auto", description="快递公司,默认自动识别") def query_logistics(order_id: str, carrier: str = "auto") -> str: return f"订单 {order_id} 由 {carrier} 承运,预计明天送达" logistics_tool = StructuredTool.from_function( func=query_logistics, name="query_logistics", description="查询订单物流信息", args_schema=LogisticsInput )

这个工具用StructuredTool,两个参数,其中一个有默认值。注意函数签名和 Pydantic 模型的字段名必须完全一致,默认值也要一致。

工具三:发通知,用Tool类,无参数

from langchain_core.tools import Tool def send_notification() -> str: return "通知已发送" notify_tool = Tool( name="send_notification", func=lambda _: 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(model="gpt-4o-mini", temperature=0) tools = [query_order, logistics_tool, notify_tool] agent = initialize_agent( tools=tools, llm=llm, agent=AgentType.OPENAI_FUNCTIONS, verbose=True ) 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 参数计算与选择过程

工具定义里涉及几个关键参数选择,我逐个说明理由。

temperature=0:Agent 调用工具时需要确定性输出,温度必须设为 0。如果温度太高,模型可能随机选择工具或者生成错误的参数格式。

AgentType.OPENAI_FUNCTIONS:这个类型依赖模型的 function calling 能力,工具调用的准确率最高。如果模型不支持 function calling,只能降级到 ReAct 模式,但 ReAct 模式下工具描述的质量直接影响调用成功率。

verbose=True:调试阶段一定要打开,可以看到模型每一步的思考过程和工具调用参数。生产环境可以关掉,减少日志量。

工具数量控制在 5 个以内:我试过给 Agent 传 10 个工具,模型的选择准确率明显下降。如果业务需要更多工具,建议按场景分组,每组一个 Agent。

4. 常见问题与排查技巧实录

4.1 工具调用报错速查表

报错信息可能原因排查方法
ValidationError: field requiredPydantic 模型字段名和函数参数名不一致逐字对比字段名,包括大小写和下划线
TypeError: func() missing 1 required positional argument函数签名和 args_schema 不匹配检查函数参数是否有默认值,Pydantic 字段是否必填
KeyError: 'name'Tool 对象缺少 name 属性确认用的是@tool或StructuredTool,而不是裸函数
openai.BadRequestError: Invalid schema转换后的 JSON Schema 模型不支持打印convert_to_openai_tool的结果,检查是否有anyOf等关键字
Agent stopped due to iteration limit模型反复调用工具但无法得出答案检查工具返回值是否包含模型需要的信息,增加max_iterations

4.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,然后花几个小时排查为什么模型不调用某个工具。单独测试五分钟,能省下几小时的调试时间。

返回列表