没有工具的 LLM 只是一个"纸上谈兵"的聊天机器人。本章将深入讲解如何为 LLM 赋予调用外部工具的能力,让它不仅能"思考",还能"执行"。
上一篇我们盘点了 LangChain 三大核心组件,把模型、提示词与输出解析串成了基础链路。但链路再顺,LLM 也只会"说"------查不了天气,算不了账,动不了数据库。本篇解决"从会说到会做":先用 @tool 定义工具,用 Pydantic 约束参数,再对比 bind_tools 与 create_agent 两种接入方式,最后拆解工具调用的完整生命周期。读完你就能搭出一个真正能干活的 Agent。
一、@tool 装饰器
先从最基础的做起:定义一个工具。v1.0 里这件事简单到只需要一个装饰器。
1.1 最简单的工具定义
在 LangChain v1.0 中,定义一个工具最简单的方式就是使用 @tool 装饰器(从 langchain.tools 导入):
from langchain.tools import tool
@tool
def get_current_time() -> str:
"""获取当前的日期和时间"""
from datetime import datetime
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
这个装饰器自动做了三件事:
-
函数名 → 工具名 :
get_current_time成为工具的名称 -
docstring → 工具描述 :
"""获取当前的日期和时间"""成为工具的说明,LLM 会据此判断何时调用 -
类型注解 → JSON Schema :返回值
-> str生成工具的 JSON Schema查看自动生成的工具定义
print(get_current_time.name) # "get_current_time"
print(get_current_time.description) # "获取当前的日期和时间"
print(get_current_time.args) # {} # 无参数
1.2 带参数的工具
无参工具只是热身,真正有用的工具大多需要接收参数:
@tool
def calculate(expression: str) -> str:
"""执行数学计算并返回结果"""
try:
# 安全地执行数学表达式
result = eval(expression, {"__builtins__": {}}, {})
return f"计算结果: {result}"
except Exception as e:
return f"计算错误: {str(e)}"
# 查看工具参数 schema
print(calculate.args)
# {'expression': {'title': 'Expression', 'description': '数学表达式', 'type': 'string'}}
1.3 类型注解自动生成 JSON Schema
LangChain 会根据 Python 类型注解自动生成 JSON Schema,支持丰富的类型:
from typing import List, Optional, Dict, Any
@tool
def search_database(
query: str,
limit: int = 10,
tags: Optional[List[str]] = None,
sort_by: str = "relevance",
) -> List[Dict[str, Any]]:
"""搜索数据库中的记录
Args:
query: 搜索关键词
limit: 返回结果数量上限
tags: 筛选标签列表
sort_by: 排序方式 (relevance / date / rating)
"""
# 模拟数据库查询
results = [
{"id": 1, "title": f"结果 {i}", "score": 0.95 - i * 0.1}
for i in range(limit)
]
return results
# 自动生成的 args_schema
print(search_database.args)
# {
# 'query': {'title': 'Query', 'type': 'string'},
# 'limit': {'title': 'Limit', 'default': 10, 'type': 'integer'},
# 'tags': {'title': 'Tags', 'default': None, 'type': 'array', 'items': {'type': 'string'}},
# 'sort_by': {'title': 'Sort By', 'default': 'relevance', 'type': 'string'},
# }
类型标注与 JSON Schema 对照表
| Python 类型 | JSON Schema |
|---|---|
str |
{"type": "string"} |
int |
{"type": "integer"} |
float |
{"type": "number"} |
bool |
{"type": "boolean"} |
List[str] |
{"type": "array", "items": {"type": "string"}} |
Optional[str] |
{"type": "string", "default": None} |
Dict[str, Any] |
{"type": "object"} |
1.4 完整的日期工具示例
看一个把可选参数与默认值组合起来的完整示例:
from langchain.tools import tool
from datetime import datetime, timedelta
from typing import Optional
@tool
def get_date_info(
date_str: Optional[str] = None,
format: str = "%Y-%m-%d",
) -> str:
"""获取日期信息,包括星期、是否为工作日等
Args:
date_str: 日期字符串,格式如 2024-01-15。默认为今天
format: 日期格式,默认为 %Y-%m-%d
"""
if date_str:
date = datetime.strptime(date_str, "%Y-%m-%d")
else:
date = datetime.now()
weekdays = ["星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日"]
weekday = weekdays[date.weekday()]
# 判断是否为工作日(周一到周五)
is_workday = date.weekday() < 5
return (
f"日期: {date.strftime(format)}\n"
f"星期: {weekday}\n"
f"工作日: {'是' if is_workday else '否(周末)'}"
)
# 测试
print(get_date_info.invoke({"date_str": "2024-06-01"}))
# 日期: 2024-06-01
# 星期: 星期六
# 工作日: 否(周末)
1.5 异步工具
对于 I/O 密集型操作(如网络请求、文件读写),应该使用异步工具:
import httpx
from langchain.tools import tool
@tool
async def fetch_website_title(url: str) -> str:
"""异步获取网页的标题
Args:
url: 网页 URL
"""
async with httpx.AsyncClient(timeout=10) as client:
response = await client.get(url)
response.raise_for_status()
# 简单提取 title 标签
content = response.text
start = content.find("<title>") + len("<title>")
end = content.find("</title>", start)
if start > 0 and end > start:
return content[start:end].strip()
return "无法找到网页标题"
# 调用异步工具
# result = await fetch_website_title.ainvoke({"url": "https://example.com"})
1.6 工具与错误处理
工具在执行过程中可能出错,良好的错误处理能让 Agent 更好地应对异常:
@tool
def divide_numbers(a: float, b: float) -> str:
"""计算两个数字的除法
Args:
a: 被除数
b: 除数
"""
try:
result = a / b
return f"{a} ÷ {b} = {result}"
except ZeroDivisionError:
return "错误:除数不能为零"
except Exception as e:
return f"计算错误:{str(e)}"
# Agent 会接收到错误信息并判断下一步操作
1.7 实用工具集锦
最后是一组可以直接抄走的实用工具:
"""一个实用的工具集合"""
@tool
def get_random_joke() -> str:
"""获取一个随机笑话"""
import random
jokes = [
"为什么程序员总是混淆万圣节和圣诞节?因为 Oct 31 == Dec 25!",
"程序员 A:我有问题。程序员 B:让我 Google 一下。",
"我不是在写 bug,我是在创建 feature。",
]
return random.choice(jokes)
@tool
def convert_currency(
amount: float,
from_currency: str,
to_currency: str,
) -> str:
"""模拟货币汇率转换
Args:
amount: 金额
from_currency: 源货币代码(如 USD, CNY, EUR)
to_currency: 目标货币代码
"""
# 模拟汇率(实际场景应该调用 API)
rates = {
"USD": {"CNY": 7.24, "EUR": 0.92, "JPY": 149.50},
"CNY": {"USD": 0.14, "EUR": 0.13, "JPY": 20.65},
"EUR": {"USD": 1.09, "CNY": 7.87, "JPY": 162.50},
}
from_currency = from_currency.upper()
to_currency = to_currency.upper()
if from_currency in rates and to_currency in rates[from_currency]:
rate = rates[from_currency][to_currency]
result = amount * rate
return f"{amount} {from_currency} = {result:.2f} {to_currency}(汇率:{rate})"
return f"暂不支持 {from_currency} 到 {to_currency} 的汇率转换"
@tool
def generate_qr_code(data: str, size: int = 200) -> str:
"""生成 QR 码的 URL(使用在线 API)
Args:
data: 要编码的数据
size: 图片尺寸(像素)
"""
from urllib.parse import quote
encoded = quote(data)
return f""
二、工具参数类型安全
工具能跑还不够,得跑得稳------当 LLM 传来的参数类型不对、取值越界时,谁来兜底?答案是 Pydantic。
2.1 使用 Pydantic 定义复杂参数
当工具参数复杂时,使用 args_schema 结合 Pydantic 模型可以获得更好的类型安全:
from pydantic import BaseModel, Field
from typing import List, Optional
from langchain.tools import tool
# 定义参数模型
class WeatherQuery(BaseModel):
"""天气查询参数"""
city: str = Field(description="城市名称(中文)")
days: int = Field(
default=1,
description="预报天数(1-7)",
ge=1,
le=7,
)
units: str = Field(
default="metric",
description="温度单位:metric(摄氏度)或 imperial(华氏度)",
)
@tool(args_schema=WeatherQuery)
def get_weather(city: str, days: int = 1, units: str = "metric") -> str:
"""获取指定城市的天气预报"""
# 工具实现...
return f"{city}未来{days}天天气预报:晴天,温度适宜。"
# 查看 schema 可以看到明确的约束条件
print(get_weather.args)
# {
# 'city': {'title': 'City', 'description': '城市名称(中文)', 'type': 'string'},
# 'days': {'title': 'Days', 'description': '预报天数(1-7)', 'default': 1, 'minimum': 1, 'maximum': 7, 'type': 'integer'},
# 'units': {'title': 'Units', 'description': '温度单位:metric(摄氏度)或 imperial(华氏度)', 'default': 'metric', 'type': 'string'},
# }
2.2 Field 约束详解
Pydantic 的 Field 提供了丰富的参数约束:
from pydantic import BaseModel, Field
from typing import List, Optional, Literal
from enum import Enum
class SortOrder(str, Enum):
ASCENDING = "asc"
DESCENDING = "desc"
class SearchParams(BaseModel):
"""搜索参数"""
# 字符串约束
query: str = Field(
...,
description="搜索关键词",
min_length=1,
max_length=200,
)
# 数值范围约束
page: int = Field(
default=1,
description="页码",
ge=1, # >= 1
le=1000, # <= 1000
)
page_size: int = Field(
default=20,
description="每页数量",
ge=5,
le=100,
)
# 固定选项约束
category: Optional[Literal["tech", "science", "art", "music"]] = Field(
default=None,
description="搜索分类",
)
# 枚举约束
sort: SortOrder = Field(
default=SortOrder.DESCENDING,
description="排序方式",
)
# 列表约束
tags: List[str] = Field(
default=[],
description="筛选标签",
max_length=10,
)
2.3 嵌套对象的参数定义
对于更复杂的场景,Pydantic 支持嵌套模型:
from pydantic import BaseModel, Field
from typing import List, Optional
from datetime import datetime
class Location(BaseModel):
"""位置信息"""
city: str = Field(description="城市")
district: Optional[str] = Field(description="区/县")
latitude: Optional[float] = Field(description="纬度", ge=-90, le=90)
longitude: Optional[float] = Field(description="经度", ge=-180, le=180)
class EventSearchParams(BaseModel):
"""活动搜索参数(嵌套结构)"""
keyword: str = Field(description="搜索关键词")
location: Location = Field(description="位置信息")
date_range: Optional[str] = Field(
description="日期范围,如 '2024-01-01~2024-01-31'"
)
max_price: Optional[float] = Field(
description="最高价格",
ge=0,
)
categories: List[str] = Field(
default=[],
description="活动分类",
)
@tool(args_schema=EventSearchParams)
def search_events(
keyword: str,
location: dict,
date_range: Optional[str] = None,
max_price: Optional[float] = None,
categories: List[str] = [],
) -> str:
"""搜索本地活动"""
# location 参数接收一个字典,由 LLM 根据 Location 模型自动生成
city = location.get("city", "未知")
return f"在{city}找到以下关于'{keyword}'的活动..."
2.4 类型安全的重要性
不使用类型约束的风险:
# ❌ 危险:没有类型约束
@tool
def update_record(record_id, data):
"""更新记录"""
# 如果 LLM 传入了错误类型,这里可能崩溃
# 或者更糟:SQL 注入
pass
# ✅ 安全:有类型约束
class UpdateRecordParams(BaseModel):
record_id: int = Field(..., description="记录ID", gt=0)
data: str = Field(..., description="更新数据", max_length=1000)
timestamp: Optional[datetime] = Field(default=None, description="更新时间")
@tool(args_schema=UpdateRecordParams)
def update_record_safe(record_id: int, data: str, timestamp: Optional[datetime] = None) -> str:
"""安全地更新记录"""
# 参数已经过 Pydantic 验证
return f"记录 {record_id} 已更新"
三、bind_tools 与 create_agent
工具备好了,接下来把它交给 LLM------这一步有两条路:底层的 bind_tools 和高层的 create_agent。
3.1 方式一:手动 bind_tools(底层控制)
如果你需要手动控制工具绑定的细节,可以直接使用 bind_tools:
from langchain.chat_models import init_chat_model
from langchain.tools import tool
# 1. 定义工具
@tool
def get_weather(city: str) -> str:
"""获取城市天气"""
return f"{city}:晴天,20°C"
@tool
def get_current_time() -> str:
"""获取当前时间"""
from datetime import datetime
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# 2. 创建 LLM 并绑定工具
llm = init_chat_model("openai:gpt-4o-mini", temperature=0)
tools = [get_weather, get_current_time]
llm_with_tools = llm.bind_tools(tools)
# 3. 调用------LLM 会自主决定是否使用工具
response = llm_with_tools.invoke("北京今天天气怎么样?")
# 如果 LLM 决定调工具,response.tool_calls 会有内容
tool_choice 参数控制
tool_choice 控制 LLM 调用工具的行为模式:
# tool_choice = "auto"(默认)
# LLM 自主决定是否调用工具、调用哪个工具
llm_auto = llm.bind_tools(tools, tool_choice="auto")
# tool_choice = "any"(或 "required")
# LLM 必须调用工具,不能直接文本回答
llm_force = llm.bind_tools(tools, tool_choice="any")
# tool_choice = "none"
# LLM 不能调用任何工具,只做文本生成
llm_none = llm.bind_tools(tools, tool_choice="none")
# tool_choice = {"type": "function", "function": {"name": "get_weather"}}
# 强制使用指定工具
llm_specific = llm.bind_tools(tools, tool_choice={
"type": "function",
"function": {"name": "get_weather"}
})
三种模式的对比:
# auto 模式:灵活,但不一定调用工具
response = llm_auto.invoke("你好!")
print(response.tool_calls) # [] # 打招呼不调用工具
# any 模式:必须调用工具
response = llm_force.invoke("你好!")
print(response.tool_calls)
# [ToolCall(name='get_current_time', args={}, id='...')]
# none 模式:不能调用工具
response = llm_none.invoke("北京天气怎么样?")
print(response.tool_calls) # []
3.2 方式二:create_agent(v1.0 推荐)
v1.0 推荐直接使用 create_agent,它会自动处理工具绑定和调用循环:
from langchain.agents import create_agent
from langchain.tools import tool
@tool
def get_weather(city: str) -> str:
"""获取城市天气"""
return f"{city}:晴天,20°C"
@tool
def get_current_time() -> str:
"""获取当前时间"""
from datetime import datetime
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# create_agent 自动调用 bind_tools
agent = create_agent(
model="openai:gpt-4o-mini",
tools=[get_weather, get_current_time],
)
# 一次 invoke,内部自动完成:推理→调用工具→观察结果→继续推理→输出
result = agent.invoke({
"messages": [("human", "北京天气怎么样?现在几点了?")]
})
print(result["messages"][-1].content)
create_agent 相比于手动的优势:
| 维度 | 手动 bind_tools | create_agent |
|---|---|---|
| 工具绑定 | 手动调用 bind_tools |
自动完成 |
| 调用循环 | 需要自己实现 | 内置 LangGraph 循环 |
| 记忆持久化 | 需额外配置 checkpointer | 支持 checkpointer 参数 |
| 流式输出 | 需额外配置 | 内置支持 |
| Middleware | 需手动集成 | 支持 middleware 参数 |
| 结构化输出 | 需额外处理 | 支持 response_format 参数 |
3.3 tool_calls 数据结构
当 LLM 决定调用工具时,返回的 AIMessage 会包含 tool_calls 属性:
response = llm_with_tools.invoke("北京天气怎么样?")
# 查看 tool_calls 结构
for tc in response.tool_calls:
print(f"工具名称: {tc['name']}")
print(f"参数: {tc['args']}")
print(f"调用ID: {tc['id']}")
print("---")
# 实际输出示例:
# 工具名称: get_weather
# 参数: {'city': '北京'}
# 调用ID: call_abc123...
tool_calls 的完整结构:
# 标准的 ToolCall 结构
tool_call = {
"name": "get_weather", # 工具名称
"args": { # 调用参数
"city": "北京"
},
"id": "call_abc123def456", # 唯一调用 ID(用于匹配 ToolMessage)
"type": "tool_call", # 类型标识
}
3.4 处理并行工具调用
LLM 支持一次返回多个工具调用(并行调用),这在需要获取多种信息时非常高效:
from langchain.messages import HumanMessage, ToolMessage
# 模拟一个需要多种信息的查询
response = llm_with_tools.invoke(
"北京和上海今天天气怎么样?现在几点了?"
)
# LLM 可能同时调用三个工具
print(f"同时调用 {len(response.tool_calls)} 个工具:")
for tc in response.tool_calls:
print(f" → {tc['name']}({tc['args']})")
# 并行执行所有工具调用
results = []
for tc in response.tool_calls:
if tc["name"] == "get_weather":
result = get_weather.invoke(tc["args"])
elif tc["name"] == "get_current_time":
result = get_current_time.invoke(tc["args"])
results.append(ToolMessage(content=result, tool_call_id=tc["id"]))
四、工具调用生命周期
前几节陆续出现了 tool_calls、ToolMessage 这些名词,这一节把它们串成一条完整的链路。
4.1 完整流程图解
用户输入:"北京今天天气怎么样?适合穿什么衣服?"
│
▼
┌─────────────────────────────────────────────┐
│ 步骤1: LLM 推理 │
│ ───────────────────────────────── │
│ 输入: [...] HumanMessage("北京今天天气...") │
│ 思考: "用户想知道北京的天气, │
│ 我有 get_weather 工具可以获取天气信息" │
│ 输出: AIMessage(tool_calls=[...]) │
└─────────────────────────────────────────────┘
│
│ tool_calls 非空
▼
┌─────────────────────────────────────────────┐
│ 步骤2: 解析 tool_calls │
│ ───────────────────────────────── │
│ for tc in response.tool_calls: │
│ tool_name = tc["name"] │
│ tool_args = tc["args"] │
│ tool_id = tc["id"] │
└─────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ 步骤3: 执行工具函数 │
│ ───────────────────────────────── │
│ result = get_weather(city="北京") │
│ → "晴天,15-25°C,微风" │
└─────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ 步骤4: 包装为 ToolMessage │
│ ───────────────────────────────── │
│ ToolMessage( │
│ content="晴天,15-25°C,微风", │
│ tool_call_id="call_xxx" │
│ ) │
└─────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ 步骤5: 送回 LLM 继续推理 │
│ ───────────────────────────────── │
│ 输入: [...原始消息..., │
│ AIMessage(tool_calls), │
│ ToolMessage(天气结果)] │
│ 思考: "北京15-25°C,适合穿薄外套" │
│ 输出: AIMessage(content="北京今天...") │
└─────────────────────────────────────────────┘
│
│ tool_calls 为空 → 输出最终答案
▼
最终输出:"北京今天天气晴朗,气温15-25°C,
建议穿薄外套或长袖衬衫。"
4.2 create_agent 内部的生命周期
当使用 create_agent 时,上述生命周期由 LangGraph 底层自动管理。以下是等价的手动实现,便于理解内部机制:
from langchain.agents import create_agent
from langchain.tools import tool
from langchain.messages import HumanMessage, AIMessage, ToolMessage
# 1. 定义工具
@tool
def get_weather(city: str) -> str:
"""获取指定城市的天气"""
data = {"北京": "晴天,15-25°C", "上海": "多云,20-28°C"}
return data.get(city, f"{city}天气数据不可用")
@tool
def calculator(expression: str) -> str:
"""执行数学计算"""
try:
return str(eval(expression, {"__builtins__": {}}, {}))
except Exception as e:
return f"计算错误: {e}"
# 2. 使用 create_agent(一行代码,自动管理生命周期)
agent = create_agent(
model="openai:gpt-4o-mini",
tools=[get_weather, calculator],
)
# 3. 运行------内部自动完成 推理→调用→观察→循环
result = agent.invoke({
"messages": [("human", "北京天气怎么样?25乘以4等于多少?")]
})
print(result["messages"][-1].content)
4.3 手动实现工具调用循环
如果想深入理解底层机制,可以手动实现:
from langchain.chat_models import init_chat_model
from langchain.messages import HumanMessage, AIMessage, ToolMessage
from langchain.tools import tool
# 1. 定义工具
@tool
def get_weather(city: str) -> str:
"""获取指定城市的天气"""
data = {"北京": "晴天,15-25°C", "上海": "多云,20-28°C"}
return data.get(city, f"{city}天气数据不可用")
@tool
def calculator(expression: str) -> str:
"""执行数学计算"""
try:
return str(eval(expression, {"__builtins__": {}}, {}))
except Exception as e:
return f"计算错误: {e}"
# 2. 工具映射表
tools_map = {
"get_weather": get_weather,
"calculator": calculator,
}
tools_list = list(tools_map.values())
# 3. 初始化 LLM
llm = init_chat_model("openai:gpt-4o-mini", temperature=0)
llm_with_tools = llm.bind_tools(tools_list)
# 4. 手动实现工具调用循环
def tool_calling_loop(user_input: str, max_iterations: int = 5):
"""工具调用主循环"""
messages = [HumanMessage(content=user_input)]
for iteration in range(max_iterations):
print(f"\n{'='*50}")
print(f"迭代 {iteration + 1}")
print(f"{'='*50}")
# Step 1: LLM 推理
response = llm_with_tools.invoke(messages)
messages.append(response)
# 检查是否有工具调用
if not response.tool_calls:
print("→ LLM 决定直接回答")
break
# Step 2 & 3: 解析并执行工具
for tc in response.tool_calls:
tool_name = tc["name"]
tool_args = tc["args"]
tool_id = tc["id"]
print(f"→ 调用工具: {tool_name}({tool_args})")
# 执行工具
tool_fn = tools_map.get(tool_name)
if tool_fn:
result = tool_fn.invoke(tool_args)
else:
result = f"错误:未知工具 {tool_name}"
print(f"→ 工具结果: {result}")
# Step 4: 包装为 ToolMessage
tool_message = ToolMessage(
content=str(result),
tool_call_id=tool_id,
)
messages.append(tool_message)
# Step 5: 继续循环,LLM 会基于工具结果继续推理
return messages[-1].content
# 5. 运行
result = tool_calling_loop("北京天气怎么样?25乘以4等于多少?")
print(f"\n最终答案: {result}")
4.4 使用 ToolNode
LangGraph 提供了 ToolNode 来简化工具执行步骤:
from langgraph.prebuilt import ToolNode
# 创建 ToolNode
tool_node = ToolNode(tools_list)
# 执行工具------自动处理 tool_calls 的解析和 ToolMessage 包装
messages = [HumanMessage(content="北京天气怎么样?")]
response = llm_with_tools.invoke(messages)
# 将 LLM 回复和工具节点串联
tool_messages = tool_node.invoke({"messages": [response]})
for msg in tool_messages:
print(f"{msg.type}: {msg.content}")
# tool: 晴天,15-25°C
4.5 完整的 Agent 实现(手写 LangGraph)
如果需要对 Agent 行为进行精细控制,可以手写 LangGraph:
from langgraph.graph import StateGraph, MessagesState
from langgraph.prebuilt import ToolNode
from langgraph.checkpoint.memory import InMemorySaver
from langchain.tools import tool
from langchain.chat_models import init_chat_model
from typing import Literal
# 1. 定义工具
@tool
def get_weather(city: str) -> str:
"""获取城市天气"""
data = {"北京": "晴天,15-25°C", "上海": "多云,20-28°C", "广州": "阵雨,25-32°C"}
return data.get(city, f"暂无{city}的天气数据")
@tool
def calculator(expression: str) -> str:
"""执行数学计算"""
try:
return str(eval(expression, {"__builtins__": {}}, {}))
except Exception as e:
return f"计算错误: {e}"
tools = [get_weather, calculator]
llm = init_chat_model("openai:gpt-4o-mini", temperature=0)
llm_with_tools = llm.bind_tools(tools)
# 2. 构建图
def call_model(state: MessagesState) -> MessagesState:
"""Agent 节点:LLM 推理"""
response = llm_with_tools.invoke(state["messages"])
return {"messages": [response]}
def should_continue(state: MessagesState) -> Literal["tools", "__end__"]:
"""条件边:判断是否继续调用工具"""
last_message = state["messages"][-1]
if last_message.tool_calls:
return "tools"
return "__end__"
# 3. 编译图
graph = StateGraph(MessagesState)
graph.add_node("agent", call_model)
graph.add_node("tools", ToolNode(tools))
graph.set_entry_point("agent")
graph.add_conditional_edges("agent", should_continue)
graph.add_edge("tools", "agent")
# 添加记忆
memory = InMemorySaver()
app = graph.compile(checkpointer=memory)
# 4. 运行
config = {"configurable": {"thread_id": "1"}}
result = app.invoke(
{"messages": [("human", "北京天气怎么样?")]},
config,
)
print(result["messages"][-1].content)
五、Middleware 与工具控制
工具能调用只是第一步,生产环境中还需要对调用过程做更精细的控制:
中间件(Middleware)系统提供了更精细的工具控制能力,包括自动重试(ToolRetryMiddleware)、人工审批(HumanInTheLoopMiddleware)、敏感信息过滤(PIIMiddleware)等。详见第4章 Middleware 系统。
六、MCP (Model Context Protocol) 简介
另一个值得关注的方向:工具未必都要自己写,也可以通过协议接入现成的工具生态:
MCP 是标准化 LLM 与外部工具通信的开放协议。LangChain 通过 MCP 集成包支持将远程 MCP Server 暴露的接口作为工具使用。详见第15章 上下文工程与MCP。
七、本章小结
回顾本章:@tool 装饰器让一个普通函数摇身变成 LLM 可调用的工具,Pydantic 为参数加上类型与约束的双保险,create_agent 则把绑定、执行、循环一股脑托管。三者拼出一条 tool_calls 到 ToolMessage 的完整闭环,这正是 Agent 从"能聊"走向"能干活"的地基。下一篇我们转向 LCEL(LangChain 表达式语言),看如何用管道操作符把这些组件灵活编排起来。
核心公式:
v1.0 工具系统 = @tool + Pydantic 类型安全 + create_agent