智能体-LangChain框架-Tools工具的使用指南

LangChain Tools 使用指南

Tools(工具)是 LangChain 中让大模型与外部世界交互的核心机制。模型本身只会"说话",而工具让它能够真正"动手"------搜索、计算、查数据库、调 API。通过工具,Agent 可以执行搜索、计算、数据库查询等操作,极大扩展了 AI 的能力边界。

本文基于 LangChain 官方文档,系统讲解 Tools 的核心概念、创建方式、与 Agent 的结合使用,以及高级模式。

一、什么是 Tool

1.1 核心定义

Tool 是 Agent 调用以执行操作的组件。它通过明确定义的输入和输出,扩展模型的能力,使其能够:

  • 与外部 API 交互(搜索、天气、股票等)
  • 执行计算(数学运算、数据分析)
  • 操作数据库(查询、插入、更新)
  • 调用其他服务(发送邮件、文件操作等)

工具的价值不是"让代码更复杂",而是给模型一个稳定、可控、可观测的外部能力:模型负责决定「要不要用、用哪个、传什么参数」,工具负责「确定性地把事情做完」。

1.2 Tool 的核心组成

无论用哪种方式创建,一个工具本质上都包含这几个要素:

要素 作用
name 工具名称,模型通过它识别和选择工具
description 工具描述,帮助模型理解「何时该用它」
args_schema 参数 Schema,定义输入的字段、类型、约束
_run / func 同步执行逻辑
_arun / coroutine 异步执行逻辑(可选)

其中 namedescription 直接决定模型调用工具的准确率,应当认真编写------这是工具设计里最重要的一环。


二、创建 Tools 的四种方式

LangChain 提供了从「最简洁」到「最可控」的四种方式,外加若干进阶方式。

2.1 方式一:@tool 装饰器(最推荐)

最简单的方式,用 @tool 把一个普通函数变成工具。LangChain 会自动从**函数签名(即函数类型声明)文档字符串(标准注释)**中提取 Schema 和描述。

python 复制代码
from langchain_core.tools import tool


@tool
def search_database(query: str, limit: int = 10) -> str:
    """搜索客户数据库以查找匹配查询的记录。

    Args:
        query: 要查找的搜索词
        limit: 返回的最大结果数
    """
    return f"找到 {limit} 条关于 '{query}' 的结果"

三个关键点

  • 类型提示(即函数类型声明)是必需的 ------ 它们定义工具的输入 Schema(query: strlimit: int)。
  • 文档字符串(标准注释)成为工具描述 ------ 帮助模型理解何时使用它,请写清楚。
  • 函数名默认成为工具名 ------ 也可以手动覆盖。

Tool的名称和描述,也可以通过给装饰器进行定义:

python 复制代码
# 下面这个例子通过装饰器, 自定义了工具名
@tool("web_search")  # 自定义工具名
def search(query: str) -> str:
    """搜索网页获取信息。"""
    return f"搜索结果: {query}"


# 下面这个例子通过装饰器,自定义了工具名和工具描述
@tool("calculator", description="执行算术计算。用于任何数学问题。")
def calc(expression: str) -> str:
    """计算数学表达式。"""
    return str(eval(expression))  # ⚠️ 生产环境请勿用 eval,见第六章

print(search.name)  # web_search

2.2 方式二:@tool + Pydantic 定义复杂输入

当参数较多、需要默认值、枚举约束或字段描述时,用 Pydantic 模型定义 args_schema,让模型对参数格式有更精确的理解。

python 复制代码
from pydantic import BaseModel, Field
from typing import Literal
from langchain_core.tools import tool

# 基于pydantic创建 Tool的输入参数 类型声明模版
class WeatherInput(BaseModel):
    """天气查询的输入参数。"""
    location: str = Field(description="城市名称或坐标,如 '北京'")
    units: Literal["celsius", "fahrenheit"] = Field(
        default="celsius", description="温度单位偏好"
    )
    include_forecast: bool = Field(
        default=False, description="是否包含5天预报"
    )

