news 2026/5/6 6:52:27

## 014、LangChain 中的 Tool 开发:自定义工具与第三方工具集成

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
## 014、LangChain 中的 Tool 开发:自定义工具与第三方工具集成

昨天凌晨三点,我被线上一个 Agent 的报警吵醒。日志里反复出现一条错误:ToolInputParsingException: Could not parse tool input。排查下来,问题出在一个自定义工具上——我写了一个查询天气的 Tool,返回的是 JSON 字符串,但 LLM 在调用时传进来的参数格式跟预期对不上。LLM 以为它传的是{"location": "Beijing"},但我的工具函数签名里写的是def run(self, location: str),LangChain 的默认解析器直接把整个字典当成了一个字符串参数塞了进去。结果就是,工具收到了"{'location': 'Beijing'}"这个字符串,而不是一个字典,自然就炸了。

这个坑让我意识到,很多人(包括当时的我)对 LangChain 中 Tool 的底层机制理解得不够深。今天这篇笔记,我们就从这个问题出发,把自定义工具和第三方工具集成的细节彻底捋一遍。

1. Tool 的本质:一个带“说明书”的函数

LangChain 里的 Tool,本质上就是一个函数,外加一份给 LLM 看的“说明书”。这个说明书就是namedescription。LLM 根据 description 决定要不要调用这个工具,根据 name 决定调用哪个工具,然后根据函数的参数签名(或者你手动定义的 args_schema)来生成调用参数。

所以,开发一个自定义工具,核心就两件事:写好函数逻辑写好说明书。说明书写不好,LLM 要么不调用,要么乱调用。

2. 自定义工具:三种写法,各有坑

LangChain 提供了三种定义 Tool 的方式,我按推荐程度排个序。

方式一:@tool 装饰器(最推荐,但要注意类型注解)

fromlangchain.toolsimporttool@tooldefget_weather(location:str)->str:"""查询指定城市的天气。输入应为城市名称,例如 'Beijing' 或 'Shanghai'。"""# 这里踩过坑:如果返回的是 dict,LLM 可能无法直接处理# 最好统一返回字符串returnf"{location}的天气是晴天,25°C"

这个写法最简洁,LangChain 会自动从函数签名和 docstring 里提取参数信息。但有个隐藏问题:如果你的参数是location: str = "Beijing"(给了默认值),LLM 可能会偷懒不传参,直接使用默认值。所以,别给工具参数设默认值,除非你确定 LLM 的行为符合预期。

方式二:继承 BaseTool 类(最灵活,但代码多)

fromlangchain.toolsimportBaseToolfrompydanticimportBaseModel,FieldclassWeatherInput(BaseModel):location:str=Field(description="城市名称,如 'Beijing'")classWeatherTool(BaseTool):name="get_weather"description="查询指定城市的天气"args_schema:type[BaseModel]=WeatherInputdef_run(self,location:str)->str:# 实际业务逻辑returnf"{location}的天气是晴天,25°C"asyncdef_arun(self,location:str)->str:# 异步版本,如果不需要可以 raise NotImplementedErrorraiseNotImplementedError("异步调用暂不支持")

这种方式的好处是你可以精确控制参数结构。比如你的工具需要同时接收locationdate两个参数,用 Pydantic 模型定义就非常清晰。而且,args_schema里每个字段的description会被 LLM 看到,这能显著提高参数生成的准确率。

方式三:直接实例化 Tool 类(最不推荐,容易出问题)

fromlangchain.toolsimportTooldefweather_func(location:str)->str:returnf"{location}的天气是晴天,25°C"weather_tool=Tool(name="get_weather",func=weather_func,description="查询指定城市的天气。输入应为城市名称。")

这种写法看着简单,但问题在于description里没有明确告诉 LLM 参数格式。LLM 可能会传{"location": "Beijing"}这种字典,而你的函数只接受字符串,就会触发我开头说的那个解析异常。除非你的函数只有一个参数且参数名非常直观,否则别用这种写法。

3. 第三方工具集成:别自己造轮子

LangChain 社区已经集成了大量第三方工具,从 SerpAPI 到 Wikipedia,从 Wolfram Alpha 到 YouTube 搜索。集成方式很简单:

fromlangchain_community.toolsimportWikipediaQueryRunfromlangchain_community.utilitiesimportWikipediaAPIWrapper wikipedia=WikipediaQueryRun(api_wrapper=WikipediaAPIWrapper(top_k_results=1,doc_content_chars_max=500))

但这里有个容易忽略的点:第三方工具的 description 可能不适合你的场景。比如 Wikipedia 工具的默认 description 是“A wrapper around Wikipedia. Useful for when you need to answer general questions about people, places, companies, facts, historical events, or other subjects.” 这个描述太长,LLM 可能会被误导。我习惯在集成后手动修改 description:

wikipedia.description="搜索维基百科,获取人物、地点、事件等知识。输入应为搜索关键词。"

另外,很多第三方工具需要 API Key。别硬编码在代码里,用环境变量。我见过有人把 API Key 直接写在 Tool 的__init__方法里,然后代码上传到 GitHub 公开仓库——别这样写。

4. 工具链的“暗坑”:参数传递与错误处理

回到开头的那个问题。为什么 LLM 传的参数格式会跟工具预期的不一致?因为 LLM 看到的“说明书”是descriptionargs_schema,但实际执行时,LangChain 内部有一个ToolInputParser负责把 LLM 输出的字符串(通常是 JSON)解析成 Python 对象。

