在 LangChain 的 Model I/O 架构中,工具(Tools)是连接大语言模型与外部物理/数字世界的关键桥梁。模型本身只能生成文本,而工具赋予了模型“行动”的能力。本章将深入剖析工具的定义范式、底层原理、调用闭环以及生产环境中的容错与工程规范。
一、 核心概念与架构定位
1.1 为什么需要工具机制?
大语言模型存在两个根本性局限:
- 知识时效性:训练数据有截止日期,无法获取实时信息(如天气、股价)。
- 行动能力缺失:只能生成文本,无法直接执行操作(如查询数据库、调用外部 API)。
工具机制通过让模型“决定调用哪个工具、传入什么参数”,由程序“执行工具并返回结果”,实现了决策与执行的分离。
1.2 工具调用的完整生命周期
二、 工具定义的“双轨制”
在 LangChain 中,定义工具存在两条路径:使用 @tool 装饰器 与 使用普通 Python 函数。理解这两条路径的差异与底层原理,是看透 LangChain 工具机制的关键。
2.1 方式一:@tool 装饰器(工业级标准)
@tool 是 LangChain 提供的装饰器,用于将普通 Python 函数转化为具备完整元数据的 BaseTool 对象。这是生产环境的首选方案。
2.1.1 基础用法与 parse_docstring
默认情况下,@tool 只提取 Docstring 的第一行作为工具描述。要让模型精确理解每个参数的含义,必须启用 parse_docstring=True。
from langchain_core.tools import tool
# 推荐:启用 parse_docstring,严格遵循 Google Style 规范
@tool(parse_docstring=True)
def get_stock_price(company: str, timeframe: str = "today") -> str:
"""获取指定公司的股票价格信息
Args:
company: 公司名称(如:苹果公司, 微软公司)
timeframe: 时间范围(today-今日, week-本周)
Returns:
股票价格的文本描述
"""
return f"{company} {timeframe} 股价: 150.00"
2.1.2 自定义参数 Schema (args_schema)
当参数需要复杂的校验(如枚举值、范围限制)时,可通过 Pydantic 模型或 JSON Schema 定义 args_schema。
使用 Pydantic 模型(推荐):
from pydantic import BaseModel, Field
from typing import Literal
class WeatherInput(BaseModel):
city: str = Field(default="北京", description="城市名称")
unit: Literal["celsius", "fahrenheit"] = Field(default="celsius", description="气温单位")
@tool(args_schema=WeatherInput)
def get_weather(city: str, unit: str = "celsius") -> str:
"""获取当日天气"""
return f"{city} 气温: 22 {unit}"
2.2 方式二:普通 Python 函数(底层原理与避坑)
核心结论:不需要 @tool 装饰,普通的 Python 函数只要具备“类型注解”和“Docstring”,同样能被 LangChain 识别为工具。
2.2.1 底层原理:convert_to_openai_tool
当我们将普通函数传递给 model.bind_tools([func]) 时,LangChain 底层会调用 convert_to_openai_tool 函数。其源码逻辑如下:
# 源码简化版
if isinstance(function, langchain_core.tools.base.BaseTool):
# 分支 A:如果是被 @tool 装饰过的 BaseTool 对象
oai_function = _format_tool_to_openai_function(function)
elif callable(function):
# 分支 B:如果是普通的可调用函数 (callable)
oai_function = _convert_python_function_to_openai_function(function)
LangChain 会通过反射机制(Inspect)强行读取普通函数的签名和 Docstring,自动生成 JSON Schema。
2.2.2 普通函数的代码范式与执行差异
from langchain_core.utils.function_calling import convert_to_openai_tool
# 1. 定义普通函数(无 @tool 装饰)
def get_weather(city: str):
"""天气查询工具\nArgs:\n city: 城市名称"""
return f"{city}天气晴朗"
# 2. 查看底层自动生成的 Tool Schema
print(convert_to_openai_tool(get_weather))
# 3. 绑定到模型
model_with_tools = model.bind_tools([get_weather])
response = model_with_tools.invoke([HumanMessage("今天北京天气如何?")])
# 4. 手动执行工具(关键区别!)
for tool_call in response.tool_calls:
if tool_call["name"] == "get_weather":
# 普通函数没有 .invoke() 方法,必须使用 Python 原生的 **kwargs 解包调用
tool_result = get_weather(**tool_call["args"])
# 手动构造 ToolMessage
tool_message = ToolMessage(
content=tool_result,
tool_call_id=tool_call["id"],
name=tool_call["name"]
)
2.2.3 核心避坑:Docstring 的“双标”行为
当 Docstring 格式不合法(如未使用标准的 Args: 关键字)时,两种方式的表現截然不同:
| 对比维度 | 使用 @tool(parse_docstring=True) | 不使用 @tool (普通函数) |
|---|---|---|
| 校验机制 | 严格阻断。发现格式不合法,立即抛出 ValueError。 | 静默失败。不会报错,而是将整段非法文本当作 description 塞给模型。 |
| 工程影响 | 在开发阶段提前暴露缺陷,防止错误流入生产环境。 | 模型可能无法准确理解参数含义,导致调用时传错参数,且极难排查。 |
2.3 双轨制选型指南
| 对比维度 | @tool 装饰器 (推荐) | 普通函数 |
|---|---|---|
| 对象类型 | 转化为 LangChain 的 BaseTool 对象。 | 保持为 Python 原生的 function 对象。 |
| 执行方式 | 支持 .invoke(args),自动处理参数校验与 ToolMessage 封装。 | 只能使用原生调用 func(**args),需手动构造 ToolMessage。 |
| 适用场景 | 生产环境、Agent 构建、需要参数强校验的复杂场景。 | 极简的临时测试、不想引入额外依赖的轻量级脚本。 |
三、 工具绑定与调用闭环
3.1 绑定工具 (bind_tools)
定义好工具后,必须通过 bind_tools() 方法将工具列表绑定到模型实例。绑定后,模型在推理时会将工具的 Schema 作为上下文一并发送。
# bind_tools 返回一个新的模型实例,原模型不受影响
model_with_tools = model.bind_tools([get_weather, get_news])
3.2 完整的工具调用闭环实现
这是工具机制中最核心的工程代码。它包含:解析 tool_calls -> 执行工具 -> 封装 ToolMessage -> 追加到历史 -> 再次调用模型。
生产级闭环代码:
from langchain_core.messages import HumanMessage, AIMessage, ToolMessage
# 1. 用户输入
messages = [HumanMessage("今天杭州天气如何?今天新闻是什么?")]
# 2. 模型推理(决定调用哪些工具)
response = model_with_tools.invoke(messages)
messages.append(response) # 保存 AIMessage(包含 tool_calls)
# 3. 构建工具映射表(名称 -> 函数)
tool_map = {
"get_weather": get_weather,
"get_news": get_news
}
# 4. 执行每个工具调用,封装 ToolMessage
if response.tool_calls:
for tool_call in response.tool_calls:
tool_name = tool_call["name"]
tool_args = tool_call["args"]
tool_call_id = tool_call["id"]
# 执行工具
selected_tool = tool_map[tool_name]
# 注意:如果是 @tool 装饰的对象,使用 .invoke(tool_call)
# 如果是普通函数,使用 func(**tool_args)
tool_result = selected_tool.invoke(tool_call)
# 封装为 ToolMessage
# 【核心铁律】tool_call_id 必须与 AIMessage 中的 id 严格一致!
tool_message = ToolMessage(
content=tool_result.content if hasattr(tool_result, 'content') else str(tool_result),
name=tool_name,
tool_call_id=tool_call_id
)
messages.append(tool_message)
# 5. 将 ToolMessage 传回模型,生成最终的自然语言回复
final_response = model_with_tools.invoke(messages)
print(final_response.content)
四、 进阶控制:tool_choice 参数
在 bind_tools 中,可以通过 tool_choice 参数强制干预模型的工具调用行为。
tool_choice 值 | 行为描述 | 适用场景 |
|---|---|---|
"auto" | 默认值。模型自主决定是否调用工具,以及调用哪个。 | 绝大多数常规场景。 |
"none" | 模型绝对不会调用任何工具,即使提示词中要求调用。 | 强制模型进行纯文本推理,禁用工具。 |
"required" | 模型必须调用至少一个工具(数量不限)。 | 强制触发工具执行流程。 |
"any" | 等价于 "required"。 | 同上。 |
"工具名称" | 模型必须且只能调用指定的这一个工具。 | 路由控制,强制使用特定工具。 |
代码示例:
# 强制模型必须调用 get_weather 工具,即使它认为不需要
model_with_tools = model.bind_tools([get_weather], tool_choice="get_weather")
五、 工具设计规范与最佳实践
5.1 单一职责原则
每个工具应该只做一件事。将多个功能塞进一个工具,会导致参数复杂、模型调用准确率下降。
# 正确:职责单一,参数清晰
@tool(parse_docstring=True)
def search_flights(origin: str, destination: str, date: str) -> str: ...
# 错误:一个工具做太多事
@tool
def do_everything(action: str, data: str) -> str: ...
5.2 返回值规范:必须是字符串且处理中文
工具的返回值应始终为 str 类型。如果返回的是字典,必须手动序列化为 JSON 字符串,并且必须设置 ensure_ascii=False。
底层原理:
json.dumps() 默认 ensure_ascii=True,会将中文转换为 \uXXXX 转义序列。模型看到 \u5f20\u4e09 和直接看到 张三,其理解能力和输出准确率存在显著差距。
import json
@tool
def get_user_info(user_id: str) -> str:
"""获取用户信息"""
user = {"id": user_id, "name": "张三", "age": 28}
# 必须设置 ensure_ascii=False,确保模型直接看到干净的中文
return json.dumps(user, ensure_ascii=False)
5.3 同步工具与异步工具
| 类型 | 适用场景 | 代码定义 |
|---|---|---|
| 同步工具 | CPU 密集型任务、简单的本地计算。 | def sync_tool(...) |
| 异步工具 | IO 密集型任务(API 调用、数据库查询)。 | async def async_tool(...) |
注:在 Agent 链路中,如果绑定了异步工具,Agent 的执行器也必须使用异步模式(ainvoke)。
六、 生产环境容错机制
在大模型应用中,网络请求和外部工具调用是最脆弱的环节。必须建立多层次的容错体系。
6.1 三层防护架构
6.2 第1层:工具内部异常处理
工具函数内部必须使用 try-except 捕获所有可能的异常,并返回一个错误描述字符串,而不是让异常向上传播导致整个链路崩溃。
@tool(parse_docstring=True)
def get_stock_price(company: str) -> str:
"""获取指定公司的股票价格"""
try:
# 模拟外部 API 调用
data = {"company": company, "price": 150.00}
return json.dumps(data, ensure_ascii=False)
except ConnectionError:
return json.dumps({"error": "网络连接失败,请稍后重试"}, ensure_ascii=False)
except Exception as e:
return json.dumps({"error": f"查询失败: {str(e)}"}, ensure_ascii=False)
6.3 第3层:使用 tenacity 实现调用级重试
tenacity 是 Python 中最成熟的重试库。通过 @retry 装饰器,可以在工具调用失败时自动重试。
from tenacity import retry, stop_after_attempt, wait_exponential
@retry(
stop=stop_after_attempt(5), # 最多尝试 5 次
wait=wait_exponential(multiplier=1, min=2, max=30), # 指数退避: 2s, 4s, 8s...
reraise=True # 所有重试都失败后,重新抛出异常
)
def call_external_api(url: str) -> dict:
"""调用外部 API,自动处理网络抖动"""
response = requests.get(url, timeout=10)
response.raise_for_status()
return response.json()
七、 总结与工程决策清单
7.1 工具设计检查清单
| 序号 | 检查项 | 原因 |
|---|---|---|
| 1 | 优先使用 @tool 装饰器定义工具 | 提供强类型校验、自动封装 ToolMessage,代码更优雅。 |
| 2 | 开启 parse_docstring=True | 将参数描述注入 Schema,大幅提升模型调用准确率。 |
| 3 | 返回值始终为 str | 避免序列化异常,确保跨平台兼容。 |
| 4 | json.dumps 必须设置 ensure_ascii=False | 避免中文被转义为 Unicode,确保模型理解准确。 |
| 5 | 工具内部使用 try-except 返回错误字符串 | 防止异常向上传播导致 Agent 链路崩溃。 |
| 6 | 闭环引擎中严格校验 tool_call_id | 错一个字符都会导致底层 API 抛出 400 Bad Request。 |
| 7 | 使用 tenacity 配置指数退避重试 | 优雅应对网络抖动和 API 限流。 |
7.2 常见反模式(Anti-patterns)
- 参数过于泛化:如
def do_action(action: str, data: str),模型无法准确生成参数。 - 工具描述模糊:如
"""这是一个工具""",模型不知道何时该调用。 - 忽略 Docstring 规范:不使用
parse_docstring,导致模型对参数含义产生幻觉。 - 无迭代上限:在
while True循环中不设置max_iterations,模型可能在两个工具之间反复调用,消耗大量 Token 甚至死循环。
详细介绍&spm=1001.2101.3001.5002&articleId=162765274&d=1&t=3&u=c0e5be5c4c7446c68efcaafca81a11ee)
2278

被折叠的 条评论
为什么被折叠?