# 通过装饰器,将基于pydantic的类型声明模版定位为工具的入参
@tool(args_schema=WeatherInput)
def get_weather(location: str, units: str = "celsius", include_forecast: bool = False) -> str:
    """获取当前天气和可选的预报。"""
    temp = 22 if units == "celsius" else 72
    result = f"{location} 当前天气: {temp}°{units[0].upper()}"
    if include_forecast:
        result += "\n未来5天: 晴朗"
    return result

Field(description=...) 里的说明会进入 Schema,模型在填参数时能看到,对提升调用准确率帮助很大。当你同时写了函数类型注解又指定了 args_schemaargs_schema 为准

2.3 方式三:StructuredTool.from_function(动态创建)

当函数逻辑已经存在、不方便加装饰器,或需要在运行时动态地 把函数封装成工具时,用工厂方法 StructuredTool.from_function

python 复制代码
from langchain_core.tools import StructuredTool

# 已存在的函数
def search_function(query: str) -> str:
    """搜索当前事件的相关信息。"""
    return "LangChain"

# 将其通过"StructuredTool.from_function" 转为工具
search = StructuredTool.from_function(func=search_function)

print(search.name)   # search_function
print(search.args)   # {'query': {'title': 'Query', 'type': 'string'}}
print(search.invoke({"query": "hello"}))  # LangChain

它会自动从函数签名、docstring 提取名称、描述和 args_schema,效果等价于 @tool,区别只在于调用形式 ------from_function 是普通函数调用,便于在代码里按条件批量生成工具。它也支持异步:StructuredTool.from_function(func=..., coroutine=...)

2.4 方式四:继承 BaseTool(完全控制)

需要复杂初始化、持有内部状态、自定义错误恢复逻辑时,直接继承 BaseTool

python 复制代码
from langchain_core.tools import BaseTool
from pydantic import BaseModel, Field
from typing import Type
import asyncio


class CalculatorInput(BaseModel):
    expression: str = Field(description="数学表达式,如 '25 * 4 + 10'")


class CalculatorTool(BaseTool):
    name: str = "calculator"
    description: str = "执行数学计算,支持加减乘除和括号"
    args_schema: Type[BaseModel] = CalculatorInput

    def _run(self, expression: str) -> str:
        """同步执行计算。"""
        # 用字符白名单做最基本的安全过滤(生产建议用 ast/numexpr,见第六章)
        allowed = set("0123456789+-*/.() ")
        if not all(c in allowed for c in expression):
            return "错误: 表达式包含非法字符"
        try:
            return f"计算结果: {eval(expression)}"
        except ZeroDivisionError:
            return "错误: 除数不能为零"
        except Exception as e:
            return f"计算错误: {e}"

    async def _arun(self, expression: str) -> str:
        """异步执行计算。重活建议用 asyncio.to_thread 包裹同步逻辑。"""
        return await asyncio.to_thread(self._run, expression)

继承 BaseTool 是自定义工具的标准方式 :你能拿到框架的全部原生支持(参数校验、与 ToolNode 集成等),同时对执行细节有最大控制权。注意 _arun 里如果是 CPU 密集或阻塞 IO,应该用 asyncio.to_thread 包裹,避免阻塞事件循环。

2.5 四种方式对比与选型

没有绝对的「最好」,只有「最适合」。区别主要在简洁度可控性之间权衡:

方式 简洁度 可控性 适用场景
@tool 装饰器 最高 默认首选,满足约 90% 需求,代码最干净,LLM 最易理解
@tool + args_schema 参数复杂、需要字段描述/枚举/默认值
StructuredTool.from_function 函数已存在、需运行时动态创建工具
继承 BaseTool 最低 最高 复杂初始化、持有状态、精细控制 _run/_arun

选型建议 :默认用 @tool;参数复杂就配 Pydantic;要动态生成用 from_function;只有在需要连接复杂资源(如数据库实例)或极致性能控制时才继承 BaseTool

其他进阶方式

python 复制代码
# 1) 把现有的 LangChain 组件(如 Retriever)转成工具
from langchain.tools.retriever import create_retriever_tool
retriever_tool = create_retriever_tool(retriever, "search_docs", "搜索文档以回答问题")

# 2) Tool 类(Legacy)------ 仅维护旧项目时用,新项目请用上面四种
from langchain.agents import Tool
legacy_tool = Tool(name="name", func=lambda x: x, description="description")

