LangChain 工具(Tools)详细介绍

在 LangChain 的 Model I/O 架构中,工具(Tools)是连接大语言模型与外部物理/数字世界的关键桥梁。模型本身只能生成文本,而工具赋予了模型“行动”的能力。本章将深入剖析工具的定义范式、底层原理、调用闭环以及生产环境中的容错与工程规范。


一、 核心概念与架构定位

1.1 为什么需要工具机制?

大语言模型存在两个根本性局限:

  1. 知识时效性:训练数据有截止日期,无法获取实时信息(如天气、股价)。
  2. 行动能力缺失:只能生成文本,无法直接执行操作(如查询数据库、调用外部 API)。

工具机制通过让模型“决定调用哪个工具、传入什么参数”,由程序“执行工具并返回结果”,实现了决策与执行的分离

1.2 工具调用的完整生命周期

不需要

需要

用户输入

模型推理

模型判断是否需要工具

直接返回文本 AIMessage

返回 tool_calls AIMessage

程序解析 tool_calls

执行对应工具函数

封装结果为 ToolMessage

将 ToolMessage 追加到历史


二、 工具定义的“双轨制”

在 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 三层防护架构

第3层: 调用级重试

第2层: Agent 级重试

第1层: 工具内部防护

try-except 捕获异常
返回错误提示字符串
防止程序崩溃

通过 Prompt 引导模型
换一种方式重新调用
或换用替代工具

使用 tenacity 等库
自动重试整个调用链
应对网络抖动

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避免序列化异常,确保跨平台兼容。
4json.dumps 必须设置 ensure_ascii=False避免中文被转义为 Unicode,确保模型理解准确。
5工具内部使用 try-except 返回错误字符串防止异常向上传播导致 Agent 链路崩溃。
6闭环引擎中严格校验 tool_call_id错一个字符都会导致底层 API 抛出 400 Bad Request
7使用 tenacity 配置指数退避重试优雅应对网络抖动和 API 限流。

7.2 常见反模式(Anti-patterns)

  1. 参数过于泛化:如 def do_action(action: str, data: str),模型无法准确生成参数。
  2. 工具描述模糊:如 """这是一个工具""",模型不知道何时该调用。
  3. 忽略 Docstring 规范:不使用 parse_docstring,导致模型对参数含义产生幻觉。
  4. 无迭代上限:在 while True 循环中不设置 max_iterations,模型可能在两个工具之间反复调用,消耗大量 Token 甚至死循环。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值