LangChain 学习笔记(五):Tools 工具调用
本文基于《尚硅谷 LangChain 1.2》第5章整理,并结合实际开发经验进行补充。
本章不仅介绍如何定义工具,更重要的是理解 Tools 的底层机制:Message 流转、bind_tools 原理、以及 AI 如何自主决定调用工具。
一、本章学习目标
学习完本章,你应该能够:
- 理解 Tools 的本质:将大模型从"认识世界"推向"改变世界"
- 掌握两种工具定义方式:不使用 @tool 和使用 @tool 装饰器
- 理解 bind_tools() 的工作原理和底层 convert_to_openai_tool
- 掌握完整的工具调用流程(四个步骤)
- 理解 Message 流转:HumanMessage -> AIMessage(tool_calls) -> ToolMessage
- 使用 Pydantic BaseModel 或 JSON Schema 定义复杂参数
- 掌握 tool_choice 控制工具调用行为
- 能够在多工具场景下编写正确的调用循环
- 了解工具设计的最佳实践
二、什么是 Tools?为什么重要?
1、从"认识世界"到"改变世界"
大模型本身只能生成文本。
无论多么强大的模型,本质上都是:
用户输入文本
↓
模型理解 + 推理
↓
模型输出文本
这种"纸上谈兵"的能力。
对于真正的 AI 应用来说远远不够。
例如:
用户:北京今天天气怎么样?
纯模型:我无法获取实时天气数据......
模型只能回答:
"建议您查看天气预报网站。"
因为它没有与外部世界交互的能力。
而有了 Tools:
用户:北京今天天气怎么样?
↓
AI 决定调用 get_weather 工具
↓
工具返回:北京晴天,15°C
↓
AI 回答:北京今天晴天,气温 15°C
模型从"只能说话"变成了"可以做事"。
这就是工具的核心价值:
工具是赋予大语言模型与外部世界交互能力的关键组件。
借助工具,大模型才能从"认识世界"走向"改变世界"。
工具让 AI 能够:
搜索实时信息
↓
查询数据库
↓
发送邮件
↓
调用第三方 API
↓
执行计算
↓
操作文件系统
工具是构建智能体(Agent)的核心要素之一。
三、Tools = Function Calling
在 LangChain 中,工具(Tools)实际上就是:
明确定义了输入和输出的可调用函数。
因此,工具调用(Tool Calling)也被称为函数调用(Function Calling)。
核心思路很简单:
普通 Python 函数
↓
LangChain 封装
↓
将函数签名(名称、描述、参数)告诉模型
↓
模型根据用户问题决定是否调用、如何调用
↓
开发者执行函数,将结果返回模型
↓
模型基于结果生成最终回答
四、两种工具调用方式
LangChain 提供两种使用工具的方式:
方式 1:直接调用(测试用)
python
from langchain_core.tools import tool
@tool
def get_weather(city: str) -> str:
"""
获取指定城市的天气信息
参数:
city: 城市名称,如"北京"、"上海"
返回:
天气信息字符串
"""
return city + "晴天,温度 15°C"
# 使用 .invoke() 方法直接调用
result = get_weather.invoke({"city": "北京"})
print(result)
# 输出:北京晴天,温度 15°C
这种方式适合测试时使用。
开发者自己手动传参,不经过 AI 决策。
方式 2:绑定到模型(★★★★★ 开发主流)
python
# 绑定工具
model_with_tools = model.bind_tools([get_weather])
# AI 可以决定是否调用工具
response = model_with_tools.invoke("北京天气如何?")
# 检查 AI 是否要调用工具
if response.tool_calls:
print("AI 想调用工具:", response.tool_calls)
else:
print("AI 直接回答:", response.content)
这种方式让 AI 来决策,是开发中的主流方式。
AI 根据用户输入自动判断:
需要工具 → 返回 tool_calls
不需要 → 直接返回文本回答
| 方式 | 谁决定调用 | 适用场景 |
|---|---|---|
| 直接调用 | 开发者 | 测试、调试 |
| 绑定模型 | AI | 正式开发 ★★★★★ |
五、工具调用的整体流程
大模型能根据对话上下文决定何时调用工具以及传递哪些参数。
经典流程如下:
┌──────────────────────────────────────────────────────────┐
│ 工具调用流程 │
├──────────────────────────────────────────────────────────┤
│ │
│ ① 用户输入 │
│ "北京今天天气怎么样?" │
│ ↓ │
│ ② 模型 + 绑定的工具列表 │
│ model.bind_tools([get_weather]) │
│ ↓ │
│ ③ 模型决策 │
│ 需要调用 get_weather? │
│ ├── 不需要 → 直接回答 │
│ └── 需要 → 返回 tool_calls │
│ {name: "get_weather", │
│ args: {city: "北京"}} │
│ ↓ │
│ ④ 开发者执行工具 │
│ get_weather.invoke(tool_call) │
│ ↓ │
│ ⑤ 工具结果返回模型 │
│ "北京晴天,温度 15°C" │
│ ↓ │
│ ⑥ 模型生成最终回答 │
│ "北京今天晴天,气温 15°C......" │
│ │
└──────────────────────────────────────────────────────────┘
这个流程中有两点特别重要:
大模型调用工具是单次推理,直接响应。
需要开发者手动执行工具并管理循环。
模型本身不会主动执行工具。
模型只是"告诉开发者"它想调用哪个工具以及传什么参数。
真正执行工具的是开发者的代码。
六、从 Message 流转看工具调用(★★★★★ 面试重点)
这是理解工具调用最核心的角度。
1、完整的三段式 Message 流转
工具调用的本质是三种 Message 的接力:
HumanMessage
↓
AIMessage (含 tool_calls)
↓
ToolMessage
↓
AIMessage (最终回答)
每一步详细展开:
用户说:今天北京天气如何
↓
① HumanMessage
content="今天北京天气如何"
↓
传给模型 (bind_tools)
↓
② AIMessage
content="" ← 内容为空!
tool_calls=[{ ← 包含工具调用信息
"name": "get_weather",
"args": {"city": "北京"},
"id": "call_xxxx"
}]
↓
开发者手动执行工具
↓
③ ToolMessage
content="北京天气晴朗" ← 工具执行结果
name="get_weather"
tool_call_id="call_xxxx"
↓
再次传给模型
↓
④ AIMessage
content="北京今天天气晴朗,适合出行......"
tool_calls=[] ← 不再调用工具
2、核心要点
关键发现:
AIMessage 调用工具时 content 为空!
模型在决定调用工具时:
不会同时生成文本。
只返回 tool_calls 信息。
ToolMessage 的三个必需字段:
| 字段 | 含义 | 示例 |
|---|---|---|
| content | 工具执行结果 | "北京天气晴朗" |
| name | 工具名称 | "get_weather" |
| tool_call_id | 匹配对应的调用 | "call_xxxx" |
这三个字段缺一不可。
模型需要通过 tool_call_id 将 ToolMessage 与之前的 tool_call 对应起来。
七、两种实现方式的对比
方式 1:不使用 @tool 装饰器
python
from langchain.messages import HumanMessage, ToolMessage
def get_weather(city: str):
"""获取天气的工具"""
return f"{city}天气晴朗"
# 将模型和工具绑定
model_with_tools = model.bind_tools([get_weather])
messages = [
HumanMessage("今天北京天气如何")
]
# 模型生成调用工具请求
response = model_with_tools.invoke(messages)
# 添加 AIMessage
messages.append(response)
tool_calls = response.tool_calls
for tool_call in tool_calls:
if tool_call["name"] == "get_weather":
# 手动拼接 ToolMessage 实例
tool_response = ToolMessage(
content=get_weather(**tool_call["args"]),
tool_call_id=tool_call["id"],
name=tool_call["name"]
)
messages.append(tool_response)
# 将结果返回模型,获取最终回答
final_response = model_with_tools.invoke(messages)
print(final_response.content)
这种方式需要手动创建 ToolMessage,分别传入 content、tool_call_id、name 三个参数。
方式 2:使用 @tool 装饰器(★★★★★ 推荐)
python
from langchain.messages import HumanMessage
@tool
def get_weather(city: str):
"""获取天气的工具"""
return f"{city}天气晴朗~"
model_with_tools = model.bind_tools([get_weather])
messages = [
HumanMessage("今天北京天气如何")
]
response = model_with_tools.invoke(messages)
messages.append(response)
tool_calls = response.tool_calls
for tool_call in tool_calls:
if tool_call["name"] == "get_weather":
# 自动返回 ToolMessage 类型!不再需要手动拼接
tool_response = get_weather.invoke(tool_call)
print(type(tool_response)) # <class 'langchain_core.messages.tool.ToolMessage'>
messages.append(tool_response)
final_response = model_with_tools.invoke(messages)
print(final_response.content)
关键区别:
不使用 @tool:
手动执行函数 → 手动创建 ToolMessage → 手动填三个字段
使用 @tool:
直接 tool.invoke(tool_call) → 自动返回 ToolMessage
| 对比项 | 不使用 @tool | 使用 @tool ★★★★★ |
|---|---|---|
| 创建 ToolMessage | 手动拼接 | 自动生成 |
| 代码量 | 较多 | 较少 |
| 易出错 | 容易遗漏字段 | 不易出错 |
| 推荐场景 | 理解底层原理 | 正式开发 |
八、Message 流转完整示例
以下是一个完整的工具调用示例的消息打印输出:
================================ Human Message =================================
今天北京天气如何
================================== Ai Message ==================================
Tool Calls:
get_weather (call_00_f65kV4JKjBPK0HhURzO99449)
Call ID: call_00_f65kV4JKjBPK0HhURzO99449
Args:
city: 北京
================================= Tool Message =================================
Name: get_weather
北京天气晴朗~
================================== Ai Message ==================================
今天北京天气晴朗,是个好天气!适合出行~
可以清楚地看到四段 Message 的完整接力。
九、工具调用的四个步骤(总结)
无论使用哪种方式,完整的工具调用流程都包括四个步骤:
步骤 1:模型绑定工具
model.bind_tools([...])
步骤 2:模型生成工具调用请求
用户提问 → 模型返回 AIMessage(含 tool_calls)
步骤 3:开发者手动执行工具
提取 tool_calls → 执行对应函数 → 获取 ToolMessage
步骤 4:将结果传递给模型生成最终回答
ToolMessage 加入 messages → model.invoke(messages) → 最终 AIMessage
特别注意:
大模型调用工具是单次推理,直接响应。
需要开发者手动执行工具并管理循环。
模型不会自己执行工具,它只是"表达意愿"。
十、工具的定义方式 1:不使用 @tool
1、普通 Python 函数直接绑定
python
from langchain.chat_models import init_chat_model
from rich import print as rprint
# 定义工具(普通 Python 函数)
def get_weather(city: str):
return f"{city}天气晴朗"
# 将模型和工具绑定
model_with_tools = model.bind_tools([get_weather])
response = model_with_tools.invoke("今天北京天气如何")
rprint(response)
此时 AIMessage 的 content 为空,tool_calls 包含调用信息:
python
AIMessage(
content='', # 内容为空!
tool_calls=[
{
'name': 'get_weather',
'args': {'city': '北京'},
'id': 'call_ECvZNV7RLTWpKQSjhvdGzKBd',
'type': 'tool_call'
}
],
...
)
2、底层原理:convert_to_openai_tool
执行 model.bind_tools([get_weather]) 时,底层最终会调用 convert_to_openai_tool 生成工具描述。
python
from langchain_core.utils.function_calling import convert_to_openai_tool
from rich import print as rprint
def get_weather(city: str):
return f"{city}天气晴朗"
rprint(convert_to_openai_tool(get_weather))
输出:
python
{
'type': 'function',
'function': {
'name': 'get_weather',
'description': '',
'parameters': {
'properties': {
'city': {
'type': 'string'
}
},
'required': ['city'],
'type': 'object'
}
}
}
这就是模型"看到"的工具描述。
字段说明:
| 字段 | 含义 |
|---|---|
| type | 数据类型,常见:string, number, integer, boolean, object, array |
| properties | 定义 JSON 对象中可以包含哪些属性及其类型和说明 |
| required | 必须存在的属性名列表 |
3、为什么普通函数也能当工具?
查看 convert_to_openai_tool 底层源码逻辑:
python
elif isinstance(function, langchain_core.tools.base.BaseTool):
oai_function = _format_tool_to_openai_function(function)
elif callable(function):
oai_function = _convert_python_function_to_openai_function(function)
两条分支:
@tool 装饰的函数 → 走 BaseTool 分支
普通 Python 函数 → 走 callable 分支(基于函数定义和 docstring 生成描述)
所以普通函数也能直接被 bind_tools 使用。
十一、工具描述的各部分详解
1、description ------ 来自 docstring
convert_to_openai_tool 从 docstring(文档字符串)加载工具的描述信息。
没有 docstring 时,description 为空:
python
def get_weather(city: str):
return f"{city}天气晴朗"
# convert_to_openai_tool 后 description: ''
添加 docstring 后:
python
def get_weather(city: str):
"""
天气查询工具
"""
return f"{city}天气晴朗"
# convert_to_openai_tool 后 description: '天气查询工具'
2、参数说明 ------ 要求 Google 风格 docstring
使用 Args:、Returns:、Raises: 等关键字:
python
def get_weather(city: str):
"""
天气查询工具
Args:
city: 城市名称
"""
return f"{city}天气晴朗"
输出中 city 字段会带有 description:
python
'city': {
'description': '城市名称',
'type': 'string'
}
AI 依赖 docstring 来理解工具。
python
# ❌ 不好:太模糊
@tool
def tool1(x: str) -> str:
"""做一些事情"""
...
# ✅ 好:清晰明确
@tool
def search_products(query: str) -> str:
"""
在产品数据库中搜索产品
Args:
query: 搜索关键词,如"笔记本电脑"、"手机"
Returns:
产品列表的 JSON 字符串
"""
...
3、参数类型 ------ 来自类型注解
参数类型来源于函数的类型注解。
python
def get_weather(city: str): # city 的类型为 string
...
def get_weather(city): # city 的类型信息丢失!
...
删除了参数类型注解,则工具描述中不包含参数类型说明。
注意:如果 docstring 中包含参数说明,则对应的参数必须有类型注解,否则 LangChain 会报错:
ValueError: Arg city in docstring not found in function signature.
4、参数默认值
参数有默认值时:
- 描述信息中会出现 default 字段
- 不会出现在 required 列表中
参数没有默认值时:
- 描述信息中无 default 字段
- 出现在 required 列表中
举例:
python
def get_weather(dt: str, city: str = "北京"):
"""
天气查询工具
Args:
dt: 日期
city: 城市名称
"""
return f"{city}天气晴朗"
输出:
python
{
'properties': {
'dt': {'description': '日期', 'type': 'string'},
'city': {'default': '北京', 'description': '城市名称', 'type': 'string'}
},
'required': ['dt'] # 只有 dt,没有 city
}
十二、工具的定义方式 2:使用 @tool 装饰器(★★★★★ 推荐)
使用 @tool 装饰器修饰,可以自动将普通 Python 函数转化为智能体可调用的工具。
此方式最直接,代码量极少,非常适合快速验证想法或创建参数简单的工具。
1、必须提供 docstring
@tool 装饰的函数必须有 docstring,否则报错:
ValueError: Function must have a docstring if description not provided.
python
from langchain.tools import tool
@tool
def get_weather(city: str):
"""
天气查询工具
"""
return f"{city}天气晴朗"
2、自定义 description 参数
@tool 的 description 参数可以更改工具描述,优先级高于 docstring:
python
@tool(description="根据城市名称查询当日天气的工具")
def get_weather(city: str):
"""
天气查询工具
"""
return f"{city}天气晴朗"
# description: '根据城市名称查询当日天气的工具' ← 使用了 description 参数
3、parse_docstring 参数(★★★ 重要)
当没有向 @tool 传递 description 时,默认情况下 tool 会将 docstring 整体视为 description。
这意味着 Google 风格的 Args/Returns 也会被当作 description 的一部分,导致 description 非常冗长。
通过设置 parse_docstring=True,docstring 会被解析,分别填充到对应的字段:
python
@tool(parse_docstring=True)
def get_weather(city: str, units: str = "celsius", include_forecast: bool = False) -> str:
"""
获取当日天气,可选择是否同时查询未来五日天气预报
Args:
city: 城市
units: 气温单位,可选:celsius-摄氏度,fahrenheit-华氏度
include_forecast: 是否包含未来五日的天气预报
"""
...
使用 parse_docstring=True 后:
python
{
'name': 'get_weather',
'description': '获取当日天气,可选择是否同时查询未来五日天气预报', # 只有第一行
'parameters': {
'properties': {
'city': {'description': '城市', 'type': 'string'},
'units': {'default': 'celsius', 'description': '气温单位,可选:...', 'type': 'string'},
'include_forecast': {'default': False, 'description': '是否包含未来五日的天气预报', 'type': 'boolean'}
},
'required': ['city']
}
}
关键区别:
| 设置 | description 内容 |
|---|---|
| 不设置 parse_docstring | 整个 docstring(包括 Args/Returns) |
| parse_docstring=True | 只取第一行,Args 分别填充到对应字段 |
注意事项:
不使用 @tool 时:
docstring 不合法会被视为普通文本,作为 description
使用 @tool 时:
如果 parse_docstring=True 且 docstring 不合法 → 抛出异常
4、更改工具名称:name_or_callable
默认使用函数名作为工具名称。
可以向 @tool 传参更改:
python
@tool(name_or_callable="getWeather")
def get_weather(city: str):
"""
天气查询工具
"""
return f"{city}天气晴朗"
# 工具名称变成 'getWeather'
也可以简写:
python
@tool("getWeather")
def get_weather(city: str):
...
开发中习惯使用函数名作为工具名称,不推荐自定义。
注意:不要使用 config 或 runtime 作为参数名,这些是 LangChain 内部保留的。
十三、自定义 args_schema
当工具参数变得复杂,需要枚举值、范围限制或更复杂的业务逻辑验证时,需要自定义 args_schema。
方式 1:使用 Pydantic BaseModel(★★★★★ 推荐)
(1)BaseModel 基类
python
from pydantic import BaseModel
class WeatherInput(BaseModel):
city: str
print(WeatherInput(city="北京")) # city='北京'
注意:BaseModel 子类初始化时,不接收位置参数,字段值必须以关键字参数的形式传入。
(2)Field ------ 定制字段
python
from pydantic import BaseModel, Field
class WeatherInput(BaseModel):
city: str = Field(
default="北京",
description="城市"
)
include_forecast: bool = Field(
default=False,
description="是否包含未来五日天气预报"
)
每个字段的 description 参数至关重要,它直接影响大模型理解参数含义的能力。
(3)Literal ------ 限定固定选项
python
from pydantic import BaseModel
from typing import Literal
class WeatherInput(BaseModel):
city: str
unit: Literal["celsius", "fahrenheit"]
# 合法
print(WeatherInput(city="北京", unit="celsius"))
# 非法 → 抛出 ValidationError
print(WeatherInput(city="北京", unit="kelvin"))
(4)完整使用示例
python
from pydantic import BaseModel, Field
from typing import Literal
from langchain.tools import tool
class WeatherInput(BaseModel):
city: str = Field(
default="北京",
description="城市"
)
unit: Literal["celsius", "fahrenheit"] = Field(
default="celsius",
description="气温单位"
)
include_forecast: bool = Field(
default=False,
description="是否包含未来五日天气预报"
)
@tool(args_schema=WeatherInput)
def get_weather(city: str, unit: str = "celsius", include_forecast: bool = False) -> str:
"""获取当日天气,可选未来五日天气预报"""
temp = 22 if unit == "celsius" else 72
result = f'{city}当天气温: {temp} {"摄氏度" if unit == "celsius" else "华氏度"}'
if include_forecast:
result += "\n未来五天都是晴天"
return result
使用 Pydantic 定义后,Literal 字段会自动生成 enum 约束:
python
'unit': {
'default': 'celsius',
'description': '气温单位',
'enum': ['celsius', 'fahrenheit'],
'type': 'string'
}
方式 2:使用 JSON Schema 字典
在 LangChain 中,还可以直接使用 JSON Schema 字典来定义工具的参数模式。
这种方式特别适合参数结构需要动态生成的场景。
python
weather_schema = {
"type": "object",
"properties": {
"location": {"type": "string"},
"units": {"type": "string"},
"include_forecast": {"type": "boolean"}
},
"required": ["location", "units", "include_forecast"]
}
@tool(args_schema=weather_schema)
def get_weather(city: str, unit: str = "celsius", include_forecast: bool = False) -> str:
"""获取当日天气,可选未来五日天气预报"""
...
注意:应该传递给 args_schema 的只有 parameters 对应的 JSON 对象,不需要外层的 type/function 包裹。
| 方式 | 适用场景 | 验证能力 |
|---|---|---|
| Pydantic | 复杂参数、枚举、验证 | 强 ★★★★★ |
| JSON Schema | 动态生成、运行时可配置 | 中 |
| docstring | 简单参数、快速原型 | 弱 |
十四、工具应用案例
案例 1:使用 args_schema 定义复杂参数
python
from pydantic import BaseModel, Field
from langchain.tools import tool
from langchain.messages import HumanMessage
class WeatherSchema(BaseModel):
city: str = Field(default="北京", description="城市名称")
if_forecast: bool = Field(default=False, description="是否包含明日天气预报")
@tool("get_weather_and_forecast", description="查询当日天气,可以包含明日天气预报", args_schema=WeatherSchema)
def get_weather(city: str, if_forecast: bool):
res = f"{city} 今天天气不错"
if if_forecast:
res += "\n明天也不错"
return res
model_with_tools = model.bind_tools([get_weather])
messages = [HumanMessage("今天杭州天气如何?明天呢?")]
response = model_with_tools.invoke(messages)
messages.append(response)
tool_calls = response.tool_calls
for tool_call in tool_calls:
if tool_call["name"] == "get_weather_and_forecast":
tool_msg = get_weather.invoke(tool_call)
messages.append(tool_msg)
final_response = model_with_tools.invoke(messages)
messages.append(final_response)
for msg in messages:
msg.pretty_print()
AI 智能推断:
用户说:今天杭州天气如何?明天呢?
↓
AI 决定:调用 get_weather_and_forecast
city=杭州, if_forecast=True
案例 2:通过 docstring 定义参数(parse_docstring=True)
python
@tool("get_weather_and_forecast", parse_docstring=True)
def get_weather(city: str = "北京", if_forecast: bool = False):
"""
查询当日天气,可以包含明日天气预报
Args:
city: 城市名称
if_forecast: 是否包含明日天气预报
"""
res = f"{city} 今天天气不错"
if if_forecast:
res += "\n明天要下雨"
return res
model_with_tools = model.bind_tools([get_weather])
messages = [HumanMessage("今天杭州天气如何?明天呢?")]
response = model_with_tools.invoke(messages)
messages.append(response)
tool_calls = response.tool_calls
for tool_call in tool_calls:
if tool_call["name"] == "get_weather_and_forecast":
tool_msg = get_weather.invoke(tool_call)
messages.append(tool_msg)
final_response = model_with_tools.invoke(messages)
messages.append(final_response)
注意:要正确解析 docstring,必须在 @tool 中将 parse_docstring 设置为 True。
十五、多工具调用(★★★★★ 重要)
1、核心原则
大模型调用工具是单次推理,每次运行可以返回多个 tool_calls。
当一次回复中包含多个 tool_calls 时,需要遍历执行。
当需要多轮工具调用时,需要开发者自己管理循环。
2、多工具调用完整示例
python
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage
# 1. 定义工具
@tool(parse_docstring=True)
def get_stock_price(company: str, timeframe: str = "today") -> str:
"""获取指定公司的股票价格信息
Args:
company: 公司名称(如:苹果公司, 微软公司, 谷歌公司)
timeframe: 时间范围(today-今日, week-本周, month-本月)
"""
mock_data = {
"苹果公司": {"today": 185.20, "week": 183.50, "month": 180.75},
"微软公司": {"today": 415.86, "week": 412.30, "month": 405.42},
"谷歌公司": {"today": 15.42, "week": 15.20, "month": 14.85}
}
if company in mock_data:
price = mock_data[company].get(timeframe, "未知时间范围")
return f"{company} {timeframe} 价格: {price} 美元"
else:
return f"未找到 {company} 的数据"
@tool(parse_docstring=True)
def search_news(company: str) -> str:
"""搜索指定公司的财经新闻
Args:
company: 公司名称
Returns:
公司的财经新闻,每个新闻占一行
"""
mock_news = {
"苹果公司": [
"苹果发布新款 iPhone,股价上涨 3%",
"苹果与欧盟达成反垄断和解协议",
"苹果将在印度扩大生产规模"
],
"微软公司": [
"微软Azure云业务季度增长超预期",
"微软完成对 Nuance 的收购",
"微软推出新一代 AI 助手 Copilot"
],
"谷歌公司": [
"谷歌发布新 AI模型,性能提升 20%",
"谷歌与OpenAI合作,开发新的 AI 助手",
"谷歌在欧洲展开 AI 研究项目"
]
}
news_list = mock_news.get(company, [f"未找到{company}的相关新闻"])
return "\n".join(news_list)
# 2. 初始化模型并绑定工具
tools = [get_stock_price, search_news]
model_with_tools = model.bind_tools(tools)
message_list = []
human_message = HumanMessage(content="苹果公司今天的股价是多少?最近有什么新闻?")
message_list.append(human_message)
# 3. 工具调用循环
while True:
response = model_with_tools.invoke(message_list)
message_list.append(response)
# 如果模型不需要调用工具,直接退出循环
if not response.tool_calls:
print("没有工具调用,直接返回答案")
break
# 开发者根据模型的响应,调用工具并获取结果
for tool_call in response.tool_calls:
if tool_call["name"] == "get_stock_price":
stock_result = get_stock_price.invoke(tool_call)
print("stock_result", stock_result)
message_list.append(stock_result)
if tool_call["name"] == "search_news":
news_result = search_news.invoke(tool_call)
print("news_result", news_result)
message_list.append(news_result)
# 打印完整消息历史
for msg in message_list:
msg.pretty_print()
3、循环模式分析
while True:
response = model_with_tools.invoke(message_list)
message_list.append(response)
if not response.tool_calls:
break # ← 没有工具调用,退出
for tool_call in response.tool_calls:
执行工具
message_list.append(工具结果)
# 继续循环,让模型基于结果生成回答
这个循环可以处理:
场景 1:不需要工具 → 第一次就退出
场景 2:需要一次工具调用 → 调用后退出
场景 3:需要多次工具调用 → 多次循环
4、一次返回多个 tool_calls
模型可以在一次 AIMessage 中返回多个 tool_calls:
================================== Ai Message ==================================
Tool Calls:
get_stock_price (call_cpGOhWce8rlIFSZ2G9w7ouON)
Args:
company: 苹果公司
timeframe: today
search_news (call_W9zjkAO8TNCmUbed2ld9tsxl)
Args:
company: 苹果公司
此时遍历 tool_calls 列表,挨个调用工具即可。
5、不同问题的测试
python
# 测试 1:需要工具
human_message = HumanMessage(content="苹果公司今天的股价是多少?最近有什么新闻?")
# → AI 调用 get_stock_price + search_news
# 测试 2:需要工具
human_message = HumanMessage(content="比较一下微软和苹果的股价")
# → AI 调用多次 get_stock_price
# 测试 3:需要工具
human_message = HumanMessage(content="腾讯最近有什么重大新闻?")
# → AI 调用 search_news(可能找不到,但工具会返回"未找到")
# 测试 4:不需要工具
human_message = HumanMessage(content="海水为什么是咸的?")
# → AI 直接回答,不调用任何工具
十六、拓展:强制使用工具(tool_choice)
bind_tools 可以传递参数 tool_choice,用于控制是否强制使用工具。
该字段最终会作为 payload 的 tool_choice 字段传递给模型。
1、tool_choice 取值说明
| 取值 | 含义 |
|---|---|
| "none" | 模型不会调用任何工具 |
| "auto" | 默认值,模型自主决定是否调用(推荐) |
| "required" | 模型必须调用工具,数量不限 |
| "any" | 等价于 "required" |
| "工具名" | 强制调用指定的某个工具 |
2、"none" ------ 禁止调用工具
python
model_with_tools = model.bind_tools([get_weather], tool_choice="none")
messages = [HumanMessage("今天北京天气如何?别瞎编")]
response = model_with_tools.invoke(messages)
# → AI 直接回答:"我无法直接获取实时天气数据。建议您查看......"
即使用户明确要求查询天气,模型也不会调用 get_weather。
3、"auto" ------ 默认行为
python
model_with_tools = model.bind_tools([get_weather], tool_choice="auto")
# 需要工具时
messages = [HumanMessage("今天杭州天气如何?")]
response = model_with_tools.invoke(messages)
# → AI 调用 get_weather
# 不需要工具时
messages = [HumanMessage("你好啊")]
response = model_with_tools.invoke(messages)
# → AI 直接回答:"你好!很高兴见到你......"
4、"required" ------ 必须调用工具
python
model_with_tools = model.bind_tools([get_weather], tool_choice="required")
# 需要工具时
messages = [HumanMessage("今天杭州天气如何?别瞎编")]
response = model_with_tools.invoke(messages)
# → AI 调用 get_weather
# 不需要工具时,模型依然会调用!
messages = [HumanMessage("你好啊")]
response = model_with_tools.invoke(messages)
# → AI 仍然调用 get_weather(可能用默认参数 city=北京)
5、强制调用特定工具
python
@tool(parse_docstring=True)
def get_weather1(city: str) -> str:
"""获取当日天气"""
return f'{city}当天晴朗'
@tool(parse_docstring=True)
def get_weather2(city: str) -> str:
"""获取当日天气"""
return f'{city}当天晴朗'
# 强制调用 get_weather2
model_with_tools = model.bind_tools(
[get_weather1, get_weather2],
tool_choice="get_weather2"
)
messages = [HumanMessage("杭州今天天气如何?")]
response = model_with_tools.invoke(messages)
# → 只会调用 get_weather2
十七、实践经验总结
1、清晰的描述(★★★★★)
AI 依赖 docstring 来理解工具的用途和调用时机。
清晰、准确的文档字符串是工具能被正确调用的前提。
python
# ✅ 好
@tool(parse_docstring=True)
def search_flights(origin: str, destination: str, date: str) -> str:
"""
搜索航班信息
Args:
origin: 出发城市,如"北京"
destination: 目的地城市,如"上海"
date: 出发日期,格式 YYYY-MM-DD
Returns:
可用航班的 JSON 列表
"""
2、功能单一(★★★★★)
每个工具只做一件事。
python
# ❌ 不好:一个工具做太多事
@tool
def do_everything(action: str, data: str) -> str:
"""做各种事情"""
if action == "weather": ...
elif action == "calculate": ...
elif action == "search": ...
# ✅ 好:每个工具做一件事
@tool
def get_weather(city: str) -> str:
"""获取天气"""
...
@tool
def calculator(operation: str, a: float, b: float) -> str:
"""计算"""
...
单一职责原则同样适用于工具设计。
3、如何处理工具失败?(三层防护)
第 1 层:工具内部处理(try/except)
↓ 仍然失败
第 2 层:Agent 级重试(使用 prompt 引导)
↓ 仍然失败
第 3 层:调用级重试(@retry 装饰器)
第 1 层:工具内部 try/except
python
@tool
def divide(a: float, b: float) -> str:
"""
除法计算
Args:
a: 被除数
b: 除数
"""
try:
if b == 0:
return "错误:除数不能为零"
result = a / b
return f"{a} / {b} = {result}"
except Exception as e:
return f"计算错误:{e}"
第 2 层:Agent 级提示
python
agent = create_agent(
model=model,
tools=[...],
prompt="如果工具失败,尝试使用其他方法解决问题。"
)
第 3 层:@retry 装饰器
python
from tenacity import retry, stop_after_attempt
@retry(stop=stop_after_attempt(3))
def call_agent(question):
return agent.invoke({"messages": [{"role": "user", "content": question}]})
@retry 的工作流程:
① 调用 call_agent("你好")
↓
② 执行 agent.invoke(...)
↓
③ 成功 → 正常返回
失败 → @retry 拦截错误,自动重试
↓
④ 连续 3 次都失败 → 抛出异常
4、返回字符串,不要返回字典
python
# ✅ 好:返回字符串
@tool
def get_user_info(user_id: str) -> str:
"""获取用户信息"""
user = {"id": user_id, "name": "张三"}
return json.dumps(user, ensure_ascii=False) # 转成 JSON 字符串
# ❌ 不好:返回字典
@tool
def get_user_info(user_id: str) -> dict:
"""获取用户信息"""
return {"id": user_id, "name": "张三"}
原因:
1. 大模型(LLM)的本质只吃"文本"
2. 字典转字符串可能出现 Unicode 编码问题:
{"name": "张三"} 而不是 {"name": "张三"}
3. 直接看到中文的模型,和看到 \uXXXX 的模型,
输出稳定性和准确率有差距
通过手动 json.dumps(..., ensure_ascii=False),确保喂给大模型的是最干净、最直观的纯文本。
5、选择同步 vs 异步
python
# 同步工具:简单场景,CPU 密集型任务
@tool
def sync_tool(x: str) -> str:
return process(x)
# 异步工具:IO 密集型(API 调用、数据库、文件操作)
@tool
async def async_tool(x: str) -> str:
return await async_process(x)
| 类型 | 适用场景 |
|---|---|
| 同步 | 简单场景、CPU 密集型 |
| 异步 | API 调用、数据库、文件 IO |
十八、面试常见问题
Q1:Tool Calling 的完整 Message 流转是怎样的?
答:工具调用的 Message 流转是四个阶段:
HumanMessage → AIMessage(含 tool_calls) → ToolMessage → AIMessage(最终回答)
- 用户发送 HumanMessage
- 模型返回 AIMessage(content 为空,tool_calls 包含调用信息)
- 开发者执行工具,返回 ToolMessage(content + name + tool_call_id)
- 模型收到 ToolMessage 后,生成最终的 AIMessage
ToolMessage 必须包含三个字段:content(执行结果)、name(工具名称)、tool_call_id(匹配调用 ID)。
Q2:bind_tools 底层做了什么?
答:model.bind_tools([tool1, tool2]) 底层调用 convert_to_openai_tool 将每个工具的函数签名、docstring、参数类型等转换为 OpenAI 兼容的 function calling 格式。
转换后的 JSON 包含:
name:工具名称description:工具描述(来自 docstring)parameters:参数定义(类型、是否必需、默认值等)
这个 JSON 会在每次请求时发送给模型,模型据此决定是否调用工具。
Q3:@tool 装饰器的作用是什么?和普通函数有什么区别?
答:@tool 装饰器将普通 Python 函数转化为 BaseTool 对象。
主要区别:
| 对比 | 普通函数 | @tool 装饰 |
|---|---|---|
| 类型 | callable | BaseTool 对象 |
| 调用方式 | get_weather(**args) | get_weather.invoke(tool_call) |
| 返回值 | str | ToolMessage |
| docstring | 可选 | 必需(否则报错) |
| 参数验证 | 无 | 可配合 args_schema |
使用 @tool 后,调用 .invoke(tool_call) 会自动返回 ToolMessage,无需手动拼接。
Q4:模型是如何决定调用哪个工具的?
答:模型通过以下信息做出决策:
- 工具描述(description):知道工具能做什么
- 参数描述(args description):知道每个参数的含义
- 用户意图:根据用户输入判断是否需要工具
- 上下文:结合对话历史做出选择
模型返回的 tool_calls 中包含 name(工具名)和 args(参数),开发者据此分发执行。
Q5:如何处理模型调用不存在工具的情况?
答:
python
for tool_call in response.tool_calls:
tool_name = tool_call["name"]
if tool_name == "get_stock_price":
result = get_stock_price.invoke(tool_call)
elif tool_name == "search_news":
result = search_news.invoke(tool_call)
else:
raise Exception(f"不存在的工具: {tool_name}")
message_list.append(result)
用 if/elif/else 做工具分发,else 分支抛出异常或返回错误信息。
Q6:tool_choice 的 "auto" 和 "required" 有什么区别?
答:
"auto"(默认):模型自主判断是否需要调用工具
需要 → 调用
不需要 → 直接回答
"required":无论如何都必须调用工具
即使用户说"你好",模型也会编一个参数调用工具
关键区别在于自主判断的空间。
Q7:为什么工具要返回字符串而不是字典?
答:三个原因:
- LLM 只能理解文本,字符串是最直接的形式
- 避免 Unicode 编码问题(字典转字符串可能出现
张三而非张三) - 干净的文本能提高模型输出的稳定性和准确率
十九、本章总结
| 知识点 | 核心内容 |
|---|---|
| Tools 本质 | 可调用函数,让模型与外部世界交互 |
| 两种调用方式 | 直接 invoke()(测试) / bind_tools(开发 ★★★★★) |
| Message 流转 | HumanMessage → AIMessage → ToolMessage → AIMessage |
| 工具描述生成 | convert_to_openai_tool 负责转换 |
| 普通函数当工具 | 基于函数签名和 docstring 生成 schema |
| @tool 装饰器 | 自动生成 ToolMessage,推荐方式 ★★★★★ |
| parse_docstring | 解析 Google 风格 docstring 到对应字段 |
| args_schema | Pydantic BaseModel 或 JSON Schema 定义复杂参数 |
| 工具调用循环 | while True + tool_calls 检查 |
| tool_choice | none / auto / required / 指定工具名 |
| 最佳实践 | 清晰描述、功能单一、三层防护、返回字符串、同步/异步选择 |
核心理解:
Tools 让大模型从"纸上谈兵"变为"实际行动"。
模型只负责"表达调用意愿"(返回 tool_calls),真正执行工具的是开发者的代码。
Message 流转(HumanMessage → AIMessage → ToolMessage → AIMessage)是理解整个工具调用机制的根本。
@tool 装饰器 + parse_docstring=True + args_schema 是生产环境的标准工具定义模式。