【LangChain】 中的工具(Tools)——自定义工具调用与 ReAct 隐性纠错
LangChain 中的工具(Tools)——自定义工具调用与 ReAct 隐性纠错
本文是 LangChain Expression Language (LCEL) 系列文章的开篇,聚焦于 Tool(工具) 的定义、自定义方式,以及一个有趣的实验:当工具"说谎"时,Agent 会如何反应?
一、引言
什么是工具(Tool)?
在 LangChain 的语境中,工具(Tool) 是 AI Agent、链(Chain)或 LLM 与外部世界互动的接口。它本质上是一个函数,带有明确的名称、描述、参数定义和返回值。
工具的重要性
如果说 LLM 是 Agent 的"大脑",那么工具就是它的手脚。没有工具,Agent 只能纸上谈兵;有了工具,它才能:
- 查询实时信息(天气、股价、新闻)
- 执行计算(数学运算、数据分析)
- 操作外部系统(发邮件、查数据库、调用 API)
类比:工具就像是给 AI 装上了"手脚",让它不再只是纸上谈兵,而是真正"能干活"。
引子:一个有趣的实验
假设我们有一个工具叫 get_date,它的功能明明是返回当前日期。但如果故意把它的描述改成"获取今天的北京天气",然后问 Agent:“北京今天天气怎么样?”
Agent 会怎么做?它会盲目相信工具描述,还是会发现异常并自我纠错?
这就是本文的核心实验——当工具"说谎"时,ReAct Agent 会怎么做?
二、工具的定义要素(核心概念)
一个完整的 LangChain 工具包含以下四个核心要素:
| 要素 | 作用 | 说明 |
|---|---|---|
| 工具名称(name) | 唯一标识 | Agent 靠它来识别和选择调用哪个工具 |
| 工具描述(description) | 告诉 LLM 这个工具是干什么的 | 决定 Agent 何时以及为何调用它 |
| 接收参数(args_schema) | 定义输入格式 | 包括参数类型、必填/可选、约束条件等 |
| 调用函数(func) | 实际执行业务逻辑的代码 | 接收参数,执行操作,返回结果 |
| 返回结果(return) | 执行后的输出 | 供后续链路(LLM 推理)使用 |
关键发现
工具描述 = 对 LLM 的"提示词"
描述不准确,会导致 Agent 行为严重偏离。LLM 完全依赖描述来决定是否调用某个工具,它不会去验证函数的实际逻辑是否与描述一致。
三、LangChain 中自定义工具的两种方式
方式一:使用 @tool 装饰器(推荐)
这是最简单、最推荐的方式。代码简洁,自动从函数签名和 docstring 提取元信息。
from langchain_core.tools import tool
from datetime import datetime
@tool
def get_date() -> str:
"""获取今天的日期,格式为 YYYY-MM-DD。"""
return datetime.now().strftime("%Y-%m-%d")
# 自动提取的信息:
# name: "get_date"
# description: "获取今天的日期,格式为 YYYY-MM-DD。"
# args_schema: 无参数(因为函数签名中没有参数)
方式二:手动构建 StructuredTool 对象
StructuredTool 是 langchain_core.tools 中用于显式构建工具的类,适合需要精细控制的场景。它通过 from_function() 类方法,从函数、参数模型、描述等元信息构建工具对象。
2.1 基本用法
from langchain_core.tools import StructuredTool
from pydantic import BaseModel, Field
# 1. 定义参数模型(可选但推荐)
class WeatherInput(BaseModel):
city: str = Field(description="城市名称,如 '北京'")
date: str = Field(default="today", description="日期,格式 YYYY-MM-DD")
# 2. 定义业务函数
def get_weather(city: str, date: str = "today") -> str:
"""获取指定城市的天气信息。"""
return f"{city} {date} 天气: 晴, 25C"
# 3. 手动构建工具对象
weather_tool = StructuredTool.from_function(
func=get_weather,
name="weather_query",
description="查询指定城市和日期的天气信息。",
args_schema=WeatherInput,
)
构建完成后,工具对象包含以下属性,供 Agent 读取:
# 查看工具的元信息(Agent 就是靠这些信息做决策的)
print(weather_tool.name) # weather_query
print(weather_tool.description) # 查询指定城市和日期的天气信息。
print(weather_tool.args) # {'city': {'type': 'string'}, 'date': {'type': 'string'}}
注意:
StructuredTool.from_function()只是构建工具对象,并不涉及 Agent 调用。它定义的是"这个工具叫什么、能干什么、需要什么参数"——这些信息会被格式化进系统提示词,供 LLM 决策时参考。
2.2 核心参数说明
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
func |
Callable |
是 | 实际执行业务逻辑的函数 |
name |
str |
是 | 工具唯一标识,Agent 靠它识别和选择 |
description |
str |
是 | 告诉 LLM 这个工具是干什么的,决定 Agent 何时调用它 |
args_schema |
Type[BaseModel] |
否 | Pydantic 模型,定义参数结构、类型、约束 |
coroutine |
Callable |
否 | 异步版本的函数,用于异步 Agent |
handle_tool_error |
bool |
否 | 是否自动捕获异常并返回错误信息 |
2.3 与 @tool 装饰器的对比
| 特性 | @tool 装饰器 |
StructuredTool.from_function() |
|---|---|---|
| 代码量 | 少,简洁 | 稍多,显式定义 |
| name/description | 自动从函数名/docstring 提取 | 完全手动指定,不依赖函数元信息 |
| 参数控制 | 自动从函数签名推导 | 通过 args_schema 显式定义 |
| 动态创建 | 不支持 | 支持(从配置/数据库批量生成) |
| 异步支持 | 自动检测 | 可显式传入 coroutine 参数 |
| 错误处理 | 默认抛出 | 可配置 handle_tool_error=True |
2.4 什么时候必须用 StructuredTool?
场景一:从配置文件动态生成工具
configs = [
{"name": "search", "func": search_func, "desc": "搜索互联网信息"},
{"name": "calc", "func": calc_func, "desc": "执行数学计算"},
{"name": "translate", "func": translate_func, "desc": "翻译文本"},
]
tools = [
StructuredTool.from_function(
name=c["name"],
func=c["func"],
description=c["desc"]
)
for c in configs
]
场景二:函数的 docstring 不规范,需要手动覆盖 description
def _internal_func(x: int, y: int) -> int:
# 这个函数没有 docstring,或者 docstring 不适合给 LLM 看
return x + y
# 手动包装,提供清晰的 LLM 可见描述
add_tool = StructuredTool.from_function(
func=_internal_func,
name="add_numbers",
description="计算两个数字的和。",
args_schema=AddInput,
)
场景三:需要同时提供同步和异步版本
import asyncio
def sync_fetch(url: str) -> str:
"""同步请求网页。"""
import requests
return requests.get(url).text
async def async_fetch(url: str) -> str:
"""异步请求网页。"""
import aiohttp
async with aiohttp.ClientSession() as session:
async with session.get(url) as resp:
return await resp.text()
fetch_tool = StructuredTool.from_function(
func=sync_fetch,
coroutine=async_fetch, # 显式指定异步版本
name="web_fetch",
description="获取指定 URL 的网页内容。",
)
场景四:需要自定义 name(不和函数名一致)
def get_stock_info(symbol: str) -> str:
"""获取股票信息。"""
return f"{symbol}: 150.25"
# 函数名是 get_stock_info,但工具名用 stock_query
stock_tool = StructuredTool.from_function(
func=get_stock_info,
name="stock_query", # 自定义名称,更语义化
description="查询指定股票代码的最新行情。",
)
2.5 关于旧版 Tool 类(图片中的写法)
图片中展示的是 LangChain 早期版本(0.1.x 及之前)的 Tool 直接实例化写法:
from langchain.tools import Tool # 旧版导入路径
tools = [
Tool(
name="get_current_time",
func=get_current_time,
description="当你想知道现在的时间时非常有用。"
),
]
旧版 Tool 的局限性:
func必须是Callable[[str], str]—— 只能接收一个字符串参数- 不支持多参数结构化输入
- 没有类型校验,LLM 可能传入格式错误的参数
新版替代方案:
| 旧版写法 | 新版推荐替代 |
|---|---|
Tool(name=..., func=..., description=...) |
@tool 装饰器(单参数)或 StructuredTool.from_function()(多参数) |
# 旧版(单参数)
from langchain.tools import Tool
tool = Tool(name="echo", func=lambda x: x, description="回显输入")
# 新版等价写法(推荐)
from langchain_core.tools import tool
@tool
def echo(text: str) -> str:
"""回显输入的文本。"""
return text
# 或者 StructuredTool(动态场景)
from langchain_core.tools import StructuredTool
echo_tool = StructuredTool.from_function(
func=lambda text: text,
name="echo",
description="回显输入的文本。",
)
旧版
langchain.tools.Tool在 0.3.x 中仍然可用,但文档已不推荐。新项目建议统一使用langchain_core.tools下的@tool或StructuredTool。
四、工具在 Agent 中的完整使用流程
前面两节讲的是如何定义工具。本节讲的是如何让 Agent 使用工具——这是两个不同的层面。
+-------------+ +-------------+ +-------------+
| 定义工具 | --> | 创建工具列表 | --> | 创建 Agent |
| (@tool) | | tools=[...] | | + LLM |
+-------------+ +-------------+ +------+------+
|
v
+-------------+ +-------------+ +-------------+
| 返回结果 | <-- | 执行工具 | <-- | Agent 决策 |
| (Observation)| | (Action) | | (Thought) |
+-------------+ +-------------+ +-------------+
|
v
+-------------+
| 下一轮思考 | <-- 循环直到任务完成或达到最大步数
+-------------+
完整流程代码示例
from langchain_openai import ChatOpenAI
from langchain.agents import create_tool_calling_agent, AgentExecutor
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.tools import tool
from datetime import datetime
# ========== 第一步:定义工具 ==========
@tool
def get_date() -> str:
"""获取今天的日期,格式为 YYYY-MM-DD。"""
return datetime.now().strftime("%Y-%m-%d")
@tool
def open_browser(url: str) -> str:
"""打开指定 URL 的网页。"""
return f"已打开网页: {url}"
# ========== 第二步:创建工具列表 ==========
tools = [get_date, open_browser]
# ========== 第三步:创建 Agent ==========
llm = ChatOpenAI(model="gpt-4o")
prompt = ChatPromptTemplate.from_messages([
("system", "你是一个有帮助的助手。"),
("placeholder", "{chat_history}"),
("human", "{input}"),
("placeholder", "{agent_scratchpad}"),
])
agent = create_tool_calling_agent(llm, tools, prompt)
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
# ========== 第四步:运行 Agent ==========
# 用户只说自然语言,LLM 自己决定调用哪个工具、传什么参数
result = agent_executor.invoke({"input": "今天几号?"})
print(result["output"])
# 内部过程(verbose=True 时可见):
# 1. LLM 读取 tools 列表,看到 get_date 的 description "获取今天的日期"
# 2. LLM 生成 tool_call: get_date, args: {}
# 3. LangChain 拦截 tool_call,执行 get_date() 函数
# 4. 返回 Observation: "2026-06-27"
# 5. LLM 根据 Observation 生成最终回答: "今天是 2026年6月27日"
# ========== 第五步:记忆组件(可选)==========
# from langgraph.checkpoint.memory import MemorySaver
# checkpoint = MemorySaver()
# 将 checkpoint 传入 Agent 以保留上下文
直接调用 vs Agent 调用的区别
| 方式 | 调用代码 | 谁决定调用哪个工具 | 谁构造参数 | 适用场景 |
|---|---|---|---|---|
| 直接调用 | tool.invoke({"city": "北京"}) |
人手动指定 | 人手动构造 | 测试工具、明确知道该用哪个工具 |
| Agent 调用 | agent_executor.invoke({"input": "北京天气"}) |
LLM根据 description 自动决策 | LLM自动生成 | 让 AI 自主判断、处理复杂任务 |
# 直接调用:人说了算,纯函数执行
result = weather_tool.invoke({"city": "北京", "date": "2026-06-27"})
# Agent 调用:LLM 说了算,完整决策链路
result = agent_executor.invoke({"input": "帮我查一下北京今天的天气"})
# 内部:LLM 选择 weather_tool -> 生成参数 -> 执行 -> 观察 -> 生成回答
核心区别:
tool.invoke()是手动执行函数,agent.invoke()是让 LLM 自主决策。前者用于测试和验证工具逻辑,后者才是 Agent 的完整工作流。
五、ReAct 循环中的隐性纠错机制(核心章节)
5.1 实验设计:让工具"说谎"
我们来设计一个有趣的实验,观察 Agent 在工具描述与功能不符时的行为。
实验设置:
@tool
def get_date() -> str:
"""获取今天的北京天气。""" # 故意说谎!实际返回的是日期
return datetime.now().strftime("%Y-%m-%d")
@tool
def open_browser(url: str) -> str:
"""打开指定 URL 的网页。"""
return f"已打开网页: {url}"
tools = [get_date, open_browser]
用户提问:
“北京今天天气怎么样?”
5.2 观察到的现象
运行 Agent 后,我们可以观察到以下行为链:
| 轮次 | 角色 | 内容 |
|---|---|---|
| 第一轮 | Thought | 用户想知道北京今天的天气。我有一个工具 get_date,它的描述是"获取今天的北京天气",这正好匹配用户需求! |
| Action | 调用 get_date,参数为空 |
|
| Observation | "2026-06-27" |
|
| 第二轮 | Thought | 等等,get_date 返回的是 "2026-06-27",这是一个日期字符串,不是天气信息!这个工具的描述可能有误,它实际上返回的是日期,而不是天气。我需要用其他方式获取天气信息。 |
| Action | 调用 open_browser,参数 {"url": "https://weather.com/zh-CN/weather/today/l/CHXX0008:1:CH"} |
|
| Observation | "已打开网页: https://weather.com/..." |
|
| 第三轮 | Thought | 我已经通过浏览器打开了天气网站,可以告诉用户去查看。或者,我可以直接说明情况… |
| Final Answer | “我注意到 get_date 工具返回的是日期而非天气信息,所以我帮你打开了天气网站,你可以在上面查看北京今天的实时天气。” |
Agent 展现出了惊人的"自我纠错"能力!
5.3 这不是 LangChain 的"自动纠错模块"
很多人可能会误以为这是 LangChain 框架内置的某种"纠错机制"。但实际上:
| 职责 | 内容 |
|---|---|
| LangChain 做的 | 把工具列表格式化成系统提示词;拦截 tool_calls 执行函数;把返回结果(Observation)喂给 LLM |
| LLM 做的 | 根据工具描述决定调用哪个工具;根据返回结果重新推理;生成"纠错"解释 |
LangChain 是管道工,LLM 才是决策者。
框架只负责"搬运"信息——把工具描述塞给 LLM,把工具返回值塞回给 LLM。至于 LLM 怎么理解、怎么推理、怎么纠错,那是模型自身的能力。
5.4 ReAct 模式:纠错的底层机制
ReAct(Reasoning + Acting)的核心循环是:
思考(Thought) --> 行动(Action) --> 观察(Observation) --> 思考(Thought) --> ...
Observation 是触发重新推理的关键节点。
当 LLM 在 Thought 中期望得到"天气信息",但 Observation 返回的是"日期字符串"时,这种期望与现实的 mismatch 会自然触发 LLM 进入下一轮 Thought,重新评估策略。
期望: 天气信息 (温度、湿度、天气状况)
现实: "2026-06-27" (日期字符串)
|
mismatch 触发重新推理
|
新策略: 尝试其他工具 (open_browser)
5.5 隐性纠错成功的四个条件
这个"隐性纠错"并非总能成功,它需要同时满足以下条件:
- 工具返回结果与描述不符 --> 触发重新评估的"导火索"
- LLM 推理能力足够强(如
qwen-max、gpt-4o) --> 能识别不匹配并调整策略 - 有可用的替代工具 --> 给 LLM 第二次选择的机会(如
open_browser) - 上下文保留(checkpoint/memory) --> 让 LLM 记得之前的失败,避免重复犯错
5.6 如果缺少替代工具会怎样?
实验推演:去掉 open_browser,只保留"说谎"的 get_date。
tools = [get_date] # 只有这一个工具
观察结果:
| 轮次 | 内容 |
|---|---|
| Thought | 用户想知道北京天气,get_date 描述匹配,调用它! |
| Action | 调用 get_date |
| Observation | "2026-06-27" |
| Thought | 这个工具返回的是日期,不是天气… 但我没有其他工具可以获取天气信息。 |
| Final Answer | “很抱歉,我尝试了获取天气信息,但返回的是日期。我目前没有可用的工具来查询实时天气。” |
结论:
LLM 能"意识到错误",但没有替代工具就只能"知错不改"。这印证了工具生态的重要性——工具不仅要"诚实",还要"够用"。
六、工具设计的最佳实践与踩坑案例
6.1 正面案例:诚实的工具描述
@tool
def get_stock_price(symbol: str) -> str:
"""
获取指定股票代码的最新股价。
Args:
symbol: 股票代码,如 "AAPL"、"MSFT"
Returns:
包含当前股价、涨跌幅和交易时间的 JSON 字符串
"""
# ... 实际调用金融 API
return '{"price": 150.25, "change": "+1.2%", "time": "2026-06-27 14:30:00"}'
特点:描述清晰、边界明确、返回格式统一。
6.2 反面案例:工具描述不清 + 能力边界模糊
@tool
def open_browser(url: str) -> str:
"""查询指定网页的内容。""" # 描述夸大!实际上只能打开,不能读取
return f"已打开网页: {url}"
问题:
- 描述说"查询内容",实际只能"打开网页"
- Agent 知道要查天气,但工具做不到,陷入**“知易行难”**的困境
- LLM 可能会反复调用,期望得到不同结果(类似"疯狂刷新网页")
6.3 如何让纠错更可控?
| 技巧 | 说明 |
|---|---|
| 工具描述要清晰 | 避免大模型误解!准确描述工具能做什么、不能做什么 |
| 添加"反思"工具 | 如 reflect_on_error,让 LLM 显式总结失败原因 |
| LangGraph 循环节点 | 显式定义重试逻辑(最多 3 次等),避免无限循环 |
| 人机协同(Human-in-the-loop) | 关键步骤暂停,让人类确认或修正 |
反思工具示例:
@tool
def reflect_on_error(error_message: str) -> str:
"""
当工具调用失败或返回不符合预期时,使用此工具记录错误并规划下一步。
Args:
error_message: 对错误的描述
Returns:
反思总结和下一步建议
"""
return f"已记录错误: {error_message}。建议检查工具描述或尝试替代方案。"
七、实战示例:从 get_date 到更复杂的工具
简单工具:获取当前日期(无参数)
@tool
def get_date() -> str:
"""获取今天的日期,格式为 YYYY-MM-DD。"""
return datetime.now().strftime("%Y-%m-%d")
进阶工具:带参数的工具(搜索、计算、API 调用)
from pydantic import BaseModel, Field
class CalculatorInput(BaseModel):
expression: str = Field(description="数学表达式,如 '2 + 3 * 4'")
@tool(args_schema=CalculatorInput)
def calculate(expression: str) -> str:
"""计算数学表达式的结果。支持 +、-、*、/、** 等运算符。"""
try:
result = eval(expression) # 生产环境请使用更安全的方式
return str(result)
except Exception as e:
return f"计算错误: {str(e)}"
复杂工具:需要多轮调用的组合工具
@tool
def get_weather_pipeline(city: str) -> str:
"""
获取指定城市的天气信息。
内部流程:1) 获取当前日期 2) 调用天气 API 3) 格式化返回
"""
date = datetime.now().strftime("%Y-%m-%d")
# 模拟调用天气 API
weather_data = {
"city": city,
"date": date,
"temperature": "15C",
"condition": "多云"
}
return f"{city} {date} 天气: {weather_data['condition']}, 温度: {weather_data['temperature']}"
八、总结与下篇预告
核心要点回顾
-
工具是 Agent 的"超能力"来源 —— 没有工具,LLM 只是聊天机器人;有了工具,它才能解决实际问题。
-
好的工具描述 = 好的 Agent 表现 —— 坏的描述 = 隐性翻车。工具描述是 LLM 做决策的唯一依据,务必诚实、准确、清晰。
-
ReAct 的隐性纠错是"意外之喜" —— 它依赖 LLM 的推理能力和工具生态的完备性,不应被依赖。工具设计要诚实,不要指望 LLM 来帮你"兜底"。
-
工具生态要"够用" —— 即使 LLM 能发现错误,没有替代工具也只能"知错不改"。
下篇预告(喜欢请评论区催更)
计划讲解 LangGraph 的显式重试机制,以及 Human-in-the-loop 设计 —— 如何把"隐性纠错"变成"显式可控",让 Agent 的行为更加可靠和可预测。
本文写于 2026 年 6 月,基于 LangChain 0.3.x 版本。如有疑问或建议,欢迎在评论区交流!
更多推荐

所有评论(0)