LangChain Tools深度解析:让Agent拥有超能力
前言
这篇是 LangChain 系列的学习笔记,这次整理的是第五章 Tools(工具)模块。
说到 Tools,这可是 Agent 的核心能力。你想想,大模型再强大也只是会"说话"而已,它不能帮你移动文件、不能帮你查询天气、不能帮你计算复杂的数学公式。但有了 Tools,大模型就像获得了"手"一样,可以真正做一些实际的操作了。
本篇会详细介绍如何定义自定义工具、如何让大模型分析工具调用,以及完整的工具调用流程。你会发现,原来给 Agent 赋予"超能力"是这么简单的事儿。
🏠个人主页:山沐与山
文章目录
- 一、什么是Tools?
- 二、自定义工具:@tool装饰器
- [2.1 最基础的用法](#2.1 最基础的用法)
- [2.2 带参数的装饰器](#2.2 带参数的装饰器)
- [2.3 使用Pydantic精确控制参数](#2.3 使用Pydantic精确控制参数)
- 三、StructuredTool.from_function()方式
- [3.1 基础用法](#3.1 基础用法)
- [3.2 使用自定义参数描述](#3.2 使用自定义参数描述)
- [3.3 @tool vs StructuredTool:该用哪个?](#3.3 @tool vs StructuredTool:该用哪个?)
- 四、大模型如何分析工具调用
- [4.1 工具转换为函数格式](#4.1 工具转换为函数格式)
- [4.2 大模型的分析结果](#4.2 大模型的分析结果)
- [4.3 完整的工作流程](#4.3 完整的工作流程)
- 五、实际调用工具
- [5.1 解析function_call](#5.1 解析function_call)
- [5.2 根据工具名称调用对应工具](#5.2 根据工具名称调用对应工具)
- [5.3 完整的调用流程](#5.3 完整的调用流程)
- 六、常见问题
- 七、总结
- 热门专栏推荐
一、什么是 Tools?
在 LangChain 中,Tools(工具)是指可以被大模型调用的函数或接口。听起来有点抽象?举个例子就明白了:
假设你问 ChatGPT:"帮我把桌面上的 report.pdf 移动到 Documents 文件夹"。ChatGPT 会回答:"我无法直接操作您的文件系统,但您可以手动进行以下操作......"
为什么它做不到?因为它只是个语言模型,没有操作系统权限。
但在 LangChain 里,我们可以给大模型定义一个"移动文件"的工具。当用户发出同样的请求时,大模型会:
- 分析用户意图:"哦,用户想移动文件"
- 选择对应的工具:"我有一个
MoveFileTool" - 提取参数:"源路径是
Desktop/report.pdf,目标路径是Documents/" - 调用工具,真正执行移动操作
这就是 Tools 的作用。
二、自定义工具:@tool 装饰器
定义一个工具最简单的方式就是用 @tool 装饰器。
2.1 最基础的用法
代码来自 01-自定义工具.ipynb:
python
from langchain_core.tools import tool
@tool
def add_numbers(a: int, b: int) -> int:
"""计算两个整数的和"""
return a + b
# 看看这个工具的属性
print(f"工具名称: {add_numbers.name}")
print(f"工具参数: {add_numbers.args}")
print(f"工具描述: {add_numbers.description}")
print(f"直接返回: {add_numbers.return_direct}")
输出:
工具名称: add_numbers
工具参数: {'a': {'title': 'A', 'type': 'integer'}, 'b': {'title': 'B', 'type': 'integer'}}
工具描述: 计算两个整数的和
直接返回: False
看到没有,你只需要:
- 写一个普通函数
- 加上类型注解(
a: int, b: int -> int) - 写个
docstring(三引号注释) - 加个
@tool装饰器
LangChain 就会自动把它变成一个工具!工具的描述会自动从 docstring 里提取,参数信息会从类型注解里推断。
2.2 带参数的装饰器
如果你想自定义工具的名字、描述等属性,可以给 @tool 传参数:
python
@tool(
name_or_callable="add_two_numbers", # 自定义工具名称
return_direct=True, # 是否直接返回结果
description="这是一个计算两个整数和的函数" # 自定义描述
)
def add_numbers(a: int, b: int) -> int:
"""计算两个整数的和"""
return a + b
print(f"工具名称: {add_numbers.name}")
print(f"直接返回: {add_numbers.return_direct}")
print(f"工具描述: {add_numbers.description}")
输出:
工具名称: add_two_numbers
直接返回: True
工具描述: 这是一个计算两个整数和的函数
这里的 return_direct=True 是个挺有意思的参数。如果设置为 True,Agent 调用这个工具后会直接返回结果,不会再做额外处理。如果是 False(默认值),Agent 可能会拿着结果继续思考、继续调用其他工具。
2.3 使用 Pydantic 精确控制参数
有时候你想给参数加更详细的说明,让大模型更容易理解这个参数的含义。这时候可以用 Pydantic 的 BaseModel:
python
from pydantic import BaseModel, Field
class FieldInfo(BaseModel):
a: int = Field(description="第一个整型参数")
b: int = Field(description="第二个整型参数")
@tool(
name_or_callable="add_two_numbers",
return_direct=True,
description="这是一个计算两个整数和的函数",
args_schema=FieldInfo # 使用 Pydantic 模型定义参数
)
def add_numbers(a: int, b: int) -> int:
"""计算两个整数的和"""
return a + b
print(add_numbers.args)
输出:
{
'a': {'description': '第一个整型参数', 'type': 'integer'},
'b': {'description': '第二个整型参数', 'type': 'integer'}
}
这样一来,大模型在分析用户输入时,会更清楚每个参数的含义,调用工具的准确度也会提高。
三、StructuredTool.from_function() 方式
除了 @tool 装饰器,还有一种更灵活的方式:StructuredTool.from_function()。
3.1 基础用法
代码来自 01-自定义工具.ipynb:
python
from langchain_core.tools import StructuredTool
def search_engine(query: str):
"""一个搜索引擎的模拟函数"""
return "最后查询到的结果"
# 用 StructuredTool 包装函数
search_tool = StructuredTool.from_function(
func=search_engine,
name="search_engine",
description="一个搜索引擎的模拟函数",
return_direct=True
)
# 调用工具
result = search_tool.invoke({"query": "Python"})
print(result) # 输出: 最后查询到的结果
这种方式的好处是,你可以把函数定义和工具配置分开。如果你有一些现成的函数不想改(比如来自第三方库),用这种方式就很方便。
3.2 使用自定义参数描述
同样,你也可以用 Pydantic 给参数加详细描述:
python
from pydantic import BaseModel, Field
class FieldInfo(BaseModel):
query: str = Field(description="搜索的关键词")
search_tool01 = StructuredTool.from_function(
func=search_engine,
name="search_engine",
description="一个搜索引擎的模拟函数",
return_direct=True,
args_schema=FieldInfo
)
print(search_tool01.args)
输出:
{'query': {'description': '搜索的关键词', 'type': 'string'}}
3.3 @tool vs StructuredTool:该用哪个?
其实这俩没啥本质区别,看你喜欢哪种风格:
@tool装饰器:写法简洁,适合快速定义新工具StructuredTool.from_function():配置灵活,适合包装已有函数
我个人更喜欢 @tool,因为看起来更简洁。但如果是要包装第三方库的函数,还是 StructuredTool 更方便。
四、大模型如何分析工具调用
定义工具只是第一步,更重要的是让大模型能够理解这些工具,并在合适的时候调用它们。
4.1 工具转换为函数格式
大模型(比如 GPT-4)本身支持 Function Calling 功能,但它需要特定的格式。LangChain 提供了一个工具函数来做转换:
代码来自 02-大模型分析工具的调用.ipynb:
python
from langchain_core.utils.function_calling import convert_to_openai_function
from langchain_community.tools import MoveFileTool
from langchain_core.messages import HumanMessage
from langchain_openai import ChatOpenAI
import os
import dotenv
# 加载环境变量
dotenv.load_dotenv()
# 初始化大模型
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
# 创建工具列表
tools = [MoveFileTool()]
# 转换为 OpenAI 函数格式
functions = [convert_to_openai_function(tool) for tool in tools]
# 构造用户消息
messages = [HumanMessage(content="将文件a移动到桌面")]
# 调用大模型(传入 functions 参数)
response = llm.invoke(input=messages, functions=functions)
print(response)
注意这里的关键点:
- 使用
convert_to_openai_function()把LangChain的工具转换成OpenAI的函数格式 - 调用
llm.invoke()时传入functions参数(不是tools!)
4.2 大模型的分析结果
运行上面的代码,大模型会返回一个 AIMessage 对象。这个对象有两种可能的情况:
情况 1:找到匹配的工具
python
AIMessage(
content='', # 内容为空,因为大模型决定调用工具
additional_kwargs={
'function_call': {
'name': 'move_file',
'arguments': '{"source_path":"a","destination_path":"/Users/YourUsername/Desktop/a"}'
}
}
)
看到了吗?content 是空的,因为大模型没有生成文本回复,而是决定调用 move_file 这个工具。所有调用信息都在 additional_kwargs['function_call'] 里:
name:要调用的工具名称arguments:JSON格式的参数
情况 2:没找到匹配的工具
如果用户问的是:"查询上海明天的天气信息",但你只提供了 MoveFileTool,那么大模型会发现没有合适的工具,于是直接返回文本回复:
python
messages = [HumanMessage(content="查询上海明天的天气信息")]
response = llm.invoke(input=messages, functions=functions)
print(response)
输出:
python
AIMessage(
content='抱歉,我无法提供实时的天气信息。建议您查看天气预报网站或使用天气应用程序获取上海明天的天气信息。',
additional_kwargs={} # 没有 function_call
)
所以判断大模型是否要调用工具,只需要检查 additional_kwargs 里是否有 function_call 字段。
4.3 完整的工作流程
让我用图示表示一下整个流程:
用户输入: "将文件 a 移动到桌面"
↓
传给大模型 (附带 functions 列表)
↓
大模型分析:
- 用户想做什么?移动文件
- 我有什么工具?MoveFileTool
- 匹配吗?匹配!
- 需要什么参数?source_path 和 destination_path
↓
返回 AIMessage:
- content: '' (空)
- function_call: {
name: 'move_file',
arguments: '{"source_path":"a", "destination_path":"/Users/.../Desktop/a"}'
}
五、实际调用工具
前面我们说了,大模型只是"分析"出要调用哪个工具,但工具并没有真正执行。这是大模型和 Agent 的区别:
- 大模型 :只负责分析,生成
function_call指令 Agent:不仅分析,还会真正执行工具
在这个章节,我们先手动执行工具,下一章讲 Agent 时就能自动执行了。
5.1 解析 function_call
代码来自 02-大模型分析工具的调用.ipynb:
python
import json
# 检查是否有 function_call
if "function_call" in response.additional_kwargs:
# 提取工具名称和参数
tool_name = response.additional_kwargs["function_call"]["name"]
tool_args = json.loads(response.additional_kwargs["function_call"]["arguments"])
print(f"函数名/工具名: {tool_name}")
print(f"函数参数: {tool_args}")
else:
# 没有 function_call,说明大模型直接回复了
print(f"模型回复: {response.content}")
输出:
函数名/工具名: move_file
函数参数: {'source_path': 'a.txt', 'destination_path': 'C:\\Users\\Chenxr\\Desktop\\a.txt'}
这里用 json.loads() 把 arguments 字符串解析成 Python 字典,方便后续使用。
5.2 根据工具名称调用对应工具
有了工具名称和参数,接下来就是调用真正的工具了:
python
if "move_file" in response.additional_kwargs["function_call"]["name"]:
# 创建工具实例
tool = MoveFileTool()
# 调用工具
result = tool.run(tool_args)
print(f"工具调用结果: {result}")
输出:
工具调用结果: File moved successfully from a.txt to C:\Users\Chenxr\Desktop\a.txt.
这样,文件就真的被移动了!
5.3 完整的调用流程
把前面的代码串起来,完整的流程是这样的:
python
from langchain_core.utils.function_calling import convert_to_openai_function
from langchain_community.tools import MoveFileTool
from langchain_core.messages import HumanMessage
from langchain_openai import ChatOpenAI
import json
import dotenv
dotenv.load_dotenv()
# 1. 准备工具
tools = [MoveFileTool()]
functions = [convert_to_openai_function(tool) for tool in tools]
# 2. 初始化大模型
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
# 3. 用户输入
messages = [HumanMessage(content="将文件a.txt移动到桌面")]
# 4. 大模型分析
response = llm.invoke(input=messages, functions=functions)
# 5. 检查是否需要调用工具
if "function_call" in response.additional_kwargs:
tool_name = response.additional_kwargs["function_call"]["name"]
tool_args = json.loads(response.additional_kwargs["function_call"]["arguments"])
print(f"工具名: {tool_name}")
print(f"参数: {tool_args}")
# 6. 调用工具
if tool_name == "move_file":
tool = MoveFileTool()
result = tool.run(tool_args)
print(f"结果: {result}")
else:
# 没有匹配的工具,输出大模型的回复
print(f"模型回复: {response.content}")
这就是 Tools 的完整工作流程。虽然这里是手动调用,但这正是 Agent 内部的运行逻辑。
六、常见问题
学 Tools 过程中,这几个问题经常被问到:
6.1 @tool 和 StructuredTool 该用哪个?
看场景选择:
| 场景 | 推荐 | 原因 |
|---|---|---|
| 新写的函数 | @tool 装饰器 |
简洁、直观,代码更清晰 |
| 包装已有函数(如第三方库) | StructuredTool.from_function() |
不需要修改原函数 |
| 需要复杂参数验证 | 都可以 + Pydantic |
通过 args_schema 精确控制 |
6.2 为什么给参数加 description 这么重要?
因为大模型是靠"理解"来调用工具的。
没有描述时,大模型只能通过参数名猜测含义,容易出错。比如参数名是 path,大模型不知道是源路径还是目标路径。
python
# ❌ 不好:大模型不知道 path 是什么
def move_file(path: str):
pass
# ✅ 好:清楚明确
class MoveFileArgs(BaseModel):
source_path: str = Field(description="要移动的文件的原始路径")
dest_path: str = Field(description="文件的目标位置路径")
通过 Pydantic 的 Field() 给参数加详细描述,可以显著提高调用准确率。
6.3 大模型调用工具和 Agent 有什么区别?
关键区别:谁来执行工具。
| 对比项 | 大模型 + functions 参数 |
Agent |
|---|---|---|
| 分析用户意图 | ✅ | ✅ |
生成 function_call |
✅ | ✅ |
| 实际执行工具 | ❌ 需要手动执行 | ✅ 自动执行 |
| 多轮调用 | ❌ | ✅ |
大模型只是"建议"调用什么工具,返回 function_call 指令。而 Agent 会真的去调用工具,还能根据结果决定是否继续调用其他工具。
6.4 调用工具时报错 "function_call not found" 怎么办?
检查这几点:
-
传参名称错误 :应该是
functions不是toolspython# ❌ 错误 response = llm.invoke(messages, tools=functions) # ✅ 正确 response = llm.invoke(messages, functions=functions) -
模型不支持 :确保使用支持
Function Calling的模型(如gpt-4o-mini、gpt-4等) -
工具格式转换 :确保使用了
convert_to_openai_function()进行转换pythonfunctions = [convert_to_openai_function(tool) for tool in tools]
七、总结
这篇文章介绍了 LangChain 的 Tools 模块,核心内容包括:
- 什么是 Tools:可以被大模型调用的函数或接口,让大模型拥有实际操作能力
- 定义工具的方式 :
@tool装饰器和StructuredTool.from_function()两种方式 - 参数精确控制 :通过
Pydantic的Field()给参数加详细描述 - 工具调用流程:定义工具 → 转换格式 → 大模型分析 → 解析结果 → 执行工具
- 关键区别 :大模型只做分析,
Agent会真正执行工具
关键要点总结表:
| 概念 | 说明 | 适用场景 | 注意事项 |
|---|---|---|---|
@tool 装饰器 |
最简单的定义方式 | 新写的工具函数 | 需要类型注解和 docstring |
StructuredTool |
灵活的包装方式 | 包装已有函数 | 适合第三方库函数 |
Pydantic 参数描述 |
精确控制参数信息 | 参数含义不明确时 | 显著提高调用准确率 |
Function Calling |
OpenAI 的工具调用机制 |
所有需要调用工具的场景 | 传参名是 functions 不是 tools |
下一步 :有了 Tools,接下来就是 Agent 了。Agent 可以自动完成工具的分析和执行,还能进行多轮的工具调用,实现"计划 → 执行 → 观察 → 再计划"的循环。
热门专栏推荐
- Agent小册
- 服务器部署
- Java基础合集
- Python基础合集
- Go基础合集
- 大数据合集
- 前端小册
- 数据库合集
- Redis 合集
- Spring 全家桶
- 微服务全家桶
- 数据结构与算法合集
- 设计模式小册
- 消息队列合集
等等等还有许多优秀的合集在主页等着大家的光顾,感谢大家的支持
文章到这里就结束了,如果有什么疑问的地方请指出,诸佬们一起来评论区一起讨论😊
希望能和诸佬们一起努力,今后我们一起观看感谢您的阅读🙏
如果帮助到您不妨3连支持一下,创造不易您们的支持是我的动力🌟