资讯详情

资讯详情

LangChain Agent工具定义五种方式踩坑与选型指南

1. 五种工具定义方式我踩过的坑比文档还多LangChain 里给 Agent 挂工具看起来就是一行tool装饰器的事但真到项目里五种定义方式各有各的脾气。我前后在两个项目里把tool、StructuredTool、Tool类、BaseTool子类、以及convert_runnable_to_tool这五条路都走了一遍踩的坑从参数 schema 不识别到异步调用直接崩再到流式输出把工具结果吞掉基本把能踩的都踩了。这篇文章不是官方文档的复述而是我把这五种方式的实际表现、适用边界、以及那些文档里不会写的细节整理出来。如果你正在用 LangChain 做 Agent 开发或者准备把现有函数接进工具链这篇内容能帮你省掉至少两天的调试时间。我会从每种方式的定义写法讲起重点说清楚参数校验、异步支持、流式兼容、以及和不同 Agent 类型的配合情况最后给一张对照表方便你直接选型。2. 为什么工具定义方式值得单独拿出来讲2.1 工具定义直接决定 Agent 能不能正确调用很多人觉得工具定义就是写个函数加个装饰器但实际跑起来会发现Agent 调不调你的工具、传什么参数、拿到结果后怎么处理全都取决于工具定义阶段暴露出来的 schema。我遇到过最典型的情况是用tool装饰一个带默认参数的函数Agent 死活不传那个参数最后发现是 schema 里默认值没被正确识别模型以为那个参数是必填的干脆就不调了。还有一次用Tool类手动拼 description结果模型把 description 里的示例参数当成真实参数传进来直接报参数不匹配。这些问题的根源在于LangChain 的工具定义方式不止一种每种方式生成的 schema 结构、参数解析逻辑、以及和 Agent 的交互协议都有细微差别。官方文档虽然列了这几种方式但很少讲清楚什么场景该用哪种更不会告诉你哪种方式在异步、流式、或者多参数场景下会出问题。2.2 五种方式的核心差异在哪里先给一个整体判断tool装饰器适合快速把简单函数变成工具StructuredTool适合参数复杂、需要严格校验的场景Tool类适合需要完全手动控制输入输出的老式写法BaseTool子类适合需要复用状态或复杂初始化的工具convert_runnable_to_tool则是把现有 Runnable 链直接转成工具适合已经在用 LCEL 的项目。这五种方式在参数 schema 生成、异步支持、流式兼容、错误处理四个维度上表现完全不同。比如tool默认用函数签名生成 schema但如果你函数里有*args或**kwargsschema 就会退化成空对象Agent 根本不知道传什么。StructuredTool则强制你显式定义 args_schema虽然麻烦但可控。Tool类最原始输入就是一个字符串适合那种只接受单一文本输入的工具但多参数场景下你得自己解析字符串很容易出错。2.3 选错方式会带来哪些连锁问题选错工具定义方式最直接的后果是 Agent 调用失败率飙升。我统计过一个小样本用tool定义带三个以上参数的工具时如果参数名不够语义化模型传错参数的概率大概在 15% 左右。换成StructuredTool并给每个参数加上 description 后错误率降到 3% 以下。另一个连锁问题是异步支持如果你用Tool类定义了一个同步函数但在异步 Agent 里调用整个事件循环会被阻塞表现就是 Agent 响应突然变慢甚至超时。还有一个容易被忽略的点是流式输出。LangChain 的流式模式对工具定义有额外要求某些方式定义的工具在流式模式下会直接把结果吞掉前端什么都收不到。这个问题在tool和convert_runnable_to_tool上都出现过后面会详细说。3. 五种工具定义方式逐一拆解3.1 tool 装饰器最顺手但坑也最隐蔽tool是大多数人第一个接触的方式写法简单from langchain_core.tools import tool tool def search_weather(city: str, date: str today) - str: 查询指定城市指定日期的天气。 return f{city} {date} 晴25度这个工具定义看起来没问题但实际跑起来有几个坑。第一个坑是 docstring 的格式。LangChain 会把 docstring 作为工具描述传给模型但如果你 docstring 写得太随意比如只写“查天气”模型可能不知道这个工具需要城市和日期两个参数。我试过把 docstring 写成“查询天气”结果模型只传城市不传日期因为日期有默认值模型觉得可以不传。后来把 docstring 改成“查询指定城市指定日期的天气日期默认为今天”模型才开始正确传参。第二个坑是参数类型注解。如果你不写类型注解LangChain 会默认参数是字符串但模型可能传数字进来导致类型错误。比如def calculate(a, b)不写注解模型传a1, b2实际收到的是字符串1和2做加法会变成字符串拼接。这个坑我在一个计算器工具上踩过排查了半天才发现是类型注解缺失。第三个坑是tool对异步函数的支持。如果你用async def定义工具tool能识别但前提是你用的 Agent 也支持异步。我试过在同步 Agent 里挂异步工具调用直接报错说 coroutine 不能被序列化。反过来同步工具挂在异步 Agent 里虽然能跑但会阻塞事件循环高并发场景下性能掉得厉害。注意用tool时函数名会成为工具名模型靠这个名字决定调不调。所以函数名要语义化别用func1、do_stuff这种。3.2 StructuredTool参数复杂时的首选但定义成本高StructuredTool的核心优势是你可以显式定义 args_schema用 Pydantic 模型描述每个参数的类型、描述、默认值。写法如下from langchain_core.tools import StructuredTool from pydantic import BaseModel, Field class WeatherInput(BaseModel): city: str Field(description城市名称例如北京) date: str Field(defaulttoday, description日期格式 YYYY-MM-DD) def get_weather(city: str, date: str) - str: return f{city} {date} 晴 weather_tool StructuredTool.from_function( funcget_weather, nameget_weather, description查询指定城市指定日期的天气, args_schemaWeatherInput, )这种方式的好处是 schema 完全可控模型拿到的参数描述非常清晰调用准确率明显提升。但坑在于from_function的func参数和args_schema必须严格对应。我遇到过args_schema里定义了三个字段但func只接受两个参数运行时报参数数量不匹配。还有一次args_schema里字段名用了驼峰func参数用了下划线结果传参时直接 KeyError。另一个坑是StructuredTool的异步支持。如果你用from_function传一个同步函数它默认是同步工具。要支持异步得用coroutine参数传异步函数或者用from_function的func传同步、coroutine传异步两个都传。我试过只传coroutine不传func结果同步调用时报错说没有同步实现。所以如果你的工具可能被同步和异步两种 Agent 调用最好两个都提供。3.3 Tool 类老式写法适合单一文本输入Tool类是最原始的写法输入就是一个字符串输出也是一个字符串。写法如下from langchain_core.tools import Tool def search(query: str) - str: return f搜索结果{query} search_tool Tool( namesearch, funcsearch, description输入搜索关键词返回搜索结果, )这种方式适合那种只接受单一文本输入的工具比如搜索、翻译、摘要。但坑在于多参数场景。如果你需要传多个参数得在func里自己解析字符串比如让模型传city北京,date2024-01-01然后你 split 解析。这种方式非常容易出错模型可能传北京 2024-01-01也可能传北京,2024-01-01格式完全不固定。我试过用这种方式做一个多参数工具调试了一下午最后换成StructuredTool才稳定。另一个坑是Tool类不支持参数校验。模型传什么进来你的函数就收到什么类型错误、缺参数、多参数全都得自己处理。如果你在func里没做防御性编程一个空字符串就能让整个 Agent 崩掉。3.4 BaseTool 子类适合需要状态或复杂初始化的工具BaseTool子类是最灵活的方式你可以完全控制工具的初始化、参数解析、同步和异步执行。写法如下from langchain_core.tools import BaseTool from pydantic import BaseModel, Field from typing import Type class CalculatorInput(BaseModel): expression: str Field(description数学表达式例如 12*3) class CalculatorTool(BaseTool): name: str calculator description: str 计算数学表达式 args_schema: Type[BaseModel] CalculatorInput def _run(self, expression: str) - str: try: return str(eval(expression)) except Exception as e: return f计算错误{e} async def _arun(self, expression: str) - str: return self._run(expression)这种方式适合需要维护内部状态、或者初始化时需要加载模型、连接数据库的工具。坑在于_run和_arun的返回值必须是字符串或可序列化对象如果你返回一个自定义对象Agent 拿到后可能不知道怎么处理。我试过返回一个 dict结果 Agent 直接把 dict 的字符串表示当结果后面解析全乱套。另一个坑是args_schema的类型注解。如果你不写Type[BaseModel]Pydantic 可能不认导致 schema 生成失败。还有name和description必须是类属性不能是实例属性否则 Pydantic 校验会报错。3.5 convert_runnable_to_toolLCEL 项目的快捷通道如果你已经在用 LCEL 构建链convert_runnable_to_tool可以把一个 Runnable 直接转成工具。写法如下from langchain_core.tools import convert_runnable_to_tool from langchain_core.runnables import RunnableLambda chain RunnableLambda(lambda x: f处理结果{x[input]}) tool convert_runnable_to_tool( chain, namemy_chain, description处理输入并返回结果, arg_types{input: str}, )这种方式的好处是复用现有链不用重新包装函数。但坑在于arg_types必须和 Runnable 的输入 schema 严格对应。我试过arg_types只写了一个字段但 Runnable 实际需要两个字段调用时直接 KeyError。还有一次 Runnable 的输入是 Pydantic 模型arg_types写成了 dict结果 schema 生成失败。另一个坑是流式兼容。convert_runnable_to_tool转出来的工具在流式模式下如果 Runnable 本身不支持流式工具结果会被缓冲前端收不到中间输出。这个问题在需要实时反馈的场景下很致命。4. 五种方式横向对比与选型建议4.1 参数校验能力对比定义方式参数校验schema 生成多参数支持类型安全tool弱自动一般中StructuredTool强显式强高Tool无无弱低BaseTool 子类强显式强高convert_runnable_to_tool中依赖 Runnable中中从参数校验角度看StructuredTool和BaseTool子类最强适合参数复杂、需要严格校验的场景。tool适合参数简单、快速定义的场景。Tool类基本没有校验只适合单一文本输入。convert_runnable_to_tool的校验能力取决于原 Runnable 的输入 schema。4.2 异步与流式支持对比定义方式同步支持异步支持流式兼容阻塞风险tool是是需 async def中中StructuredTool是是需 coroutine中低Tool是否低高BaseTool 子类是是需 _arun高低convert_runnable_to_tool是是依赖 Runnable依赖 Runnable中异步支持最好的是BaseTool子类和StructuredToolTool类完全不支持异步在异步 Agent 里会阻塞。流式兼容最好的是BaseTool子类因为你可以完全控制_run和_arun的输出方式。4.3 选型决策树如果你只是快速原型参数简单用tool。如果参数复杂、需要严格校验用StructuredTool。如果只接受单一文本输入用Tool。如果需要维护状态或复杂初始化用BaseTool子类。如果已经有 LCEL 链用convert_runnable_to_tool。我个人的经验是生产环境优先选StructuredTool或BaseTool子类因为参数校验和异步支持更可靠。tool适合 demo 和简单场景Tool类除非有历史包袱否则不建议在新项目里用。5. 实操中遇到的典型问题与排查记录5.1 参数 schema 不识别Agent 不传参这个问题我遇到过三次每次原因都不一样。第一次是tool装饰的函数有**kwargsschema 退化成空对象模型不知道传什么。解决方法是去掉**kwargs改成显式参数。第二次是StructuredTool的args_schema字段名和func参数名不一致模型按 schema 传参但func收不到。解决方法是统一命名。第三次是BaseTool子类的args_schema没有写Type[BaseModel]注解Pydantic 不认schema 生成失败。解决方法是补上类型注解。排查这类问题的通用方法是先打印工具的args_schema看 schema 是否正确生成。如果 schema 是空的或者字段不对那就是定义阶段的问题。如果 schema 正确但模型还是不传参那就是 description 写得不够清楚模型不知道什么时候该传什么参数。5.2 异步调用报 coroutine 错误这个错误通常出现在同步 Agent 挂异步工具或者异步 Agent 挂同步工具的场景。报错信息类似Object of type coroutine is not JSON serializable。原因是 Agent 在序列化工具结果时拿到了一个 coroutine 对象而不是实际结果。解决方法是确保工具的执行方式和 Agent 的执行方式匹配。同步 Agent 用同步工具异步 Agent 用异步工具。如果必须混用用asyncio.run或run_in_executor包装一下。我试过在异步 Agent 里挂同步工具用run_in_executor包装后阻塞问题解决了但性能还是不如原生异步工具。5.3 流式模式下工具结果被吞这个问题在tool和convert_runnable_to_tool上都出现过。表现是 Agent 在流式模式下调用工具后前端收不到工具的输出只能收到最终结果。原因是某些工具定义方式在流式模式下会把工具结果缓冲起来等整个工具执行完才输出。解决方法是改用BaseTool子类在_run或_arun里用yield逐步输出结果。或者用StructuredTool的coroutine参数传一个异步生成器。我试过用BaseTool子类加yield流式输出正常了但要注意_run的返回值类型要改成Iterator。5.4 常见问题速查表问题现象可能原因解决方法Agent 不调用工具description 不清晰补充工具用途和参数说明参数传错schema 字段名与函数参数不一致统一命名类型错误缺少类型注解补上类型注解异步报错同步异步混用统一执行方式流式无输出工具不支持流式改用 BaseTool 子类参数缺失默认值未识别显式定义 args_schema6. 几个文档里不会写的实操心得6.1 description 的写法比想象中重要工具 description 是模型决定调不调、怎么调的核心依据。我试过同一个工具description 写“查询天气”和写“查询指定城市指定日期的天气城市必填日期可选默认今天”调用准确率差了将近 20%。description 里最好包含三要素工具用途、参数含义、使用场景。如果工具有副作用比如写数据库、发请求也要在 description 里说明避免模型在不该调的时候调。6.2 参数名用英文description 用中文参数名建议用英文因为模型对英文参数名的识别更稳定。description 可以用中文因为中文描述对模型理解参数含义更友好。我试过参数名用拼音模型传参错误率明显上升。也试过 description 用英文模型理解没问题但中文场景下还是中文描述更准确。6.3 工具数量控制在 10 个以内Agent 挂太多工具模型选择困难调用准确率会下降。我试过挂 20 个工具模型经常调错。后来精简到 8 个准确率明显提升。如果工具确实多可以分组用不同的 Agent 处理不同组的工具或者用路由层先选工具组再选具体工具。6.4 错误处理要返回字符串而不是抛异常工具执行出错时最好捕获异常并返回错误信息字符串而不是直接抛异常。因为抛异常会导致整个 Agent 中断而返回错误信息字符串可以让模型根据错误信息决定下一步。我试过在工具里直接raise ValueError结果 Agent 直接崩了。改成return f错误{e}后模型会尝试修正参数或换工具。6.5 测试工具时先用同步模式跑通新定义的工具建议先用同步模式测试确认参数传递、执行逻辑、返回值都正确后再切异步模式。我试过直接上异步结果异步和同步的行为不一致排查起来很麻烦。同步跑通后再切异步问题范围小很多。7. 一个完整的工具定义示例7.1 需求描述假设我们需要一个工具输入城市和日期返回该城市该日期的天气信息。要求支持同步和异步调用参数有校验description 清晰。7.2 用 StructuredTool 实现from langchain_core.tools import StructuredTool from pydantic import BaseModel, Field import asyncio class WeatherInput(BaseModel): city: str Field(description城市名称例如北京、上海) date: str Field(defaulttoday, description日期格式 YYYY-MM-DD默认为今天) def get_weather_sync(city: str, date: str today) - str: if not city: return 错误城市名称不能为空 return f{city} {date} 天气晴25度 async def get_weather_async(city: str, date: str today) - str: await asyncio.sleep(0.1) return get_weather_sync(city, date) weather_tool StructuredTool.from_function( funcget_weather_sync, coroutineget_weather_async, nameget_weather, description查询指定城市指定日期的天气。城市必填日期可选默认为今天。, args_schemaWeatherInput, )这个定义同时提供了同步和异步实现参数校验用 Pydantic 模型description 清晰说明了参数含义和默认值。实测下来模型调用准确率很高异步场景下也不会阻塞。7.3 用 BaseTool 子类实现from langchain_core.tools import BaseTool from pydantic import BaseModel, Field from typing import Type, Iterator import asyncio class WeatherInput(BaseModel): city: str Field(description城市名称例如北京、上海) date: str Field(defaulttoday, description日期格式 YYYY-MM-DD默认为今天) class WeatherTool(BaseTool): name: str get_weather description: str 查询指定城市指定日期的天气。城市必填日期可选默认为今天。 args_schema: Type[BaseModel] WeatherInput def _run(self, city: str, date: str today) - str: if not city: return 错误城市名称不能为空 return f{city} {date} 天气晴25度 async def _arun(self, city: str, date: str today) - str: await asyncio.sleep(0.1) return self._run(city, date)这种方式更灵活适合需要维护状态或复杂初始化的场景。比如你可以在__init__里加载模型、连接数据库然后在_run里使用这些资源。8. 最后再分享几个小技巧工具定义完成后建议写一个简单的测试脚本模拟模型传参验证工具能否正确执行。我通常会测三种情况正常参数、缺参数、错误类型参数。正常参数看返回值缺参数看默认值是否生效错误类型参数看是否有防御性处理。另外工具定义不要频繁改动。每次改动后模型的调用行为可能变化需要重新测试。如果必须改建议先在小范围测试确认没问题再上线。还有一个技巧是给工具加版本号。比如get_weather_v2这样新旧工具可以共存方便灰度切换。我试过直接改工具名结果旧 Agent 找不到工具直接报错。加版本号后新旧 Agent 各用各的互不影响。工具定义这件事看起来简单但细节很多。五种方式各有适用场景选对了能省很多事选错了就是无尽的调试。希望这篇总结能帮你少踩几个坑。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →