文章目录
- [第 5 章:工具调用 --- 让 AI 拥有行动能力](#第 5 章:工具调用 — 让 AI 拥有行动能力)
-
- [5.1 本章目标](#5.1 本章目标)
- [5.2 核心概念(图文讲解)](#5.2 核心概念(图文讲解))
-
- [5.2.1 生动类比:给 AI 装上手脚](#5.2.1 生动类比:给 AI 装上手脚)
- [5.2.2 工具调用时序图](#5.2.2 工具调用时序图)
- [5.2.3 关键术语解释](#5.2.3 关键术语解释)
- [5.3 实战:旅行规划助手 v0.4](#5.3 实战:旅行规划助手 v0.4)
-
- [5.3.1 场景描述](#5.3.1 场景描述)
- [5.3.2 完整代码](#5.3.2 完整代码)
- [5.3.3 运行结果展示](#5.3.3 运行结果展示)
- [5.3.4 代码逐段解析](#5.3.4 代码逐段解析)
- [5.4 API 速查](#5.4 API 速查)
- [5.5 常见错误与避坑指南](#5.5 常见错误与避坑指南)
-
- [错误 1:忘记将工具结果追加到消息列表](#错误 1:忘记将工具结果追加到消息列表)
- [错误 2:工具函数没有文档字符串](#错误 2:工具函数没有文档字符串)
- [错误 3:工具返回值不是字符串](#错误 3:工具返回值不是字符串)
- [错误 4:忘记在工具调用循环中处理异常](#错误 4:忘记在工具调用循环中处理异常)
- [错误 5:工具调用循环无限循环](#错误 5:工具调用循环无限循环)
- [5.6 最佳实践总结](#5.6 最佳实践总结)

第 5 章:工具调用 --- 让 AI 拥有行动能力
5.1 本章目标
学完本章后,你将能够:
- 使用
@tool装饰器定义 LLM 可调用的工具函数 - 使用
bind_tools()将工具绑定到 LLM,让模型知道有哪些工具可用 - 实现工具调用循环(Tool Calling Loop),让 LLM 自主决定何时调用工具
- 理解 Function Calling 的完整流程:LLM 请求调用 → 执行工具 → 返回结果 → LLM 综合
- 构建旅行规划助手 v0.4:接入模拟的机票、酒店、天气查询工具
5.2 核心概念(图文讲解)
5.2.1 生动类比:给 AI 装上手脚
到目前为止,我们的 Agent 只能「思考」------它调用 LLM 获取信息,然后生成文本。这就像一个只有大脑但没有手脚的人:他能思考、能规划,但无法行动。
工具调用(Tool Calling) 就是给 AI 装上「手脚」:
- 手 :
search_flights()--- 查询机票,就像用手打开手机查航班 - 脚 :
search_hotels()--- 查询酒店,就像用脚走到酒店前台 - 感官 :
get_weather()--- 查询天气,就像用眼睛看天气预报
当用户问「帮我查一下 7 月 20 日从北京到东京的机票」,AI 的思考过程是:
- 理解意图:用户想查机票
- 选择工具 :我需要用
search_flights工具 - 调用工具 :
search_flights("北京", "东京", "2026-07-20") - 获取结果:工具返回航班列表
- 综合回复:把航班信息整理成易读的格式回复用户
5.2.2 工具调用时序图
工具函数 LLM (gpt-4o-mini) call_llm_with_tools Entrypoint 用户 工具函数 LLM (gpt-4o-mini) call_llm_with_tools Entrypoint 用户 #mermaid-svg-dceUJfWVwZ78VYGr{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-dceUJfWVwZ78VYGr .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-dceUJfWVwZ78VYGr .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-dceUJfWVwZ78VYGr .error-icon{fill:#552222;}#mermaid-svg-dceUJfWVwZ78VYGr .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-dceUJfWVwZ78VYGr .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-dceUJfWVwZ78VYGr .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-dceUJfWVwZ78VYGr .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-dceUJfWVwZ78VYGr .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-dceUJfWVwZ78VYGr .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-dceUJfWVwZ78VYGr .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-dceUJfWVwZ78VYGr .marker{fill:#333333;stroke:#333333;}#mermaid-svg-dceUJfWVwZ78VYGr .marker.cross{stroke:#333333;}#mermaid-svg-dceUJfWVwZ78VYGr svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-dceUJfWVwZ78VYGr p{margin:0;}#mermaid-svg-dceUJfWVwZ78VYGr .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-dceUJfWVwZ78VYGr text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-dceUJfWVwZ78VYGr .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-dceUJfWVwZ78VYGr .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-dceUJfWVwZ78VYGr .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-dceUJfWVwZ78VYGr .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-dceUJfWVwZ78VYGr #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-dceUJfWVwZ78VYGr .sequenceNumber{fill:white;}#mermaid-svg-dceUJfWVwZ78VYGr #sequencenumber{fill:#333;}#mermaid-svg-dceUJfWVwZ78VYGr #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-dceUJfWVwZ78VYGr .messageText{fill:#333;stroke:none;}#mermaid-svg-dceUJfWVwZ78VYGr .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-dceUJfWVwZ78VYGr .labelText,#mermaid-svg-dceUJfWVwZ78VYGr .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-dceUJfWVwZ78VYGr .loopText,#mermaid-svg-dceUJfWVwZ78VYGr .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-dceUJfWVwZ78VYGr .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-dceUJfWVwZ78VYGr .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-dceUJfWVwZ78VYGr .noteText,#mermaid-svg-dceUJfWVwZ78VYGr .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-dceUJfWVwZ78VYGr .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-dceUJfWVwZ78VYGr .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-dceUJfWVwZ78VYGr .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-dceUJfWVwZ78VYGr .actorPopupMenu{position:absolute;}#mermaid-svg-dceUJfWVwZ78VYGr .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-dceUJfWVwZ78VYGr .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-dceUJfWVwZ78VYGr .actor-man circle,#mermaid-svg-dceUJfWVwZ78VYGr line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-dceUJfWVwZ78VYGr :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} loop 工具调用循环 "帮我查北京到东京的机票" 调用 LLM Task 发送消息 + 工具列表 返回 tool_calls(需要调用 search_flights) 执行 search_flights("北京","东京","...") 返回航班数据 发送工具返回结果 返回最终回复(不需要更多工具) 返回最终回复 展示航班信息
5.2.3 关键术语解释
| 术语 | 含义 | 在「装手脚」中的类比 |
|---|---|---|
@tool |
将普通函数标记为 LLM 可调用的工具 | 给手脚贴上「可用」标签 |
bind_tools() |
将工具列表绑定到 LLM,让模型知道有哪些工具 | 告诉 AI 你有哪些手脚可以用 |
tool_calls |
LLM 返回的「请调用工具」请求 | AI 说「我需要用右手拿这个」 |
| 工具调用循环 | LLM 反复请求工具 → 执行 → 返回结果,直到不再需要 | AI 反复用手脚做事直到完成 |
| Function Calling | OpenAI 的原生能力,让 LLM 输出结构化工具调用请求 | AI 大脑的「行动指令」功能 |
5.3 实战:旅行规划助手 v0.4
5.3.1 场景描述
v0.4 为旅行规划助手添加三个真实工具:(1) search_flights --- 搜索航班;(2) search_hotels --- 搜索酒店;(3) get_weather --- 查询天气。这些工具使用模拟数据,但结构和调用方式与真实 API 完全一致。用户可以直接说「帮我查北京到东京的机票」或「东京下周天气怎么样」,Agent 会自动选择合适的工具并返回结果。
5.3.2 完整代码
python
# ============================================================
# 旅行规划助手 v0.4 --- 第 5 章完整可运行代码
# 保存为 ch05_tool_calling.py 后运行:python ch05_tool_calling.py
# 运行前请确保已设置:export OPENAI_API_KEY="your-key"
# 依赖:langgraph>=1.2.9, langchain>=0.3.0, langchain-openai>=0.3.0
# ============================================================
import os # 导入操作系统模块,用于读取环境变量
import json # 导入 JSON 模块,用于序列化工具参数
from pydantic import BaseModel, Field # 导入 Pydantic 用于定义 State
from typing import Optional, Literal # 导入类型注解工具
from langgraph.func import entrypoint, task # 导入 LangGraph Functional API
from langchain_openai import ChatOpenAI # 导入 OpenAI 聊天模型
from langchain.tools import tool # 导入 @tool 装饰器,用于定义 LLM 可调用的工具
from langchain_core.messages import (
HumanMessage, # 人类消息类型
AIMessage, # AI 回复消息类型
ToolMessage, # 工具返回消息类型
SystemMessage, # 系统消息类型
)
# ============================================================
# 定义 State
# ============================================================
# 注意:v0.4 的 State 设计发生了重大变化!
# v0.3 使用结构化字段(destination, days, budget 等),
# v0.4 改用 messages 列表承载所有对话内容。
# 原因:工具调用场景下,LLM 需要完整的对话历史------
# SystemMessage、HumanMessage、AIMessage(含 tool_calls)、ToolMessage------
# 这种多类型消息序列用 messages 列表管理更自然。
# 第 6 章将展示如何将两种方式结合(结构化字段 + messages 列表)。
class TravelPlanState(BaseModel):
"""旅行规划状态 v0.4:支持工具调用"""
# 对话消息列表(存储所有消息,包括工具调用和返回)
messages: list = Field(
default_factory=list, # 默认空列表,使用 default_factory 避免可变默认值问题
description="完整的对话消息列表",
)
# 用户原始输入
raw_input: str = Field(default="", description="用户的原始输入")
# 最终回复
final_output: str = Field(default="", description="最终回复给用户的内容")
# ============================================================
# 第一步:定义工具 --- 使用 @tool 装饰器
# 每个工具都有:名称、描述(告诉 LLM 何时使用)和参数
# ============================================================
@tool # @tool 装饰器:将函数标记为 LLM 可调用的工具
def search_flights(
departure: str, # 出发城市
destination: str, # 到达城市
date: str, # 出发日期,格式:YYYY-MM-DD
) -> str:
"""搜索航班信息。当用户询问机票、航班、飞行相关问题时使用此工具。
参数:
- departure: 出发城市名称(中文)
- destination: 到达城市名称(中文)
- date: 出发日期,格式为 YYYY-MM-DD
"""
# 模拟航班数据(实际应用中应调用真实机票 API)
mock_flights = [
{
"flight_no": "CA1234", # 航班号
"airline": "中国国际航空", # 航空公司
"departure_time": f"{date} 08:00", # 出发时间
"arrival_time": f"{date} 11:30", # 到达时间
"duration": "3小时30分", # 飞行时长
"price": 2800, # 价格(人民币)
"stops": "直飞", # 经停信息
},
{
"flight_no": "MU5678", # 航班号
"airline": "东方航空", # 航空公司
"departure_time": f"{date} 13:00", # 出发时间
"arrival_time": f"{date} 16:20", # 到达时间
"duration": "3小时20分", # 飞行时长
"price": 2200, # 价格(人民币)
"stops": "直飞", # 经停信息
},
{
"flight_no": "CZ9012", # 航班号
"airline": "南方航空", # 航空公司
"departure_time": f"{date} 07:30", # 出发时间
"arrival_time": f"{date} 12:00", # 到达时间
"duration": "4小时30分", # 飞行时长
"price": 1800, # 价格(人民币)
"stops": "经停首尔", # 经停信息
},
]
# 将模拟数据格式化为可读文本
result = f"从 {departure} 到 {destination} 的航班({date}):\n\n"
for i, flight in enumerate(mock_flights, 1): # enumerate 从 1 开始编号
result += (
f"{i}. {flight['airline']} {flight['flight_no']}\n" # 航空公司和航班号
f" 出发:{flight['departure_time']}\n" # 出发时间
f" 到达:{flight['arrival_time']}\n" # 到达时间
f" 时长:{flight['duration']}\n" # 飞行时长
f" 价格:¥{flight['price']}\n" # 价格
f" 类型:{flight['stops']}\n\n" # 经停信息
)
return result # 返回格式化后的航班信息
@tool # 标记为 LLM 可调用的工具
def search_hotels(
city: str, # 城市名称
check_in: str, # 入住日期
check_out: str, # 退房日期
budget: str = "中等", # 预算水平,默认值"中等"
) -> str:
"""搜索酒店信息。当用户询问酒店、住宿、民宿相关问题时使用此工具。
参数:
- city: 城市名称(中文)
- check_in: 入住日期,格式 YYYY-MM-DD
- check_out: 退房日期,格式 YYYY-MM-DD
- budget: 预算水平(经济型/中等/豪华),默认中等
"""
# 模拟酒店数据(实际应用中应调用真实酒店 API)
mock_hotels = [
{
"name": f"{city}中心商务酒店", # 酒店名称
"location": f"{city}市中心", # 酒店位置
"rating": 4.5, # 评分
"price_per_night": 500, # 每晚价格
"amenities": "WiFi、健身房、早餐", # 设施
"type": "中等", # 酒店类型
},
{
"name": f"{city}花园酒店", # 酒店名称
"location": f"{city}景区附近", # 酒店位置
"rating": 4.8, # 评分
"price_per_night": 900, # 每晚价格
"amenities": "WiFi、泳池、SPA、早餐", # 设施
"type": "豪华", # 酒店类型
},
{
"name": f"{city}青年旅舍", # 酒店名称
"location": f"{city}交通枢纽旁", # 酒店位置
"rating": 4.2, # 评分
"price_per_night": 150, # 每晚价格
"amenities": "WiFi、公共厨房", # 设施
"type": "经济型", # 酒店类型
},
]
# 根据预算筛选酒店
filtered = [h for h in mock_hotels if h["type"] == budget] # 筛选匹配预算的酒店
if not filtered:
filtered = mock_hotels # 如果没有匹配的,显示全部
# 格式化酒店信息
result = f"{city} 酒店推荐({check_in} 至 {check_out},预算:{budget}):\n\n"
for i, hotel in enumerate(filtered, 1): # 从 1 开始编号
result += (
f"{i}. {hotel['name']}\n" # 酒店名称
f" 位置:{hotel['location']}\n" # 位置
f" 评分:{hotel['rating']}/5.0\n" # 评分
f" 价格:¥{hotel['price_per_night']}/晚\n" # 每晚价格
f" 设施:{hotel['amenities']}\n\n" # 设施
)
return result # 返回格式化后的酒店信息
@tool # 标记为 LLM 可调用的工具
def get_weather(
city: str, # 城市名称
days: int = 3, # 查询天数,默认 3 天
) -> str:
"""查询天气信息。当用户询问天气、气候、温度相关问题时使用此工具。
参数:
- city: 城市名称(中文)
- days: 查询天数,默认 3 天
"""
# 模拟天气数据(实际应用中应调用真实天气 API)
import random # 用于生成随机温度
weather_conditions = ["晴", "多云", "阴", "小雨", "阵雨"] # 可能的天气状况
result = f"{city} 未来 {days} 天天气预报:\n\n"
for i in range(days): # 遍历每一天
# 随机生成温度(模拟不同季节的温度范围)
high = random.randint(20, 30) # 最高温 20-30°C
low = random.randint(10, 18) # 最低温 10-18°C
condition = random.choice(weather_conditions) # 随机选择天气状况
result += f"第{i+1}天:{condition},{low}°C ~ {high}°C\n" # 格式化输出
return result # 返回格式化后的天气信息
# ============================================================
# 第二步:初始化 LLM 并绑定工具
# ============================================================
# 将所有工具放入列表
tools = [search_flights, search_hotels, get_weather] # LLM 可以调用的工具集合
# 初始化 LLM
llm = ChatOpenAI(
model="gpt-4o-mini", # 使用 gpt-4o-mini(支持 Function Calling)
temperature=0.3, # 工具调用场景建议使用较低温度,确保调用准确
)
# 将工具绑定到 LLM:bind_tools() 告诉模型有哪些工具可用
# 绑定后,LLM 会在需要时自动返回 tool_calls 而不是普通文本
llm_with_tools = llm.bind_tools(tools) # 返回绑定了工具的新 LLM 实例
# ============================================================
# 第三步:定义 Task --- 带工具调用的 LLM 交互
# ============================================================
@task # 标记为 LangGraph 任务
def call_llm_with_tools(state: TravelPlanState) -> TravelPlanState:
"""调用 LLM 并处理工具调用循环。
核心逻辑:LLM 回复 → 检查是否需要工具 → 执行工具 → 返回结果给 LLM → 重复。
"""
# 获取当前消息列表
messages = state.messages.copy() # 复制消息列表,避免修改原 state
# ---- 工具调用循环 ----
# 循环条件:LLM 可能返回 tool_calls(需要调用工具)
# 循环体内:执行工具 → 将结果追加到消息 → 再次调用 LLM
while True:
# 调用绑定了工具的 LLM
response = llm_with_tools.invoke(messages) # response 是 AIMessage
messages.append(response) # 将 AI 回复追加到消息列表
# 检查 LLM 是否请求调用工具
if response.tool_calls: # tool_calls 是列表,非空表示 LLM 想调用工具
# 遍历每个工具调用请求
for tool_call in response.tool_calls:
# 获取工具名称
tool_name = tool_call["name"] # 如 "search_flights"
# 获取工具参数(JSON 字符串格式)
tool_args = tool_call["args"] # 如 {"departure": "北京", ...}
# 根据工具名称查找对应的工具函数
tool_fn = None # 初始化工具函数引用
for t in tools: # 遍历工具列表
if t.name == tool_name: # 匹配工具名称
tool_fn = t # 找到对应工具
break # 跳出循环
# 执行工具并获取结果
if tool_fn:
# 调用工具函数,**tool_args 将字典解包为关键字参数
tool_result = tool_fn.invoke(tool_args) # 工具返回字符串结果
else:
# 如果找不到对应工具,返回错误信息
tool_result = f"错误:未找到工具 {tool_name}"
# 将工具返回结果包装为 ToolMessage 并追加到消息列表
messages.append(
ToolMessage(
content=tool_result, # 工具返回的内容
tool_call_id=tool_call["id"], # 对应 tool_call 的 ID
)
)
else:
# 没有 tool_calls:LLM 认为任务完成,不再需要工具
break # 退出循环
# 更新 state:保存最终的消息列表和 LLM 回复
updated_state = state.model_copy(
update={
"messages": messages, # 保存完整消息历史
"final_output": messages[-1].content, # 最后一条消息是 LLM 的最终回复
}
)
return updated_state # 返回更新后的 state
# ============================================================
# 第四步:定义 Entrypoint
# ============================================================
@entrypoint() # 标记为工作流入口
def travel_planner_v4(raw_input: str) -> TravelPlanState:
"""旅行规划助手 v0.4 入口函数。
支持通过工具调用查询航班、酒店和天气。
"""
# 构建初始消息列表
system_prompt = (
"你是一个专业的旅行规划助手。"
"你可以使用以下工具帮助用户:"
"1. search_flights - 搜索航班信息"
"2. search_hotels - 搜索酒店信息"
"3. get_weather - 查询天气信息"
"当用户提出相关需求时,请主动使用合适的工具。"
"回答要友好、专业、清晰。"
)
messages = [
SystemMessage(content=system_prompt), # 系统提示词
HumanMessage(content=raw_input), # 用户输入
]
# 创建初始 State
state = TravelPlanState(
raw_input=raw_input, # 保存用户原始输入
messages=messages, # 初始化消息列表
)
# 调用 LLM Task(内部包含工具调用循环)
state = call_llm_with_tools(state).result()
# 返回最终 state
return state
# ============================================================
# 程序入口
# ============================================================
if __name__ == "__main__":
# 检查 API Key
if not os.environ.get("OPENAI_API_KEY"):
print("错误:未设置 OPENAI_API_KEY 环境变量!")
print("请先运行:export OPENAI_API_KEY='your-api-key-here'")
exit(1)
print("=" * 60)
print("旅行规划助手 v0.4 --- 工具调用演示")
print("=" * 60)
# 测试 1:查询航班
print("\n【测试 1】查询航班")
print("-" * 60)
result1 = travel_planner_v4.invoke("帮我查一下7月20日从北京到东京的航班")
print(result1.final_output) # 打印 LLM 最终回复
# 测试 2:查询酒店
print("\n" + "=" * 60)
print("【测试 2】查询酒店")
print("-" * 60)
result2 = travel_planner_v4.invoke("我7月20日到22日要去东京,帮我找找中等价位的酒店")
print(result2.final_output) # 打印 LLM 最终回复
# 测试 3:查询天气
print("\n" + "=" * 60)
print("【测试 3】查询天气")
print("-" * 60)
result3 = travel_planner_v4.invoke("东京未来5天天气怎么样?")
print(result3.final_output) # 打印 LLM 最终回复
# 测试 4:综合查询(LLM 可能需要调用多个工具)
print("\n" + "=" * 60)
print("【测试 4】综合查询")
print("-" * 60)
result4 = travel_planner_v4.invoke(
"我计划7月20日从上海去东京玩3天,帮我查一下航班、酒店和天气"
)
print(result4.final_output) # 打印 LLM 最终回复
5.3.3 运行结果展示
测试 1(查询航班):
============================================================
旅行规划助手 v0.4 --- 工具调用演示
============================================================
【测试 1】查询航班
------------------------------------------------------------
为您查询到以下从北京到东京的航班(2026年7月20日):
1. **中国国际航空 CA1234**
- 出发:08:00
- 到达:11:30
- 时长:3小时30分
- 价格:¥2,800
- 类型:直飞
2. **东方航空 MU5678**
- 出发:13:00
- 到达:16:20
- 时长:3小时20分
- 价格:¥2,200
- 类型:直飞
3. **南方航空 CZ9012**
- 出发:07:30
- 到达:12:00
- 时长:4小时30分
- 价格:¥1,800
- 类型:经停首尔
推荐东方航空 MU5678,性价比最高!需要我帮您预订吗?
测试 4(综合查询):
【测试 4】综合查询
------------------------------------------------------------
好的!为您汇总一下从上海到东京的旅行信息:
**航班信息(7月20日):**
- 中国国际航空 CA1234:08:00-11:30,直飞,¥2,800
- 东方航空 MU5678:13:00-16:20,直飞,¥2,200
- 南方航空 CZ9012:07:30-12:00,经停首尔,¥1,800
**酒店推荐(7月20日-22日,中等预算):**
- 东京中心商务酒店:市中心,评分4.5,¥500/晚
**天气预告(7月20日-22日):**
- 第1天:晴,16°C ~ 28°C
- 第2天:多云,12°C ~ 25°C
- 第3天:阵雨,14°C ~ 22°C
建议您选择东方航空 MU5678,下午到达后可以直接入住酒店。第三天有雨,建议安排室内活动!
5.3.4 代码逐段解析
第一部分:@tool 装饰器
python
@tool
def search_flights(departure: str, destination: str, date: str) -> str:
"""搜索航班信息。当用户询问机票、航班、飞行相关问题时使用此工具。"""
@tool 装饰器将普通 Python 函数转换为 LLM 可调用的工具。关键点:(1) 函数文档字符串(docstring)会被发送给 LLM 作为工具描述,告诉模型何时使用此工具;(2) 参数类型注解也会被发送给 LLM,帮助模型理解每个参数的含义;(3) 返回值必须是字符串(str),因为 LLM 只能处理文本。
第二部分:bind_tools()
python
llm_with_tools = llm.bind_tools(tools)
bind_tools() 将工具列表「注册」到 LLM。调用后,每次 llm_with_tools.invoke() 时,LLM 都会收到「你可以使用这些工具」的信息。当 LLM 认为需要工具时,它会返回 tool_calls 而不是普通文本。
第三部分:工具调用循环
python
while True:
response = llm_with_tools.invoke(messages)
messages.append(response)
if response.tool_calls:
for tool_call in response.tool_calls:
tool_fn = find_tool(tool_call["name"])
tool_result = tool_fn.invoke(tool_call["args"])
messages.append(ToolMessage(content=tool_result, ...))
else:
break
这是工具调用循环的核心逻辑:(1) 调用 LLM;(2) 检查是否有 tool_calls;(3) 如果有,执行工具并将结果追加到消息列表;(4) 再次调用 LLM,让它基于工具结果继续思考;(5) 重复直到 LLM 不再需要工具(返回纯文本回复)。
第四部分:ToolMessage
python
messages.append(
ToolMessage(
content=tool_result,
tool_call_id=tool_call["id"],
)
)
ToolMessage 是专门用于包装工具返回结果的消息类型。它必须包含 tool_call_id 来关联对应的工具调用请求,否则 LLM 无法正确匹配。
5.4 API 速查
| API | 类型 | 说明 | 签名/导入路径 |
|---|---|---|---|
@tool |
装饰器 | 将函数标记为 LLM 可调用的工具 | from langchain.tools import tool |
bind_tools(tools) |
方法 | 将工具列表绑定到 LLM | llm_with_tools = llm.bind_tools([t1, t2]) |
AIMessage.tool_calls |
属性 | LLM 返回的工具调用请求列表 | response.tool_calls |
ToolMessage |
类 | 封装工具返回结果的消息类型 | from langchain_core.messages import ToolMessage |
SystemMessage |
类 | 系统级别的消息(设定 AI 行为) | from langchain_core.messages import SystemMessage |
HumanMessage |
类 | 用户发送的消息 | from langchain_core.messages import HumanMessage |
.invoke(args) |
方法 | 同步调用工具函数 | result = my_tool.invoke({"param": "value"}) |
5.5 常见错误与避坑指南
错误 1:忘记将工具结果追加到消息列表
错误代码:
python
while True:
response = llm_with_tools.invoke(messages)
if response.tool_calls:
for tool_call in response.tool_calls:
tool_result = execute_tool(tool_call)
# 错误:工具结果没有追加到 messages 中
# LLM 不知道工具已经执行了,会重复请求相同的工具调用
else:
break
正确代码:
python
while True:
response = llm_with_tools.invoke(messages)
messages.append(response) # 先追加 AI 消息
if response.tool_calls:
for tool_call in response.tool_calls:
tool_result = execute_tool(tool_call)
# 正确:将工具结果作为 ToolMessage 追加到消息列表
messages.append(
ToolMessage(
content=tool_result,
tool_call_id=tool_call["id"],
)
)
else:
break
原因:LLM 是无状态的------它不会记住上一次调用时发生了什么。每次调用都需要完整的消息历史。如果不追加工具结果,LLM 会认为工具还没有被调用,从而陷入无限循环。
错误 2:工具函数没有文档字符串
错误代码:
python
@tool
def search_flights(departure: str, destination: str, date: str) -> str:
# 错误:没有 docstring,LLM 不知道何时使用此工具
return "模拟航班数据..."
正确代码:
python
@tool
def search_flights(departure: str, destination: str, date: str) -> str:
"""搜索航班信息。当用户询问机票、航班、飞行相关问题时使用此工具。
参数:
- departure: 出发城市名称(中文)
- destination: 到达城市名称(中文)
- date: 出发日期,格式为 YYYY-MM-DD
"""
return "模拟航班数据..."
原因 :@tool 装饰器会将函数的 docstring 发送给 LLM 作为工具的使用说明。没有 docstring 的工具,LLM 不知道何时调用它、如何传参,导致工具永远不会被使用。
错误 3:工具返回值不是字符串
错误代码:
python
@tool
def search_flights(departure: str, destination: str, date: str) -> dict:
# 错误:返回字典而不是字符串
return {"flights": [...], "count": 3}
正确代码:
python
@tool
def search_flights(departure: str, destination: str, date: str) -> str:
# 正确:返回格式化后的字符串
result = "航班信息:\n"
for flight in mock_flights:
result += f"- {flight['airline']}: ¥{flight['price']}\n"
return result # 返回字符串
原因 :LLM 只能处理文本。工具返回值必须是字符串(str),因为返回值会被包装成 ToolMessage(content=...) 发送给 LLM。如果返回字典或列表,LLM 无法正确理解。
错误 4:忘记在工具调用循环中处理异常
错误代码:
python
while True:
response = llm_with_tools.invoke(messages)
messages.append(response)
if response.tool_calls:
for tool_call in response.tool_calls:
tool_fn = find_tool(tool_call["name"])
tool_result = tool_fn.invoke(tool_call["args"]) # 可能抛出异常
messages.append(ToolMessage(content=tool_result, ...))
else:
break
正确代码:
python
while True:
response = llm_with_tools.invoke(messages)
messages.append(response)
if response.tool_calls:
for tool_call in response.tool_calls:
try:
tool_fn = find_tool(tool_call["name"])
if tool_fn is None:
tool_result = f"错误:未找到工具 {tool_call['name']}"
else:
tool_result = tool_fn.invoke(tool_call["args"])
except Exception as e:
# 捕获异常,将错误信息作为工具结果返回给 LLM
tool_result = f"工具执行出错:{str(e)}"
messages.append(
ToolMessage(content=tool_result, tool_call_id=tool_call["id"])
)
else:
break
原因:工具执行过程中可能因为各种原因失败(网络超时、参数错误、数据不存在等)。如果不捕获异常,整个工作流会崩溃。将异常信息作为工具结果返回给 LLM,LLM 可以理解错误并采取替代方案。
错误 5:工具调用循环无限循环
错误代码:
python
while True:
response = llm_with_tools.invoke(messages)
messages.append(response)
if response.tool_calls:
for tool_call in response.tool_calls:
tool_result = execute_tool(tool_call)
messages.append(ToolMessage(content=tool_result, ...))
# 错误:没有 break 条件!即使没有 tool_calls 也不会退出
正确代码:
python
max_iterations = 10 # 设置最大迭代次数,防止无限循环
iteration = 0
while iteration < max_iterations:
iteration += 1
response = llm_with_tools.invoke(messages)
messages.append(response)
if response.tool_calls:
for tool_call in response.tool_calls:
tool_result = execute_tool(tool_call)
messages.append(ToolMessage(content=tool_result, ...))
else:
break # 正确:没有 tool_calls 时退出循环
else:
# 达到最大迭代次数仍未结束
print("警告:工具调用循环达到最大迭代次数,强制退出")
原因:即使正确处理了 tool_calls,在某些边缘情况下 LLM 可能反复请求工具调用。设置最大迭代次数是安全网,防止无限循环消耗 API 额度。
5.6 最佳实践总结
-
工具描述要详细准确:docstring 是 LLM 理解工具的唯一途径。清晰描述工具的功能、使用场景和每个参数的含义。好的描述 = 准确的工具调用。
-
工具返回值要结构化:虽然返回值必须是字符串,但字符串内容应该结构化(使用列表、表格、分段等格式)。这帮助 LLM 更好地理解工具结果并提供高质量回复。
-
设置最大迭代次数 :工具调用循环中始终设置
max_iterations。正常情况 LLM 会在 1-3 轮内完成,但安全网是必需的。 -
工具异常要捕获并返回给 LLM:不要在工具内部让异常向上传播。捕获异常并将其作为工具结果返回,让 LLM 知道发生了什么并尝试替代方案。
-
bind_tools 放在模块顶层 :
bind_tools()只需要调用一次,将绑定后的 LLM 实例放在全局作用域复用。不要在每次 task 调用时重新绑定。