LangChain @tool 装饰器:让函数秒变 Agent 工具
💡 摘要 :本文深入解析 LangChain 的
@tool装饰器,探讨如何通过最少的样板代码将普通 Python 函数转换为 Agent 可调用的智能工具,以及如何设计高质量的工具描述和错误处理机制。
引言
想象一下,你正在开发一个智能助手,用户说"帮我查一下北京的天气",助手需要能够真正去查询天气数据,而不是空洞地说"我无法访问外部数据"。这背后需要什么?
答案就是 工具(Tools) ------Agent 与外部世界交互的桥梁。而 LangChain 提供的 @tool 装饰器,就是构建这座桥梁的最简洁方式。
为什么需要 @tool 装饰器?
传统方式的困境
在 LangChain 早期版本中,创建一个工具需要继承 BaseTool 类:
python
from langchain_core.tools import BaseTool
from pydantic import BaseModel, Field
from typing import Type
class CalculatorInput(BaseModel):
"""计算器输入参数"""
expression: str = Field(description="要计算的算术表达式,例如 '1 + 2 * 3'")
class CalculatorTool(BaseTool):
name: str = "calculator"
description: str = "Perform arithmetic calculations"
args_schema: Type[BaseModel] = CalculatorInput
def _run(self, expression: str) -> str:
return str(eval(expression))
这段代码有几个问题:
- 需要定义多个属性(name、description、args_schema)
- 必须实现
_run方法 - 代码分散在类的多个位置
- 修改工具需要重构类结构
@tool 装饰器的优雅解决方案
使用 @tool 装饰器,同样的功能只需要:
python
from langchain_core.tools import tool
@tool
def calculator(expression: str) -> str:
"""Perform arithmetic calculations."""
return str(eval(expression))
一行装饰器,代码内聚,阅读体验如同普通 Python 函数,却自动获得了:
- 自动生成工具名称和 JSON Schema
- 内置的参数验证
- 同步/异步执行支持
- 与 Agent 的无缝集成
@tool 装饰器核心用法
1. 基础工具定义
python
from langchain_core.tools import tool
@tool
def search_database(query: str, limit: int = 10) -> str:
"""Search the customer database for records matching the query.
Args:
query: Search terms to look for (e.g., customer name, product)
limit: Maximum number of results to return (default: 10)
Returns:
Formatted string with search results
"""
# 模拟数据库查询
return f"Found {limit} results for '{query}' in database."
# 调用方式
result = search_database.invoke({"query": "Python programming", "limit": 5})
print(result)
2. 自定义工具名称和描述
默认情况下,工具名称取自函数名,描述来自函数的 docstring。但你也可以自定义:
python
# 自定义工具名称
from langchain_core.tools import tool
@tool("web_search")
def search(query: str) -> str:
"""Search the web for information."""
return f"Web search results for: {query}"
print(search.name) # 输出:web_search
# 自定义工具描述(覆盖自动生成)
@tool("calculator", description="Performs arithmetic calculations. Use this for any math problems.")
def calc(expression: str) -> str:
"""Evaluate mathematical expressions safely."""
try:
result = eval(expression, {"__builtins__": {}}, {})
return str(result)
except Exception as e:
return f"Error calculating: {str(e)}"
print(calc.description)
3. 使用 Pydantic 定义复杂输入
当工具参数较复杂时,使用 Pydantic 模型可以提供更清晰的 Schema:
python
from pydantic import BaseModel, Field
from typing import Literal
from langchain_core.tools import tool
class WeatherInput(BaseModel):
"""Input for weather queries."""
location: str = Field(description="City name or coordinates (e.g., 'Beijing' or '39.9,116.4')")
units: Literal["celsius", "fahrenheit"] = Field(
default="celsius",
description="Temperature unit for the result"
)
include_forecast: bool = Field(
default=False,
description="Whether to include 3-day forecast"
)
@tool(args_schema=WeatherInput)
def get_weather(location: str, units: str = "celsius", include_forecast: bool = False) -> str:
"""Get current weather and optional forecast for a location.
Args:
location: City name or GPS coordinates
units: Temperature unit (celsius or fahrenheit)
include_forecast: Whether to include 3-day forecast
Returns:
Weather report as formatted string
"""
temp = 25 if units == "celsius" else 77
forecast = "\nForecast: Sunny tomorrow, Rain on day 3" if include_forecast else ""
return f"Weather in {location}: {temp}°{units[0].upper()}{forecast}"
print(get_weather.description)
4. 异步工具开发
生产环境中的工具经常需要调用外部 API,这时异步工具能显著提升性能:
python
import asyncio
from langchain_core.tools import tool
import logging
logger = logging.getLogger(__name__)
@tool
async def fetch_news_articles(topic: str, max_results: int = 5) -> list:
"""Fetch latest news articles about a topic.
Use this to stay updated on:
- Industry trends and news
- Company announcements
- Market developments
Args:
topic: News topic or keyword
max_results: Maximum number of articles to return (default: 5, max: 20)
Returns:
List of article dictionaries with title, source, published_date
"""
try:
logger.info(f"Fetching news for topic: {topic}")
await asyncio.sleep(0.5) # 模拟网络延迟
max_results = min(max_results, 20)
articles = [
{
"title": f"Article {i} about {topic}",
"source": "News Source",
"published_date": "2026-04-18"
}
for i in range(max_results)
]
return articles
except asyncio.TimeoutError:
logger.error(f"Timeout fetching news for: {topic}")
return [{"error": "Request timeout, please try again"}]
except Exception as e:
logger.error(f"Error fetching news: {str(e)}")
return [{"error": f"Failed to fetch news: {str(e)}"}]
工具描述
工具描述是 Agent 理解何时使用工具的关键。好的描述遵循 T.R.I.G.G.E.R. 原则:
| 字母 | 含义 | 示例 |
|---|---|---|
| Task | 明确任务目标 | "将摄氏温度转换为华氏温度" |
| Reason | 何时触发(正例) | "当用户询问天气温度转换、食谱温度调整时" |
| Input | 参数规范 | celsius: float,范围 -273.15~1e6 |
| Guidance | 输出格式 | "成功返回字符串,失败返回错误信息" |
| Gate | 不适用场景(反例) | "不要用于开尔文转换" |
| Example | 典型调用示例 | "输入 32 → 返回 89.6°F" |
| Return | 副作用说明 | "此工具不修改任何状态" |
差描述 vs 好描述
❌ 差的描述:
@tool
def process_data(data: str) -> str:
"""Do something with data."""
return f"Processed: {data}"
✅ 好的描述:
@tool
def analyze_sentiment(text: str) -> str:
"""Analyze the emotional tone of text (positive/negative/neutral).
Use this when you need to understand:
- Customer feedback sentiment
- Social media post emotions
- Product review attitudes
Args:
text: The text content to analyze (max 1000 characters)
Returns:
Sentiment classification with confidence score
"""
return "Sentiment: Positive (92%)"
工具与 Agent 的集成
工具定义好后,集成到 Agent 中非常简单:
python
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
import os
# 初始化模型
model = init_chat_model(
model="qwen-flash-character",
model_provider="openai",
api_key=os.getenv("QWEN_API_KEY"),
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1"
)
# 创建 Agent,传入工具列表
agent = create_agent(
model=model,
tools=[calculator, search_database, get_weather],
system_prompt="You are a helpful assistant with access to various tools."
)
# 运行 Agent
result = agent.invoke({
"messages": [
("human", "Calculate 15 * 8")
]
})
print(result)
print(result["messages"][-1].content)
错误处理机制
工具执行可能因网络问题、输入无效等原因失败。良好的错误处理让 Agent 能够优雅应对:
基础错误处理
python
@tool
def divide(a: float, b: float) -> str:
"""Divide two numbers."""
try:
return str(a / b)
except ZeroDivisionError:
return "错误:除数不能为零,请提供一个非零的除数。"
except Exception as e:
return f"计算失败:{str(e)}"
使用 handle_tool_error 统一处理
python
def _handle_error(error: Exception) -> str:
return f"工具执行出错:{error}。请检查输入或稍后重试。"
@tool(handle_tool_error=_handle_error)
def risky_api_call(query: str) -> str:
import requests
response = requests.get(f"https://api.example.com/search?q={query}")
response.raise_for_status()
return response.text
最佳实践总结
- 优先使用 @tool 装饰器
- 代码简洁,内聚性好
- 自动处理类型注解和 Schema 生成
- 除非需要复杂状态管理,否则不必继承 BaseTool
- 工具描述要详尽
- 包含使用场景和触发条件
- 说明参数的类型、范围、单位
- 描述成功和失败时的输出格式
- 添加负面示例,避免误用
- 异步优先
- 对于 I/O 密集型操作,使用
async def - 配合
asyncio和aiohttp实现高效并发
- 对于 I/O 密集型操作,使用
- 错误返回而非抛出
- 工具内部捕获所有异常
- 返回人类可读的错误字符串
- 让 Agent 能够理解并尝试恢复
- 考虑抽象层级
- 单一职责工具更灵活,可组合
- 复杂流程可封装为粗粒度工具
- 提供"快捷方式"而非强制使用