如果你的工具参数简单(比如只有一个字符串),LLM 可能会直接输出"Beijing"而不是{"location": "Beijing"}。这时候默认的解析器会尝试把"Beijing"当成 JSON 解析,结果失败。

解决方案有两个:

  1. 在 description 里明确告诉 LLM 参数格式。比如:“输入应为 JSON 格式,例如{\"location\": \"Beijing\"}”。但 LLM 不一定每次都听话。
  2. 使用 Pydantic 模型定义 args_schema,并设置strict=True。这样 LangChain 会强制 LLM 输出符合 schema 的 JSON。

我推荐第二种,因为更可靠。但要注意,strict=True会让 LLM 在无法生成合法 JSON 时直接报错,而不是尝试“猜”一个格式。这其实更好——宁可让 Agent 明确失败,也不要让它带着错误数据继续执行。

5. 调试技巧:让 LLM 的“思考过程”可见

当你发现工具调用不正常时,第一件事不是改代码,而是看 LLM 到底输出了什么。在 LangChain 中,可以通过设置verbose=True来打印 LLM 的完整输出:

agent=initialize_agent(tools=[weather_tool],llm=llm,agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION,verbose=True# 这里踩过坑:生产环境记得关掉,否则日志会爆炸)

你会看到类似这样的输出:

> Entering new AgentExecutor chain... Thought: 用户想知道北京的天气,我需要调用 get_weather 工具。 Action: get_weather Action Input: "Beijing" Observation: Beijing 的天气是晴天,25°C Thought: 我已经得到了结果,可以回答用户了。 Final Answer: 北京今天天气晴朗,气温25°C。

如果Action Input的格式跟你预期的不一样,那就是 description 或 args_schema 的问题。如果Observation是空或者报错,那就是工具函数本身的问题。

6. 个人经验:工具开发的三条铁律

  1. 工具函数的返回值必须是字符串。不要返回 dict、list 或自定义对象。LLM 只能理解文本,你返回一个 Python 对象,它看不懂。如果必须返回结构化数据,用 JSON 字符串,并在 description 里说明格式。

  2. 每个工具只做一件事。我见过有人写一个execute_sql工具,既能查询又能修改还能删除。这很危险——LLM 可能会在用户只要求查询时执行了删除操作。把查询、修改、删除拆成三个独立的工具,每个工具的 description 里明确说明它的职责和限制。

  3. 给工具加“护栏”。比如一个发送邮件的工具,在_run方法里检查收件人地址是否在白名单中。LLM 可能会被 prompt injection 攻击,诱导它发送恶意邮件。工具函数是你最后的防线,别完全信任 LLM 的输入。

最后,关于第三方工具集成,我的建议是:能用现成的就别自己写。LangChain 社区维护的第三方工具经过了大量用户的测试,稳定性比自己写的要高。但一定要检查它的 description 和参数定义是否符合你的需求,必要时手动修改。

下一篇我会讲 Agent 的“记忆”机制——为什么你的 Agent 总是“记不住”之前说过的话,以及如何让它在长对话中保持上下文连贯。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/6 6:51:26

Ruby on Rails构建旅游搜索聚合器:架构设计与爬虫实战

1. 项目概述:一个俄罗斯旅游搜索工具的诞生最近在GitHub上看到一个挺有意思的项目,叫“travel-search-ru”。光看名字,大概就能猜到这是一个和俄罗斯旅游搜索相关的工具。作为一个经常需要处理多语言、多数据源信息的开发者,我对这…

作者头像 李华
网站建设 2026/5/6 6:44:31

使用Taotoken CLI工具一键配置团队开发环境中的模型访问参数

使用Taotoken CLI工具一键配置团队开发环境中的模型访问参数 1. 安装Taotoken CLI工具 Taotoken提供的CLI工具支持通过npm全局安装或使用npx临时调用。对于需要频繁使用CLI的团队成员,推荐全局安装: npm install -g taotoken/taotoken若仅需临时执行配…

作者头像 李华
网站建设 2026/5/6 6:44:31

告别重复劳动:用快马ai生成自动化脚本,极速部署与测试opencl计算环境

作为一名经常需要折腾OpenCL环境的开发者,我深刻理解在不同设备上重复配置环境的痛苦。每次换新机器或者重装系统,都要花大量时间查文档、找驱动、调试兼容性问题。最近发现InsCode(快马)平台可以智能生成自动化脚本,终于让我告别了这些重复劳…

作者头像 李华
网站建设 2026/5/6 6:42:29

Swoole调试避坑指南:97%开发者踩过的3类致命陷阱及秒级修复方案

更多请点击: https://intelliparadigm.com 第一章:Swoole调试避坑指南:97%开发者踩过的3类致命陷阱及秒级修复方案 协程上下文丢失导致的变量污染 在 Swoole 4.8 中,若在协程内直接使用全局静态变量或单例对象存储请求上下文&am…

作者头像 李华
网站建设 2026/5/6 6:41:21

快马AI助力:一键生成fishros自动化部署脚本,快速搭建ROS2开发原型

最近在折腾ROS2开发环境搭建,发现手动配置各种依赖和工具链特别耗时。尤其是不同Ubuntu版本对应不同ROS2发行版,新手很容易踩坑。好在发现了fishros这个神器,它提供了一键安装脚本,能自动完成大部分配置工作。不过直接使用原版脚本…

作者头像 李华