无论用哪种方式生成的工具,只要最终是 BaseTool 的实例,就能被 bind_toolsToolNode、Agent 等统一识别和执行。


三、工具的调用与使用

3.1 直接调用工具

工具本身就是可调用对象,用 invoke(异步用 ainvoke):

python 复制代码
result = search_database.invoke({"query": "张三", "limit": 5})
print(result)              # 找到 5 条关于 '张三' 的结果

print(search_database.name)          # 工具名
print(search_database.description)   # 工具描述
print(search_database.args)          # 参数 Schema

3.2 绑定模型:让模型自己决定调用(推荐)

真正的价值在于把工具交给模型,由模型分析用户意图、自动生成工具调用。现代写法是 bind_tools + 解析 .tool_calls

python 复制代码
from langchain_openai import ChatOpenAI

model = ChatOpenAI(model="gpt-4o-mini", temperature=0)
model_with_tools = model.bind_tools([search_database, get_weather])

response = model_with_tools.invoke("北京今天天气怎么样?")

# 模型若决定调用工具,tool_calls 里就是结构化的调用意图
print(response.tool_calls)
# [{'name': 'get_weather', 'args': {'location': '北京'}, 'id': 'call_xxx', 'type': 'tool_call'}]

两种情况

  • 模型决定调用工具response.content 通常为空,response.tool_calls 里是工具名 + 参数。
  • 模型决定直接回答response.tool_calls 为空,response.content 是自然语言回复。

拿到 tool_calls 后,可以手动执行并把结果以 ToolMessage 回填给模型,进入下一轮------不过这套循环 LangGraph 已经帮你封装好了(见第四章)。

3.3 旧式 functions 写法(对照了解)

早期通过 convert_to_openai_function + functions= 参数实现,结果落在 additional_kwargs['function_call'] 里。新项目请用 bind_tools,这里仅作对照:

python 复制代码
from langchain_community.tools import MoveFileTool
from langchain_core.messages import HumanMessage
from langchain_core.utils.function_calling import convert_to_openai_function

tools = [MoveFileTool()]
functions = [convert_to_openai_function(t) for t in tools]

response = chat_model.invoke([HumanMessage("将文件a移动到桌面")], functions=functions)

import json
if "function_call" in response.additional_kwargs:
    name = response.additional_kwargs["function_call"]["name"]
    args = json.loads(response.additional_kwargs["function_call"]["arguments"])
    print(f"调用工具: {name}, 参数: {args}")
else:
    print("模型回复:", response.content)

配套示例:工具调用举例.md

3.4 在 Agent 中使用(最常用)

最省心的方式是用 LangGraph 预置的 create_react_agent,它内部自动完成「模型推理 → 调用工具 → 把结果回填 → 再推理」的循环:

python 复制代码
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI
from langgraph.prebuilt import create_react_agent


@tool
def calculator(expression: str) -> str:
    """计算数学表达式。"""
    allowed = set("0123456789+-*/.() ")
    if not all(c in allowed for c in expression):
        return "错误: 表达式包含非法字符"
    return str(eval(expression))


@tool
def search_notes(topic: str) -> str:
    """查询本地笔记。"""
    notes = {
        "langchain": "LangChain 是构建 LLM 应用的框架。",
        "agent": "Agent 是能够自主决策和执行任务的 AI 系统。",
    }
    return notes.get(topic.lower(), f"未找到关于 '{topic}' 的笔记")


model = ChatOpenAI(model="gpt-4o-mini", temperature=0)
agent = create_react_agent(model, [calculator, search_notes])

result = agent.invoke({"messages": [{"role": "user", "content": "什么是 Agent?顺便算下 25 * 4"}]})
print(result["messages"][-1].content)

四、在 LangGraph 中使用 ToolNode

ToolNode 是 LangGraph 中执行工具调用 的内置节点:它读取上一条 AIMessage 里的 tool_calls,逐个执行对应工具,并把结果包成 ToolMessage 写回状态。多个工具调用会并行执行。

python 复制代码
from typing import Annotated, TypedDict
from langchain_core.messages import AIMessage, ToolMessage
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langgraph.prebuilt import ToolNode


class AgentState(TypedDict):
    messages: Annotated[list, add_messages]


