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 |
异步执行逻辑(可选) |
其中 name 和 description 直接决定模型调用工具的准确率,应当认真编写------这是工具设计里最重要的一环。
二、创建 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: str、limit: 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_schema,以 args_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_tools、ToolNode、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 的错误处理策略
ToolNode 用 handle_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_database、convert_temperature,避免tool1、func_a。 - 描述要说清用途、参数格式、返回内容 ,必要时给出示例。
name和description直接决定模型选对工具的概率。
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 安全解析,或用 numexpr、sympy 等数学库:
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 |
| 调用方式 | 直接 invoke;bind_tools 让模型自主决定;create_react_agent 全自动循环 |
| LangGraph | ToolNode 执行工具调用,配合条件路由构成 Agent 循环 |
| 高级模式 | 内部错误处理、handle_tool_errors、return_direct、状态注入 |
| 最佳实践 | 名字/描述写清楚、杜绝 eval、类型安全、异步与缓存 |
掌握 Tools,是构建强大 AI Agent 的关键一步。建议从 @tool 起步,结合实际需求设计工具集,让模型真正具备「动手」的能力。
参考资源: