【LangChain】Tools工具系统深度解析:给Agent一双灵巧的手
想象一下,你请了一位超级聪明的实习生来到公司。这位实习生博览群书、思维敏捷,但有一个致命缺陷——他无法触碰现实世界。他不能查天气、不能搜资料、不能订外卖,甚至不能帮你按个电梯按钮。
这就是没有Tools的大语言模型(LLM)的处境。再聪明的脑子,如果没有"手"去执行,也只能纸上谈兵。今天,我们就来聊聊LangChain的Tools系统——这套让AI从"思想家"变成"实干家"的神奇装备库。
一、核心概念:什么是Tool?(What & Why)
1.1 从"只会说"到"能干"的进化
在LangChain的世界里,Tool是Agent与现实世界交互的桥梁。一个Tool本质上是一个Python函数,但被赋予了特殊的元数据,让LLM能够理解:
- 这是什么工具?(名称和描述)
- 它能做什么?(功能说明)
- 怎么用?(参数 schema)

LangChain新架构(v1.0+)提供了两种创建Tool的方式:
| 方式 | 适用场景 | 复杂度 | 灵活性 |
|---|---|---|---|
@tool 装饰器 | 快速原型、简单功能 | ⭐ 低 | ⭐⭐ 中 |
BaseTool 子类 | 生产环境、复杂逻辑 | ⭐⭐⭐ 高 | ⭐⭐⭐⭐⭐ 极高 |
生活类比:@tool就像快餐店的自助点餐机——快速、简单、标准化;BaseTool则是米其林餐厅的后厨——你可以自定义每一道工序,但需要更多准备工作。
二、快速上手:10行代码创建你的第一个Tool
让我们从一个最经典的例子开始——天气查询工具。
from langchain.tools import tool
from typing import Optional
import requests
@tool
def get_weather(city: str, country_code: Optional[str] = "CN") -> str:
"""
获取指定城市的当前天气信息。
当用户询问天气、温度、降雨情况时使用此工具。
Args:
city: 城市名称,例如"北京"、"上海"
country_code: 两位国家代码,默认为"CN"(中国)
Returns:
格式化的天气信息字符串,包含温度和天气状况
"""
# 这里调用真实的天气API
api_key = "your_api_key"
url = "http://api.openweathermap.org/data/2.5/weather"
try:
response = requests.get(
url,
params={
"q": f"{city},{country_code}",
"appid": api_key,
"units": "metric",
"lang": "zh_cn"
},
timeout=10
)
data = response.json()
temp = data["main"]["temp"]
desc = data["weather"][0]["description"]
return f"🌤️ {city}当前天气:{desc},温度{temp}°C"
except requests.exceptions.RequestException as e:
return f"❌ 获取天气失败:{str(e)}"
# 使用方式
print(get_weather.name) # 输出: get_weather
print(get_weather.description) # 输出: 获取指定城市的当前天气信息...
print(get_weather.args_schema) # 输出: Pydantic模型定义的参数结构
代码解读:
-
@tool装饰器:魔法发生的地方。它会自动:- 从函数名生成Tool名称(
get_weather) - 从docstring提取描述(前三行)
- 从类型注解生成JSON Schema参数定义
- 从函数名生成Tool名称(
-
类型注解:
city: str和country_code: Optional[str]不仅是Python的类型提示,更是LLM理解"我需要传什么参数"的关键线索。 -
Args文档:这是最重要的优化点!LLM通过阅读Args部分来决定如何填充参数。描述越清晰,Tool被正确调用的概率越高。
三、深度解析:让Tool变得专业
3.1 工具描述优化:从"能用"到"好用"
很多开发者抱怨:"我的Agent总是乱调工具!" 90%的原因都是描述写得太烂。
❌ 反面教材:
@tool
def search(q: str):
"""搜索东西"""
...
✅ 正面示范:
@tool
def search(
query: str,
result_count: Optional[int] = 5,
time_range: Optional[str] = "month"
) -> str:
"""
在知识库中搜索相关信息,适用于用户询问具体事实、数据或文档内容时。
⚠️ 注意:不要用于查询实时信息(如天气、股价),也不要用于简单计算。
Args:
query: 搜索关键词,应提取用户问题中的核心实体和概念,去除语气词
result_count: 返回结果数量(1-20),默认5条。问题越复杂,需要的结果越多
time_range: 时间范围过滤 - "day"(24h内), "week"(7天内), "month"(30天内), "year"(年内), "all"(全部)
Returns:
JSON格式的搜索结果列表,包含标题、摘要、相关度和发布时间
"""
...
优化技巧清单:
| 技巧 | 说明 | 示例 |
|---|---|---|
| 触发条件 | 明确告诉LLM什么时候该用 | "当用户询问天气时使用" |
| 负面示例 | 告诉LLM什么情况下不该用 | "不要用于简单计算" |
| 参数示例 | 给LLM提供具体的输入样例 | "例如'北京'、'上海'" |
| 边界说明 | 说明参数的取值范围 | "1-20之间" |
| 返回格式 | 描述输出长什么样 | "JSON格式,包含..." |

3.2 类型注解的艺术:让LLM"看"得懂
LangChain使用Pydantic自动从类型注解生成JSON Schema。这意味着你的类型注解直接影响LLM的理解。
from typing import Literal, Annotated
from pydantic import Field
@tool
def analyze_data(
data_source: Literal["database", "api", "file"] = Field(
description="数据来源类型,决定如何连接和读取数据"
),
query: Annotated[str, Field(min_length=5, max_length=1000)] = Field(
description="分析查询语句,应包含明确的分析目标"
),
output_format: Literal["summary", "chart", "table", "raw"] = Field(
description="期望的输出格式:summary(文字摘要), chart(图表数据), table(表格), raw(原始数据)"
),
priority: Annotated[int, Field(ge=1, le=5)] = Field(
default=3,
description="任务优先级:1(最高)到5(最低),影响资源分配"
)
):
"""执行数据分析任务,从指定来源提取数据并按需处理。"""
...
进阶技巧:使用Annotated和Field可以添加验证规则和更丰富的描述,让LLM不仅知道"要什么",还知道"要怎么给"。
四、错误处理:让Agent学会" resilience"
4.1 为什么需要专门的错误处理?
想象一下:你的Agent调用了一个支付接口,结果网络超时了。如果没有恰当的错误处理,Agent可能会:
- 直接崩溃——用户体验极差
- 返回一堆Python traceback——用户完全看不懂
- 陷入死循环——不断重试同一个失败的操作
LangChain新架构提供了@wrap_tool_call装饰器,让你可以优雅地拦截和处理工具错误。
4.2 基础错误处理模式
from langchain.agents import create_agent
from langchain.agents.middleware import wrap_tool_call
from langchain.messages import ToolMessage
import time
@wrap_tool_call
def resilient_tool_handler(request, handler):
"""
带重试和降级策略的工具错误处理器。
就像一位经验丰富的外卖员:遇到堵车(错误)不会直接放弃,
而是尝试换条路(重试),实在不行就联系用户说明情况(友好错误消息)。
"""
tool_name = request.tool_call["name"]
max_retries = 3
base_delay = 1.0 # 秒
for attempt in range(max_retries):
try:
print(f"🔧 尝试执行 {tool_name},第 {attempt + 1} 次...")
result = handler(request)
print(f"✅ {tool_name} 执行成功")
return result
except Exception as e:
delay = base_delay * (2 ** attempt) # 指数退避:1s, 2s, 4s
# 判断错误类型,决定是否重试
error_msg = str(e).lower()
is_retriable = any([
"timeout" in error_msg,
"connection" in error_msg,
"rate limit" in error_msg,
"503" in error_msg,
"502" in error_msg
])
if not is_retriable or attempt == max_retries - 1:
# 不可重试的错误或重试耗尽,返回友好错误消息
friendly_message = _generate_friendly_error(tool_name, e)
return ToolMessage(
content=friendly_message,
tool_call_id=request.tool_call["id"],
name=tool_name,
status="error" # 标记为错误,LLM会识别这一点
)
print(f"⚠️ 第 {attempt + 1} 次失败: {str(e)[:50]}... {delay}秒后重试")
time.sleep(delay)
# 理论上不会执行到这里,但为了类型安全
return ToolMessage(
content=f"工具 {tool_name} 在 {max_retries} 次尝试后仍然失败",
tool_call_id=request.tool_call["id"],
name=tool_name,
status="error"
)
def _generate_friendly_error(tool_name: str, error: Exception) -> str:
"""将技术错误转换为用户友好的消息。"""
error_type = type(error).__name__
error_msg = str(error)
# 根据工具类型和错误类型生成不同的提示
error_templates = {
"ConnectionError": f"我尝试使用 {tool_name},但网络连接似乎不稳定。让我尝试其他方法或请您稍后再试。",
"TimeoutError": f"{tool_name} 响应时间过长,可能是服务繁忙。我已经记录了这个问题。",
"ValueError": f"在使用 {tool_name} 时,我发现输入参数可能有问题:{error_msg[:100]}。让我重新理解您的需求。",
"PermissionError": f"我目前没有权限执行 {tool_name} 相关的操作。请联系管理员获取权限。",
}
return error_templates.get(
error_type,
f"执行 {tool_name} 时遇到意外问题 ({error_type})。错误详情:{error_msg[:150]}"
)
# 创建Agent时使用中间件
agent = create_agent(
model="openai:gpt-4o",
tools=[get_weather, search, calculate],
middleware=[resilient_tool_handler]
)
关键点解析:
-
ToolMessage的特殊作用:与普通消息不同,ToolMessage带有tool_call_id和status="error",这让LLM明白"这是工具返回的错误,不是我生成的内容",从而可以据此调整策略。 -
指数退避策略:第一次等1秒,第二次等2秒,第三次等4秒。这避免了对已经过载的服务"疯狂敲门"。
-
错误分类:区分"可以重试"(网络超时)和"不应该重试"(参数错误)的情况。
4.3 高级模式:错误恢复与智能降级
有时候,一个工具失败了,但Agent可以用其他方式完成任务:
from langgraph.types import Command
from langgraph.constants import END
@wrap_tool_call
def smart_fallback_handler(request, handler):
"""
智能降级处理器。
就像手机没信号时自动切换到WiFi通话,
当主工具失败时,尝试替代方案或优雅结束。
"""
tool_name = request.tool_call["name"]
# 定义工具之间的替代关系
fallback_chain = {
"premium_search": "basic_search", # 高级搜索失败用基础搜索
"realtime_stock": "cached_stock", # 实时股价失败用缓存数据
"smart_calculator": "simple_calculator"
}
try:
return handler(request)
except Exception as e:
# 检查是否有替代工具
fallback_tool = fallback_chain.get(tool_name)
if fallback_tool and fallback_tool in available_tools:
print(f"🔄 {tool_name} 失败,降级到 {fallback_tool}")
# 修改请求,调用替代工具
modified_request = request.copy()
modified_request.tool_call["name"] = fallback_tool
# 递归调用处理器(注意:实际实现需要防止无限递归)
return handler(modified_request)
# 如果是关键工具且没有替代方案,可能需要中断Agent执行
if tool_name in ["payment_gateway", "security_check"]:
return ToolMessage(
content=f"🚨 关键工具 {tool_name} 失败,无法安全继续执行任务。已通知管理员。",
tool_call_id=request.tool_call["id"],
name=tool_name,
status="error",
is_critical=True # 自定义标记
)
# 普通工具失败,返回标准错误
return ToolMessage(
content=f"工具 {tool_name} 暂时不可用。让我尝试其他方法完成您的请求。",
tool_call_id=request.tool_call["id"],
name=tool_name,
status="error"
)
五、动态工具集管理:按需装备
5.1 为什么需要动态工具?
想象一个场景:你的Agent有50个工具,从查天气到写代码应有尽有。但每个用户请求其实只需要其中2-3个。如果一次性把所有工具描述都塞进Prompt,会导致:
- 上下文窗口爆炸(Token费用飙升)
- LLM选择困难(错误调用率上升)
- 响应延迟增加
解决方案:动态工具选择——根据对话上下文、用户权限、当前任务阶段,动态决定暴露哪些工具。

5.2 实现动态工具集
from typing import List, Dict, Callable
from langchain.tools import tool
from langchain.agents import create_agent
# 定义工具分类和元数据
TOOL_REGISTRY: Dict[str, Dict] = {
"weather_tools": {
"tools": [get_weather, get_forecast, get_air_quality],
"required_roles": ["basic_user", "premium_user", "admin"],
"description": "天气相关工具",
"triggers": ["天气", "温度", "下雨", "空气质量", "forecast", "weather"]
},
"finance_tools": {
"tools": [get_stock_price, get_exchange_rate, calculate_portfolio],
"required_roles": ["premium_user", "finance_team", "admin"],
"description": "金融数据工具(需要高级权限)",
"triggers": ["股价", "汇率", "股票", "投资", "portfolio", "stock"]
},
"dev_tools": {
"tools": [run_code, search_docs, debug_helper],
"required_roles": ["developer", "admin"],
"description": "开发辅助工具",
"triggers": ["代码", "bug", "调试", "API", "code", "debug", "error"]
},
"admin_tools": {
"tools": [manage_users, system_config, view_logs],
"required_roles": ["admin"],
"description": "系统管理工具(仅管理员)",
"triggers": ["用户管理", "系统设置", "日志", "admin", "config"]
}
}
class DynamicToolManager:
"""动态工具管理器 - 像一位智能管家,根据客人(用户)的需求和身份,决定展示哪些餐具(工具)。"""
def __init__(self, registry: Dict):
self.registry = registry
self.tool_cache = {} # 缓存已解析的工具
def select_tools(
self,
user_query: str,
user_role: str = "basic_user",
conversation_context: List[dict] = None
) -> List[Callable]:
"""
根据用户查询和权限动态选择工具。
策略:
1. 关键词匹配:用户问题中包含哪些工具触发词
2. 权限过滤:用户角色是否有权使用这些工具
3. 上下文增强:根据对话历史补充相关工具
"""
query_lower = user_query.lower()
selected_categories = set()
# 1. 关键词匹配
for category, meta in self.registry.items():
triggers = meta["triggers"]
if any(trigger in query_lower for trigger in triggers):
selected_categories.add(category)
# 2. 权限过滤
allowed_tools = []
for category in selected_categories:
meta = self.registry[category]
if user_role in meta["required_roles"]:
allowed_tools.extend(meta["tools"])
print(f"✅ 加载工具组: {meta['description']}")
else:
print(f"❌ 权限不足,跳过: {meta['description']}")
# 3. 上下文增强(可选)
if conversation_context:
# 如果上一回合调用了某个工具,保留相关工具
last_tools = self._extract_last_tools(conversation_context)
for tool_name in last_tools:
related = self._find_related_tools(tool_name)
allowed_tools.extend([t for t in related if t not in allowed_tools])
# 去重并保持顺序
seen = set()
unique_tools = []
for tool in allowed_tools:
if tool.name not in seen:
unique_tools.append(tool)
seen.add(tool.name)
print(f"🎯 最终加载 {len(unique_tools)} 个工具: {[t.name for t in unique_tools]}")
return unique_tools
def _extract_last_tools(self, context: List[dict]) -> List[str]:
"""从对话历史中提取上次使用的工具名"""
# 简化实现
return []
def _find_related_tools(self, tool_name: str) -> List[Callable]:
"""找到与指定工具相关的其他工具"""
# 例如:如果用了get_weather,可能也需要get_forecast
related_map = {
"get_weather": [get_forecast],
"get_stock_price": [get_exchange_rate]
}
return related_map.get(tool_name, [])
# 使用示例
tool_manager = DynamicToolManager(TOOL_REGISTRY)
def create_dynamic_agent(user_query: str, user_role: str = "basic_user"):
"""为每个请求创建定制化的Agent"""
# 动态选择工具
tools = tool_manager.select_tools(
user_query=user_query,
user_role=user_role
)
# 创建专门针对当前任务的Agent
agent = create_agent(
model="openai:gpt-4o",
tools=tools,
# 可以添加其他中间件
)
return agent
# 实际使用
user_question = "帮我查一下北京明天的天气,适合穿什么衣服?"
agent = create_dynamic_agent(user_question, user_role="basic_user")
result = agent.invoke({"messages": [{"role": "user", "content": user_question}]})
动态工具的优势:
| 场景 | 静态工具集 | 动态工具集 |
|---|---|---|
| 简单天气查询 | 加载50个工具(浪费) | 加载3个天气工具(精准) |
| 权限控制 | 代码层面判断(复杂) | 工具层过滤(优雅) |
| Token消耗 | 高(大量无关描述) | 低(仅相关描述) |
| 准确率 | 低(选择多易错) | 高(选择少精准) |
六、实战案例:智能客服Agent的工具系统
让我们构建一个完整的例子:一个电商客服Agent,需要根据用户问题动态选择工具,并优雅处理各种错误。
from langchain.tools import tool
from langchain.agents import create_agent
from langchain.agents.middleware import wrap_tool_call
from langchain.messages import ToolMessage
from typing import Optional, Literal
import random # 仅用于模拟
# ============ 1. 定义工具 ============
@tool
def query_order_status(
order_id: Annotated[str, Field(pattern=r"^ORD\d{8}$")] = Field(
description="订单号,格式为ORD+8位数字,例如ORD20240315"
),
phone_last4: Annotated[str, Field(pattern=r"^\d{4}$")] = Field(
description="手机号后4位,用于身份验证"
)
) -> str:
"""
查询订单状态和物流信息。
当用户询问"我的订单到哪了"、"查看订单状态"时使用。
⚠️ 必须提供订单号和手机尾号,缺一不可。
"""
# 模拟API调用
if random.random() < 0.2: # 20%概率失败
raise ConnectionError("订单服务暂时不可用")
return f"订单 {order_id} 状态:已发货,预计明天送达。物流:顺丰速运,单号SF1234567890"
@tool
def process_return(
order_id: str,
reason: Literal["质量问题", "尺寸不符", "不喜欢", "发错货", "其他"],
item_condition: Literal["全新未拆", "已拆未用", "轻微使用", "明显使用"] = "全新未拆"
) -> str:
"""
发起退货申请。
当用户说"我要退货"、"这个商品有问题"时使用。
注意:已拆封商品可能需要扣除部分款项。
"""
if item_condition in ["轻微使用", "明显使用"]:
return f"⚠️ 由于商品{item_condition},退货需扣除20%折旧费。确认请回复'确认扣除'。"
return f"✅ 退货申请已提交,订单{order_id},原因:{reason}。快递员将在24小时内上门取件。"
@tool
def check_inventory(
product_name: str,
size: Optional[str] = None,
color: Optional[str] = None
) -> str:
"""
查询商品库存。
当用户问"有没有货"、"还有库存吗"时使用。
"""
# 模拟库存查询
return f"📦 {product_name}({color or '默认色'} {size or '均码'})当前库存:充足(153件)"
@tool
def escalate_to_human(
reason: str,
priority: Literal["low", "medium", "high", "urgent"] = "medium"
) -> str:
"""
转接人工客服。
当用户明确要求人工、或问题超出AI能力范围时使用。
高优先级情况:投诉、账户安全、复杂售后纠纷。
"""
wait_time = {"low": "5分钟", "medium": "2分钟", "high": "30秒", "urgent": "立即"}[priority]
return f"👨💼 已为您转接人工客服,预计等待{wait_time}。原因:{reason}"
# ============ 2. 错误处理中间件 ============
@wrap_tool_call
def customer_service_error_handler(request, handler):
"""
客服场景专用的错误处理器。
原则:
1. 永远不要让用户看到Python错误堆栈
2. 提供明确的下一步行动建议
3. 关键错误(如支付)立即转人工
"""
tool_name = request.tool_call["name"]
try:
return handler(request)
except ConnectionError as e:
# 服务连接问题 - 建议重试或转人工
if tool_name in ["query_order_status", "process_return"]:
return ToolMessage(
content=(
f"抱歉,{tool_name}服务暂时繁忙(就像高峰期餐厅排队一样)。\n"
f"💡 建议:1. 请稍等30秒后重试 2. 或直接说'转人工'优先处理"
),
tool_call_id=request.tool_call["id"],
name=tool_name,
status="error"
)
raise # 其他工具继续抛出
except ValueError as e:
# 参数错误 - 引导用户修正
return ToolMessage(
content=(
f"我注意到您提供的信息格式可能需要调整:{str(e)}\n"
f"💡 请检查:订单号是否为ORD开头+8位数字?手机号后4位是否正确?"
),
tool_call_id=request.tool_call["id"],
name=tool_name,
status="error"
)
except Exception as e:
# 未知错误 - 记录并转人工
print(f"🔥 严重错误 [{tool_name}]: {str(e)}") # 后端记录
return ToolMessage(
content=(
f"抱歉,处理您的请求时遇到了意外问题。为了确保服务质量,"
f"建议转接人工客服为您专门处理。"
),
tool_call_id=request.tool_call["id"],
name=tool_name,
status="error"
)
# ============ 3. 创建Agent ============
# 动态工具选择(简化版)
def get_cs_tools(user_query: str):
"""根据查询选择客服工具"""
query = user_query.lower()
tools = [escalate_to_human] # 总是保留转人工选项
if any(kw in query for kw in ["订单", "物流", "到哪", "快递"]):
tools.append(query_order_status)
if any(kw in query for kw in ["退货", "退款", "退", "换货"]):
tools.append(process_return)
if any(kw in query for kw in ["库存", "有货", "还有吗", "缺货"]):
tools.append(check_inventory)
return tools
# 创建Agent
customer_query = "我的订单ORD20240301到哪了?手机尾号1234"
tools = get_cs_tools(customer_query)
agent = create_agent(
model="openai:gpt-4o",
tools=tools,
middleware=[customer_service_error_handler],
# 可以添加其他配置如checkpointer实现多轮对话
)
# 执行
result = agent.invoke({
"messages": [{"role": "user", "content": customer_query}]
})
print(result["messages"][-1].content)
七、最佳实践:Dos & Don'ts
✅ 推荐做法
| 类别 | 实践 | 原因 |
|---|---|---|
| 描述编写 | 使用"当...时使用"句式 | 明确触发条件,减少误调用 |
| 错误处理 | 始终使用@wrap_tool_call | 防止Agent崩溃,提升用户体验 |
| 参数设计 | 使用Literal限制选项 | 减少LLM幻觉,提升准确率 |
| 权限控制 | 动态工具集 + 角色检查 | 安全第一,最小权限原则 |
| 返回值 | 统一格式,包含状态标识 | 便于LLM解析和决策 |
❌ 避免踩坑
| 反模式 | 后果 | 解决方案 |
|---|---|---|
| 工具描述模糊 | LLM乱调用工具 | 添加明确的触发条件和负面示例 |
| 裸抛异常 | Agent崩溃,用户体验差 | 包装为ToolMessage返回 |
| 工具过多(>10) | 选择困难,Token浪费 | 动态工具集管理 |
| 无超时控制 | 长时间挂起 | 设置合理的timeout参数 |
| 敏感信息裸返回 | 安全风险 | 在Tool中做数据脱敏 |
八、延伸阅读与总结
8.1 核心要点回顾
@tool装饰器:快速将Python函数转换为LLM可调用的Tool,类型注解和docstring至关重要。- 描述优化:清晰的Args说明、触发条件、负面示例,能让Tool调用准确率提升50%以上。
- 错误处理:使用
@wrap_tool_call实现重试、降级、友好提示,让Agent具备韧性。 - 动态管理:根据用户角色和查询内容动态选择工具,既省Token又提准确率。
8.2 进阶学习路径
- 官方文档:
- Tools - LangChain Docs
- Middleware - Custom Wrappers
- LangGraph进阶:当你掌握了Tools基础,可以探索LangGraph的
ToolNode和StateGraph,构建更复杂的多Agent工作流。 - 记住:Tools是Agent的"手",但只有当这双手既灵巧(功能完善)又温柔(错误处理得当)时,Agent才能真正成为用户的得力助手,而不是一个"笨手笨脚还爱报错"的麻烦制造者。
现在,去给你的Agent装备一套称手的工具吧!🔧🤖
- 下一篇文章预告:我们将深入探讨System Prompt工程化实践,学习如何编写"会说话"的Prompt,让Agent不仅"能干",还要"情商高"。
关注公众号【dev派】,发送 "agent" 获取全部源码和模板
