LangChain 学习笔记(五):Tools 工具调用

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(最终回答)
  1. 用户发送 HumanMessage
  2. 模型返回 AIMessage(content 为空,tool_calls 包含调用信息)
  3. 开发者执行工具,返回 ToolMessage(content + name + tool_call_id)
  4. 模型收到 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:模型是如何决定调用哪个工具的?

答:模型通过以下信息做出决策:

  1. 工具描述(description):知道工具能做什么
  2. 参数描述(args description):知道每个参数的含义
  3. 用户意图:根据用户输入判断是否需要工具
  4. 上下文:结合对话历史做出选择

模型返回的 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:为什么工具要返回字符串而不是字典?

答:三个原因:

  1. LLM 只能理解文本,字符串是最直接的形式
  2. 避免 Unicode 编码问题(字典转字符串可能出现 张三 而非 张三)
  3. 干净的文本能提高模型输出的稳定性和准确率

十九、本章总结

知识点 核心内容
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 是生产环境的标准工具定义模式。

相关推荐
7哥♡ۣۖᝰꫛꫀꪝۣℋ10 分钟前
LangChain(tool)定义使用
langchain
无心水43 分钟前
24、系统架构师论文写作专题 核心考点与学习笔记
笔记·学习·系统架构·架构师·安全架构·软考架构师·软考系统架构设计师
蒸蒸yyyyzwd1 小时前
邓明 Agent 系统面试-意图识别学习笔记
笔记·学习
Zhao-Jichao4 小时前
AI Prompting for Everyone 课程笔记 - P21 总结 Conclusion
人工智能·笔记
欢欢李4 小时前
【LTE L3 笔记 05】LTE同步信号
笔记·无线通信·物理层·lte·空口协议
La Pulga6 小时前
【FreeRTOS源码学习笔记】空闲任务与阻塞延时
笔记·mcu·学习·嵌入式·freertos·实时操作系统
蒸蒸yyyyzwd6 小时前
秋招学习笔记 day106
笔记·学习
小李不想当小白7 小时前
WDG看门狗(STM32标准库学习笔记)
笔记·stm32·单片机·嵌入式硬件·学习·嵌入式
欢欢李8 小时前
【LTE L3 笔记 04】LTE帧结构
笔记·无线通信·物理层·lte·空口协议
終不似少年遊*10 小时前
【安全业务Agent】从场景到编排
python·react.js·ai·langchain·agent·安全架构·工作流