tool_node = ToolNode([calculator, get_weather, search_knowledge])

builder = StateGraph(AgentState)
builder.add_node("tools", tool_node)
builder.add_edge(START, "tools")
builder.add_edge("tools", END)
graph = builder.compile()

# 模拟模型生成的工具调用
result = graph.invoke({
    "messages": [AIMessage(content="", tool_calls=[
        {"name": "calculator", "args": {"expression": "100 * 5 + 25"},
         "id": "call_1", "type": "tool_call"},
    ])]
})

for msg in result["messages"]:
    if isinstance(msg, ToolMessage):
        print(f"工具 [{msg.name}]: {msg.content}")

在真实 Agent 里通常配合条件路由 :模型节点输出后,判断 last_message.tool_calls 是否为空------非空就路由到 tools 节点执行,执行完再回到模型节点,直到模型不再调用工具:

python 复制代码
def should_continue(state: AgentState) -> str:
    last = state["messages"][-1]
    if hasattr(last, "tool_calls") and last.tool_calls:
        return "tools"
    return END

五、错误处理与高级模式

5.1 工具内部的错误处理

最佳实践是在工具内部捕获异常并返回可读的错误字符串,而不是让异常抛出去崩掉整个 Agent。这样模型能看到错误信息,进而决定重试或换一种方式。

python 复制代码
@tool
def safe_calculator(expression: str) -> str:
    """安全计算数学表达式,带完善的错误处理。"""
    if not expression or not expression.strip():
        return "错误: 表达式不能为空"
    allowed = set("0123456789+-*/.()% ")
    for c in expression:
        if c not in allowed:
            return f"错误: 不允许的字符 '{c}'"
    try:
        return f"计算结果: {eval(expression)}"
    except ZeroDivisionError:
        return "错误: 除数不能为零"
    except SyntaxError:
        return "错误: 表达式语法错误"
    except Exception as e:
        return f"计算错误: {type(e).__name__}: {e}"

5.2 ToolNode 的错误处理策略

ToolNodehandle_tool_errors 控制工具抛异常时的行为:

python 复制代码
from langgraph.prebuilt import ToolNode

# 自动捕获异常,把错误信息作为 ToolMessage 返回给模型(推荐)
tool_node = ToolNode([risky_operation], handle_tool_errors=True)

# 关闭自动处理,异常直接抛出(用于调试或自定义处理)
strict_node = ToolNode([risky_operation], handle_tool_errors=False)

handle_tool_errors 还可以传字符串 (统一的错误提示)或函数(自定义如何把异常转成提示)。

5.3 return_direct:工具结果直接返回

设置 return_direct=True,工具执行完后 Agent 立即结束并返回该结果,不再继续推理。适合「查到即答」的简单任务,能减少一轮模型调用、提升响应速度:

python 复制代码
from pydantic import BaseModel, Field
from langchain_core.tools import tool


class AddInput(BaseModel):
    a: int = Field(description="第1个加数")
    b: int = Field(description="第2个加数")


@tool("add_two_number", description="两数相加", args_schema=AddInput, return_direct=True)
def add_number(a: int, b: int) -> int:
    """两个整数相加。"""
    return a + b


print(add_number.return_direct)            # True
print(add_number.invoke({"a": 10, "b": 20}))  # 30

5.4 注入状态与运行时上下文

工具不仅能接收模型给的参数,还能注入 框架运行时的信息(如 tool_call_id、图状态),用于更新状态或回写记忆。这类参数用 Annotated 标注,模型看不到也不需要填

python 复制代码
from typing import Annotated
from langchain_core.tools import tool, InjectedToolCallId
from langgraph.types import Command


@tool
def update_profile(name: str, tool_call_id: Annotated[str, InjectedToolCallId]) -> Command:
    """更新用户资料并写回图状态。"""
    return Command(update={"profile": {"name": name}})

此外,重试机制、超时控制、调用日志(用装饰器记录工具名/参数/耗时)等也都是常见的生产级模式。

配套示例:<06_error_handling.py>、<06_advanced_patterns.py>


六、最佳实践

