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 对象

StructuredToollangchain_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 下的 @toolStructuredTool


四、工具在 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 隐性纠错成功的四个条件

这个"隐性纠错"并非总能成功,它需要同时满足以下条件:

  1. 工具返回结果与描述不符 --> 触发重新评估的"导火索"
  2. LLM 推理能力足够强(如 qwen-maxgpt-4o) --> 能识别不匹配并调整策略
  3. 有可用的替代工具 --> 给 LLM 第二次选择的机会(如 open_browser
  4. 上下文保留(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']}"

八、总结与下篇预告

核心要点回顾

  1. 工具是 Agent 的"超能力"来源 —— 没有工具,LLM 只是聊天机器人;有了工具,它才能解决实际问题。

  2. 好的工具描述 = 好的 Agent 表现 —— 坏的描述 = 隐性翻车。工具描述是 LLM 做决策的唯一依据,务必诚实、准确、清晰。

  3. ReAct 的隐性纠错是"意外之喜" —— 它依赖 LLM 的推理能力和工具生态的完备性,不应被依赖。工具设计要诚实,不要指望 LLM 来帮你"兜底"。

  4. 工具生态要"够用" —— 即使 LLM 能发现错误,没有替代工具也只能"知错不改"。

下篇预告(喜欢请评论区催更)

计划讲解 LangGraph 的显式重试机制,以及 Human-in-the-loop 设计 —— 如何把"隐性纠错"变成"显式可控",让 Agent 的行为更加可靠和可预测。


本文写于 2026 年 6 月,基于 LangChain 0.3.x 版本。如有疑问或建议,欢迎在评论区交流!

更多推荐