一、为什么要学 Tools?
先想一个问题:大模型本质上只能"生成文字"。它上知天文下知地理,但它不能真正做事情——不能查实时天气、不能查数据库、不能发邮件、不能下单。
工具(Tools)就是给大模型装上"手脚",让它从认识世界走向改变世界。
在 LangChain 中,工具是一个非常明确的概念:
工具 = 一个定义了输入和输出的可调用函数。
所以"工具调用(Tool Calling)"也常被叫做"函数调用(Function Calling)"。大模型会根据你和它的对话,自己决定"要不要调用某个函数、传什么参数"。
一句话总结:工具是构建智能体(Agent)的核心要素之一。
二、工具的两种调用方式
方式 1:直接调用(适合测试)
我们可以把一个普通函数用 @tool 装饰,然后像普通函数那样 .invoke() 它。
from langchain_core.tools import tool
@tool
def get_weather(city: str) -> str:
"""
获取指定城市的天气信息
参数:
city: 城市名称,如"北京"、"上海"
返回:
天气信息字符串
"""
# 你的实现
return city + "晴天,温度 15°C"直接调用:
result = get_weather.invoke({"city": "北京"})
print(result)输出:
北京晴天,温度 15°C这种方式就是手动喂参数、手动拿结果,适合测试工具本身是否正常。
方式 2:绑定到模型(主流,开发中用)
把工具"交给"模型,让 AI 来决定什么时候调用它。
from langchain.chat_models import init_chat_model
from dotenv import load_dotenv
import os
load_dotenv(override=True)
CLOSEAI_API_KEY = os.getenv("CLOSEAI_API_KEY")
CLOSEAI_BASE_URL = os.getenv("CLOSEAI_BASE_URL")
model = init_chat_model(
model="gpt-5.4-mini",
model_provider="openai",
api_key=CLOSEAI_API_KEY,
base_url=CLOSEAI_BASE_URL
)绑定工具并让 AI 判断:
from langchain_core.tools import tool
@tool
def get_weather(city: str) -> str:
"""获取指定城市的天气"""
return "晴天,温度 15°C"
# 把工具"绑"到模型上
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 想调用工具: [{'name': 'get_weather', 'args': {'city': '北京'}, 'id': 'call_fR3LE8Wjqh9lnDosQ61Y892E', 'type': 'tool_call'}]看到了吗?AI 并没有直接查天气(它也没这能力),而是返回了一个"调用请求":告诉你要调用 get_weather,参数 city 是 "北京"。真正执行函数的是我们自己。
三、一次完整工具调用的底层流程
大模型调用工具,本质上是单次推理(一次请求、一次响应),完整的"让大模型根据工具结果回复"需要四个步骤:
步骤 1:模型绑定工具 — 通过
model.bind_tools([...])绑定一个或多个工具。步骤 2:模型生成工具调用请求 — 用户提问,模型返回一个
AIMessage,里面带着工具名和参数。步骤 3:开发者手动执行工具 — 从响应里取出工具调用信息,自己调用对应函数(比如
工具.invoke())。步骤 4:把结果喂回模型 — 把工具执行结果(
ToolMessage)连同历史一起再发给模型,模型生成最终回复。
特别注意:大模型调用工具是单次推理,需要开发者手动执行工具和管理循环,适合简单、确定的任务。
四、方式一:不用 @tool 装饰器
不写 @tool,直接用一个普通 Python 函数也能当工具。这能帮你理解 LangChain 底层到底做了什么。
4.1 模型绑定工具并发送请求
from rich import print as rprint
# 定义一个普通函数(没有 @tool)
def get_weather(city: str):
return f"{city}天气晴朗"
model_with_tools = model.bind_tools([get_weather])
response = model_with_tools.invoke("今天北京天气如何")
rprint(response)输出会是一个 AIMessage,关键字段是 tool_calls:
tool_calls=[
{
'name': 'get_weather',
'args': {'city': '北京'},
'id': 'call_ECvZNV7RLTWpKQSjhvdGzKBd',
'type': 'tool_call'
}
]4.2 工具描述的底层秘密:convert_to_openai_tool
model.bind_tools([...]) 底层最终会调用 convert_to_openai_tool,把函数"翻译"成模型能看懂的 JSON Schema 描述。我们可以直接调用它来观察:
from langchain_core.utils.function_calling import convert_to_openai_tool
def get_weather(city: str):
return f"{city}天气晴朗"
rprint(convert_to_openai_tool(get_weather))输出:
{
'type': 'function',
'function': {
'name': 'get_weather',
'description': '',
'parameters': {
'properties': {
'city': {'type': 'string'}
},
'required': ['city'],
'type': 'object'
}
}
}关键字段说明:
type:数据类型,常见有string、number、integer、boolean、object、array、null。object就是 JSON 对象。properties:定义 JSON 对象里有哪些"键",以及每个键的值类型和说明。required:当type为object时使用,是一个数组,列出"必须存在的字段名"。
为什么不用
@tool函数也能当工具?看底层源码:加了@tool的函数走BaseTool分支,没加的就走callable分支——后者会根据函数定义和 docstring 生成 Pydantic 模式,再转成规范的 tool_schema。所以普通函数也能被解析。
4.3 description 从哪来?
convert_to_openai_tool 会从函数的 docstring(文档字符串) 里加载描述。上面例子 docstring 为空,所以 description 为 ''。
加上 docstring:
def get_weather(city: str):
"""
天气查询工具
"""
return f"{city}天气晴朗"这次输出里 description 就变成 '天气查询工具' 了。
4.4 参数说明:用 Google 风格 docstring
参数说明也来自 docstring,但必须遵循 Google 风格(Args:、Returns:、Raises: 这些关键字):
def get_weather(city: str):
"""
天气查询工具
Args:
city: 城市名称
"""
return f"{city}天气晴朗"输出中 city 参数就带上了 description: '城市名称'。
AI 依赖 docstring 来理解工具! 描述越清晰,AI 就越能"用对"工具。
❌ 不好:
"""做一些事情"""✅ 好:
"""在产品数据库中搜索产品\n\nArgs:\n query: 搜索关键词,如"笔记本电脑"、"手机"\n\nReturns:\n 产品列表的 JSON 字符串"""
4.5 参数类型说明
参数类型来自函数的类型注解。删除 city: str 的类型注解,工具描述里 city 就变成空 {},不再包含类型信息。
⚠️ 注意:如果 docstring 里写了某参数说明,那这个参数必须有类型注解,否则报错:
ValueError: Arg city in docstring not found in function signature.
4.6 参数默认值说明
参数没有默认值 → 会出现在
required列表里。参数有默认值 → 描述里带
default字段,且不会出现在required列表。
例如 def get_weather(city: str="北京"):city 带 default: '北京',而 required 字段被移除。
五、方式二:使用 @tool 装饰器(推荐)
用 @tool 装饰器,能自动把普通 Python 函数转成智能体可调用的工具。代码量最少、最直接,适合快速验证想法或参数简单的工具。
5.1 自定义工具描述:description
情况 1:只提供 docstring
from langchain_core.utils.function_calling import convert_to_openai_tool
from langchain.tools import tool
@tool
def get_weather(city: str):
"""
天气查询工具
"""
return f"{city}天气晴朗"
print(convert_to_openai_tool(get_weather))⚠️ 用
@tool时必须有 docstring,否则报错:ValueError: Function must have a docstring if description not provided.
情况 2:用 description 参数覆盖
@tool(description=...) 优先级高于 docstring:
@tool(description="根据城市名称查询当日天气的工具")
def get_weather(city: str):
"""天气查询工具"""
return f"{city}天气晴朗"情况 3:parse_docstring 解析 docstring
不加 parse_docstring=True 时,@tool 会把整个 docstring 当成 description。加上后,会把 docstring 里的 Args: 等字段分别解析进对应参数的 description:
@tool(parse_docstring=True)
def get_weather(city: str, units: str = "celsius", include_forecast: bool = False) -> str:
"""
获取当日天气,可选择是否同时查询未来五日天气预报
Args:
city: 城市
units: 气温单位,可选:celsius-摄氏度,fahrenheit-华氏度
include_forecast: 是否包含未来五日的天气预报
"""
...这样每个参数的描述都能精确落到 JSON Schema 里。
⚠️ 区别:不用
@tool时 docstring 不合法会被当普通文本;但用@tool时 docstring 不合法会直接抛异常(ValueError: Found invalid Google-Style docstring.)。
5.2 更改工具名称:name_or_callable
默认用函数名当工具名。可以传参改名:
@tool(name_or_callable="getWeather") # 或简写 @tool("getWeather")
def get_weather(city: str):
"""天气查询工具"""
return f"{city}天气晴朗"说明:不要用
config或runtime作为参数名,这是 LangChain 内部保留的。开发中习惯用函数名作为工具名,不推荐自定义改名。
六、自定义参数 schema
当工具参数变复杂(需要枚举值、范围限制、业务校验)时,可以用 Pydantic 模型或 JSON Schema 精确定义参数。
6.1 用 Pydantic 定义 args_schema
Pydantic 能精确控制参数格式和校验规则,让大模型更准确地理解如何调用工具。
核心三件套:
① BaseModel 基类 —— 声明字段结构、类型、默认值、校验。
from pydantic import BaseModel
class WeatherInput(BaseModel):
city: str
print(WeatherInput(city="北京"))
# city='北京'⚠️ BaseModel 子类初始化时不接收位置参数,必须用关键字参数:
WeatherInput("北京")会报TypeError。
② Field() 定制字段 —— 设置默认值、描述等。
from pydantic import BaseModel, Field
class WeatherInput(BaseModel):
city: str = Field(default="北京", description="城市")
include_forecast: bool = Field(default=False, description="是否包含未来五日天气预报")每个字段的
description至关重要,直接影响大模型理解参数含义的能力。
③ Literal 限定枚举值 —— 字段只能是几个固定字面量之一。
from typing import Literal
class WeatherInput(BaseModel):
city: str
unit: Literal["celsius", "fahrenheit"]传入 unit="kelvin" 会报 ValidationError,提示只能选 celsius 或 fahrenheit。
把 Pydantic 模型关联到工具:
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解析出来的 schema 里,unit 会带上 enum: ['celsius', 'fahrenheit'],这正是 Literal 的功劳。
6.2 用 JSON Schema 定义 args_schema
适合参数结构需要运行时动态生成的场景(比如基于数据库配置或用户输入动态构造)。直接传 JSON Schema 字典:
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,不是整个 function 描述。
七、实战:多工具调用
大模型每次运行(一次推理)可能同时请求调用多个工具,也可能需要多轮。下面看两个实战例子。
案例:股票查询 + 新闻搜索(多工具 + 循环)
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}美元"
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研究项目"]
}
return "\n".join(mock_news.get(company, [f"未找到{company}的相关新闻"]))
# 2. 绑定工具
tools = [get_stock_price, search_news]
model_with_tools = model.bind_tools(tools)
message_list = [HumanMessage(content="苹果公司今天的股价是多少?最近有什么新闻?")]
# 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":
message_list.append(get_stock_price.invoke(tool_call))
if tool_call["name"] == "search_news":
message_list.append(search_news.invoke(tool_call))
for msg in message_list:
msg.pretty_print()模型会依次给出:股票查询结果、新闻搜索结果,最后生成总结性回复。
关键点:这里用一个
while True循环手动管理"调用→执行→再问→...",因为大模型每次只用一次推理决定要不要调工具。
八、强制使用工具:tool_choice
bind_tools 可以传 tool_choice 参数,控制是否强制调用工具。它会作为 tool_choice 字段传给模型(OpenAI 和 DeepSeek 官方 API 对这个字段的取值规定一致)。
8.1 none:禁止调用
model_with_tools = model.bind_tools([get_weather], tool_choice="none")即使你问"今天北京天气如何",模型也不会调用工具,而是直接说"我无法获取实时天气数据,建议你去查天气 App"。
8.2 auto:模型自己决定
model_with_tools = model.bind_tools([get_weather], tool_choice="auto")问天气 → 模型调用
get_weather。说"你好啊" → 模型直接回答,不调用工具。
8.3 required:强制调用
model_with_tools = model.bind_tools([get_weather], tool_choice="required")即使你说"你好啊",模型依然会调用工具(这个例子它会强行去调 get_weather,默认用一个城市)。
8.4 强制调用指定的某个工具
tool_choice 还能传具体工具名,强制只用那个工具:
model_with_tools = model.bind_tools([get_weather1, get_weather2], tool_choice="get_weather2")这样即使有多个工具,模型也只会调用 get_weather2。
九、实践经验总结(避坑指南)
1. 描述要清晰
@tool(parse_docstring=True)
def search_flights(origin: str, destination: str, date: str) -> str:
"""
搜索航班信息
Args:
origin: 出发城市,如"北京"
destination: 目的地城市,如"上海"
date: 出发日期,格式 YYYY-MM-DD
Returns:
可用航班的 JSON 列表
"""2. 功能要单一
一个工具只做一件事:
# ❌ 不好:一个工具做太多事
@tool
def do_everything(action: str, data: str) -> str: ...
# ✅ 好:每个工具做一件事
@tool
def get_weather(city: str) -> str: ...
@tool
def calculator(operation: str, a: float, b: float) -> str: ...3. 工具失败的三层防护
第 1 层:工具内部处理(在工具函数里 try/except,返回错误字符串而不是抛异常)。
@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 级重试(用 prompt 引导)。
agent = create_agent(
model=model,
tools=[...],
prompt="如果工具失败,尝试使用其他方法解决问题。"
)第 3 层:调用级重试(用 tenacity 的 @retry 做容错保险)。
from tenacity import retry, stop_after_attempt
@retry(stop=stop_after_attempt(3)) # 失败最多尝试 3 次
def call_agent(question):
return agent.invoke({"messages": [{"role": "user", "content": question}]})4. 工具务必返回字符串(str)
import json
# ✅ 好:返回字符串
@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": "张三"}为什么?
大模型本质只吃"文本"。
避免乱码:如果直接返回含中文的字典,LangChain 强制转字符串时可能用 Unicode 编码变成
{"name": "\u5f20\u4e09"},干扰模型判断。手动json.dumps(..., ensure_ascii=False)能让中文正常显示,喂给模型的是最干净的纯文本。
小知识:
ensure_ascii=False让中文、表情等字符在 JSON 字符串里正常显示,而不是一堆\uXXXX转义。
5. 同步 vs 异步
同步工具:简单场景、CPU 密集型任务。
异步工具:IO 密集型(API 调用、数据库、文件操作)。
# 同步
@tool
def sync_tool(x: str) -> str:
return process(x)
# 异步
@tool
async def async_tool(x: str) -> str:
return await async_process(x)十、小结
到这里,你已经掌握了 LangChain 工具调用的完整知识体系:
下一步建议: 学完工具后,就可以进入智能体(Agent)的世界了
评论区