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):
    ...

开发中习惯使用函数名作为工具名称,不推荐自定义。

注意:不要使用 configruntime 作为参数名,这些是 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 是生产环境的标准工具定义模式。

相关推荐
咸甜适中4 小时前
rust语言AI编程学习笔记(五)clap命令行参数解析
学习·rust·ai编程
诗句藏于尽头4 小时前
deepseek harness对接商汤科技免费大模型使用教程
人工智能·科技·学习
过期的秋刀鱼!4 小时前
LangChain -访问模型,创建智能体访问模型
langchain
chemddd4 小时前
豆包生成 亚马逊链接的视频
学习·交友
weixin_431600444 小时前
NestJS 入门(7):生命周期钩子——构造函数和 `OnModuleInit` 差在哪?
前端·后端·学习·node.js·nest.js
工业HMI实战笔记5 小时前
解放双手,声控未来:抗噪语音交互如何革新嘈杂车间的HMI操作体验
人工智能·学习·自动化·制造
知识分享小能手5 小时前
线性代数学习教程,从入门到精通,相似矩阵及二次型 — 知识点详解(9)
学习·线性代数·机器学习
思考着亮6 小时前
1.LangGraph
langchain
小弥儿6 小时前
GitHub今日热榜 | 2026-08-14:OSINT情报工具回流,本地AI赛道升温
人工智能·学习·开源·github
小黄人软件6 小时前
【证书批量转换】PEM->CER证书批量转换工具.exe 纯 Python实现,无需安装OpenSSL
人工智能·学习