6.1 命名与描述

  • 工具名用动词开头 、清晰具体:search_databaseconvert_temperature,避免 tool1func_a
  • 描述要说清用途、参数格式、返回内容 ,必要时给出示例。namedescription 直接决定模型选对工具的概率。
python 复制代码
@tool
def send_email(to: str, subject: str, body: str) -> str:
    """发送电子邮件给指定收件人。

    使用此工具发送邮件通知。收件人地址必须是有效的邮箱格式。

    Args:
        to: 收件人邮箱地址,如 "user@example.com"
        subject: 邮件主题
        body: 邮件正文内容

    Returns:
        发送结果,成功返回 "邮件已发送",失败返回错误信息
    """
    ...

6.2 ⚠️ 安全:不要用 eval / exec 执行用户输入

本文为了示例简洁,多处用了 eval 演示计算器。这在生产环境中是高危漏洞 :模型或用户传入 __import__('os').system('rm -rf /') 这类表达式时,会直接执行任意代码。

正确做法 是用 ast 安全解析,或用 numexprsympy 等数学库:

python 复制代码
import ast
import operator

OPERATORS = {
    ast.Add: operator.add, ast.Sub: operator.sub,
    ast.Mult: operator.mul, ast.Div: operator.truediv,
    ast.Pow: operator.pow, ast.Mod: operator.mod,
}


def safe_eval(expression: str) -> str:
    """用 AST 安全地计算数学表达式,杜绝 eval 风险。"""
    def _eval(node):
        if isinstance(node, ast.Constant):
            return node.value
        if isinstance(node, ast.BinOp):
            return OPERATORS[type(node.op)](_eval(node.left), _eval(node.right))
        raise ValueError("不支持的表达式")
    try:
        return str(_eval(ast.parse(expression, mode="eval").body))
    except Exception as e:
        return f"计算错误: {e}"

同理,文件类工具要做路径校验 (拒绝 ../etc/~ 等),网络类工具要设超时重试

6.3 类型安全与性能

  • 始终写类型提示;复杂输入用 Pydantic 校验,对数值加 ge/le 等约束。
  • 耗时操作提供异步 _arun/coroutine,阻塞逻辑用 asyncio.to_thread 包裹。
  • 对可缓存的查询结果做缓存,对外部调用设超时。

七、总结

主题 要点
基础概念 Tool = name + description + args_schema + 执行逻辑
四种创建方式 @tool(首选)、@tool+Pydantic、StructuredTool.from_function、继承 BaseTool
调用方式 直接 invokebind_tools 让模型自主决定;create_react_agent 全自动循环
LangGraph ToolNode 执行工具调用,配合条件路由构成 Agent 循环
高级模式 内部错误处理、handle_tool_errorsreturn_direct、状态注入
最佳实践 名字/描述写清楚、杜绝 eval、类型安全、异步与缓存

掌握 Tools,是构建强大 AI Agent 的关键一步。建议从 @tool 起步,结合实际需求设计工具集,让模型真正具备「动手」的能力。


参考资源

相关推荐
莱歌数字1 小时前
双歧管拓扑优化针翅冷板:汽车功率逆变器高热通量热管理的破局之道
人工智能·科技·制造·散热·液冷散热
ziyue75751 小时前
python进行磁盘文件迁移,不影响软件使用
开发语言·数据库·python
八月瓜科技1 小时前
擎策·知海知识产权数据库迭代更新,专利检索&管理效率再提一倍!
数据库·人工智能·科技·深度学习·机器人
Promise微笑1 小时前
洞察无形:红外热像仪行业标准解析与深度选型指南
网络·人工智能·算法
searchforAI1 小时前
利用AI翻译视频做双语笔记,一套视频翻译到知识库沉淀的完整方案
人工智能·笔记·gpt·音视频·语音识别·知识图谱·机器翻译
一条泥憨鱼1 小时前
苍穹外卖【day3|菜品管理】
java·数据库·sql·mysql·mybatis
andafaAPS1 小时前
安达发|金属加工企业如何靠生产计划排单软件打破产能困局?
数据库·aps生产排程·安达发aps·生产计划排单软件·计划排产软件
Z-D-K1 小时前
考验AI的“自我和意识“-AI对《红楼梦》后40回的改写(19)
人工智能·ai·aigc·交互·agi