【LangChain】Tools工具系统深度解析:给Agent一双灵巧的手

0 阅读17分钟

【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模型定义的参数结构

代码解读

  1. @tool装饰器:魔法发生的地方。它会自动:

    • 从函数名生成Tool名称(get_weather
    • 从docstring提取描述(前三行)
    • 从类型注解生成JSON Schema参数定义
  2. 类型注解city: strcountry_code: Optional[str]不仅是Python的类型提示,更是LLM理解"我需要传什么参数"的关键线索。

  3. 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(最低),影响资源分配"
    )
):
    """执行数据分析任务,从指定来源提取数据并按需处理。"""
    ...

进阶技巧:使用AnnotatedField可以添加验证规则和更丰富的描述,让LLM不仅知道"要什么",还知道"要怎么给"。


四、错误处理:让Agent学会" resilience"

4.1 为什么需要专门的错误处理?

想象一下:你的Agent调用了一个支付接口,结果网络超时了。如果没有恰当的错误处理,Agent可能会:

  1. 直接崩溃——用户体验极差
  2. 返回一堆Python traceback——用户完全看不懂
  3. 陷入死循环——不断重试同一个失败的操作

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]
)

关键点解析

  1. ToolMessage的特殊作用:与普通消息不同,ToolMessage带有tool_call_idstatus="error",这让LLM明白"这是工具返回的错误,不是我生成的内容",从而可以据此调整策略。

  2. 指数退避策略:第一次等1秒,第二次等2秒,第三次等4秒。这避免了对已经过载的服务"疯狂敲门"。

  3. 错误分类:区分"可以重试"(网络超时)和"不应该重试"(参数错误)的情况。

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 核心要点回顾

  1. @tool装饰器:快速将Python函数转换为LLM可调用的Tool,类型注解和docstring至关重要。
  2. 描述优化:清晰的Args说明、触发条件、负面示例,能让Tool调用准确率提升50%以上。
  3. 错误处理:使用@wrap_tool_call实现重试、降级、友好提示,让Agent具备韧性。
  4. 动态管理:根据用户角色和查询内容动态选择工具,既省Token又提准确率。

8.2 进阶学习路径

  • 官方文档
    • Tools - LangChain Docs
    • Middleware - Custom Wrappers
    • LangGraph进阶:当你掌握了Tools基础,可以探索LangGraph的ToolNodeStateGraph,构建更复杂的多Agent工作流。
    • 记住:Tools是Agent的"手",但只有当这双手既灵巧(功能完善)又温柔(错误处理得当)时,Agent才能真正成为用户的得力助手,而不是一个"笨手笨脚还爱报错"的麻烦制造者。

现在,去给你的Agent装备一套称手的工具吧!🔧🤖


  • 下一篇文章预告:我们将深入探讨System Prompt工程化实践,学习如何编写"会说话"的Prompt,让Agent不仅"能干",还要"情商高"。

关注公众号【dev派】,发送 "agent" 获取全部源码和模板