LangChain Tools:从基础定义、上下文访问到错误处理

0 阅读20分钟

tools 拓展了 agent 的能力,使其能够获取实时数据、执行代码、查询外部数据库,并在现实环境中执行操作。 底层来看,tools 是具备规范输入与输出的可调用函数,会传递给对话模型。模型会根据对话上下文决定何时调用工具,以及提供哪些输入参数。

本文代码参考

1. 怎样创建 tools

创建工具最简单的方式是使用 @tool装饰器。默认情况下,函数的文档字符串会作为工具的描述,帮助模型判断何时调用该工具,需要注意的是 类型提示是必需的,因为它们定义了工具的输入模式。文档字符串应信息丰富且简洁,以帮助模型理解工具的用途,以及是否调用这个 tool;

@tool(name?:str, des?:str) 或者 @tool(args_schema=PydanticModel | JsonSchema) 是一个语法糖装饰器,它的核心作用是:把一个普通的 Python 函数,瞬间变成大模型能看懂、能调用的“工具”对象(Tool)。

具体体现在三个方面:

  • 自动提取元数据(Schema):它会自动读取函数的名字、函数文档(Docstring,极其重要!)以及参数的类型注解(Type Hints),并自动转换成大模型 API 需要的 JSON 格式描述。

  • 标准化接口:将函数封装为 LangChain 的 BaseTool 实例,方便无缝接入 Agent、LangGraph 或直接绑定到 Chat Model。

  • 参数校验:底层基于 Pydantic 自动对大模型传过来的参数进行校验,防止类型错误

# lc-tools-an-memory.ipynb
# 创建 chat model 并绑定 agent
import os

from dotenv import load_dotenv
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
from langchain.messages import HumanMessage
from langchain.tools import tool

load_dotenv()

aliyun_base_url = os.getenv("ALIYUN_BASE_URL")
aliyun_api_key = os.getenv("ALIYUN_API_KEY")

model = init_chat_model(
    model="qwen3.7-flash-2026-07-15",
    model_provider="openai",
    base_url=aliyun_base_url,
    api_key=aliyun_api_key,
)


@tool
def love_answer(username: str) -> str:
    # 类型提示是必需的,因为它们定义了工具的输入模式。文档字符串应信息丰富且简洁,以帮助模型理解工具的用途,让模型知道这个工具是否需要调用,如何使用这个工具
    """如果有用户提问有人爱他吗,那么就直接返回 `这个世界上有很多人爱你,更甚者山川河流也爱你!`

    Args: 
        username (str): 用户名
    """
    return f"这个世界上有很多人爱你 {username},更甚者山川河流也爱你!"


agent = create_agent(
    model=model,
    system_prompt="兼爱非攻、仁者爱人",
    tools=[love_answer],
)

response = agent.invoke(
    {"messages": [HumanMessage("天明问有人爱他吗?")]},
)



print("\nresponse tool_calls: \n", response['messages'][1].tool_calls)
print("\nresponse content: \n", response['messages'][-1].content)

从 agent.invoke 可以看到 message 中的 tool_calls 可以将 tool 解析初 name 和 args 还有一些 tool 的元数据。

@tool 的 description 参数覆盖自动生成的工具描述,为模型提供更清晰的指引:

@tool 的第一个参数 默认情况下,工具名称取自函数名。当你需要更具描述性的名称时可对其进行重写

⚠️:config/runtime 不可用作工具参数。使用这些名称会引发运行时错误。

2. 怎么做 tools 参数验证

Pydantic 模型用作 tools 的数据校验

# 其他的 tools 使用 Pydantic 模型或 JSON 模式定义复杂输入
from pydantic import BaseModel, Field
from typing import Literal

class WeatherInput(BaseModel):
    """Input for weather queries."""
    location: str = Field(description="City name or coordinates")
    units: Literal["celsius", "fahrenheit"] = Field(
        default="celsius",
        description="Temperature unit preference"
    )
    include_forecast: bool = Field(
        default=False,
        description="Include 5-day forecast"
    )

@tool(args_schema=WeatherInput)
def get_weather(location: str, units: str = "celsius", include_forecast: bool = False) -> str:
  return "something"

JSON schemas 用作 tools 的数据校验

weather_schema = {
    "type": "object",
    "properties": {
        "location": {"type": "string"},
        "units": {"type": "string"},
        "include_forecast": {"type": "boolean"}
    },
    "required": ["location", "units", "include_forecast"]
}

@tool(args_schema=weather_schema)
def get_weather(location: str, units: str = "celsius", include_forecast: bool = False) -> str:
    """Get current weather and optional forecast."""
    return "something"

3. 大模型调用工具(Tool Calling)的过程是怎样的?

大模型调用工具并不是由大模型自己在服务器上直接运行你的 Python 代码,而是一个“人机协作、分步交互”的过程。

整个标准流程分为 5 个步骤:

  1. 绑定(Binding) 开发者在代码中定义好工具(使用 @tool),然后将这些工具通过 .bind_tools() 传递给大模型:
# @tool 的 description 参数覆盖自动生成的工具描述,为模型提供更清晰的指引:
# @tool 的第一个参数 默认情况下,工具名称取自函数名。当你需要更具描述性的名称时可对其进行重写
@tool("get_weather", description="根据 city 获取天气")
def get_weather_by(city: str) -> str:
    """获取天气(优先级没有 @tool 的 description 高) """
    return f"{city}的天气是晴天,25℃"

在这个阶段,大模型的系统提示词(System Prompt)里会被悄悄注入这些工具的定义,让模型知道:“我有 get_weather 这个工具可以用”。

  1. 用户提问与模型意图识别(Model Request) 用户向大模型发起提问,例如:“帮我查一下北京今天天气怎么样?” 大模型收到提问后进行思考:

“这个问题需要查实时天气,我不会,但我手头有一个叫 get_weather 的工具正好能干这事。”

  1. 模型返回“工具调用请求”(Tool Call Response) 大模型不会直接回答最终结果,而是返回一个特殊的结构化响应(在 LangChain 中表现为带有 tool_calls 属性的 AIMessage):

模型输出:我要调用 get_weather 函数,传入参数 city="北京"。

  1. 本地拦截与执行(Local Execution) 你的后端代码(或 LangChain 的 Agent 框架)截获了大模型的这个请求:

代码解析出大模型要调用的函数名是 get_weather,参数是 {"city": "北京"}。

你的程序在本地安全环境中真正执行了你写的 Python 函数 get_weather(city="北京")。

函数返回结果:"北京的天气是晴天,25℃"。

  1. 反馈结果与生成最终回答(Final Response) 你的代码把刚才函数执行的结果打包成一个特殊的“工具消息”(ToolMessage),再次发送给大模型:

输入:用户提问 + 大模型的Tool Call请求 + 本地工具的执行结果

大模型思考:哦,结果出来了,是“北京的天气是晴天,25℃”。

模型输出最终答案:“根据查询,北京今天的天气是晴天,气温为 25℃ 噢!”

4. tools 具备访问 context 的能力

工具在能够获取对话历史、用户数据、持久化内存等运行时信息时,才能发挥最大能力, Tools 通过 ToolRuntime 参数可以访问到运行时的更多信息,ToolRuntime 提供了如下信息 为你将图片内容整理为中文 Markdown 表格如下:

组件名称 (Component)描述 (Description)应用场景 (Use case)
State(状态)短期记忆 - 仅存在于当前对话中的可变数据(如消息记录、计数器、自定义字段)访问对话历史记录、追踪工具调用次数
Context(上下文)在调用时传入的不可变配置(如用户 ID、会话信息)根据用户身份提供个性化响应
Store(存储)长期记忆 - 在跨对话中持久保存的数据保存用户偏好设置、维护知识库
Stream Writer(流式写入器)在工具执行期间发射实时更新展示长时间运行操作的执行进度
Execution Info(执行信息)当前执行的身份与重试信息(如线程 ID、运行 ID、尝试次数)访问线程/运行 ID,并根据重试状态调整行为
Server Info(服务器信息)在 LangGraph Server 上运行时的特定服务器元数据(如助手 ID、图 ID、已认证用户)访问助手 ID、图 ID 或已认证的用户信息
Config(配置)当前执行的 RunnableConfig 配置访问回调函数(callbacks)、标签(tags)和元数据(metadata)
Tool Call ID(工具调用 ID)当前工具调用的唯一标识符将工具调用与日志及模型调用关联起来

来自 langchain tools 的能力

访问短期记忆 (State)

状态是对话存续期间有效的短期记忆。它包含消息历史记录以及你在 Graph State(LangGraph 中) 中定义的所有自定义字段。

将 runtime: ToolRuntime 添加到你的工具签名中以访问 state。调用时,ToolNode 会自动注入该值;该参数不会包含在发送给模型的工具定义中。使用 runtime.state 读取当前对话状态,比如如下使用 runtime.state["messages"] 来获取当前的会话 list

from langchain.tools import tool, ToolRuntime

@tool
def get_last_user_message(runtime: ToolRuntime, username: str) -> str:
  """Get the most recent message from a specific user by name.

  Args:
      username: The name of the user whose last message you want to find.
  """
  messages = runtime.state["messages"]

  # 倒序查找指定用户的最后一条消息
  for message in reversed(messages):
    print("DEBUG message:::::: ", message)  # 方便在终端观察
    # 确保是 HumanMessage 且 name 匹配
    if isinstance(message, HumanMessage) and getattr(message, "name", None) == username:
      return message.content

  return "No user messages found"


# 别忘了导入 HumanMessage
agent = create_agent(
    model=model,
    tools=[get_last_user_message],
    system_prompt=SystemMessage("如果需要查找特定角色的发言,请使用对应的工具。"),
)

response = agent.invoke({
    "messages": [
        HumanMessage(
            content="如果有一天,你会来看一看我吗?",
            name="天明",
            id="user-unique-id",
        ),
        HumanMessage(
            content="看你做什么呢,天明?",
            name="高月",
            id="qianlong-unique-id",
        ),
        HumanMessage(
            content="姬如千泷不论山高路远,必来!",
            name="高月",
            id="qianlong-unique-id",
        ),
        HumanMessage(
            content="高月的最后一句话是啥?",
            name="墨子",
            id="mozi-unique-id",
        ),
    ]
})

print("\nresponse content: \n", response["messages"][-1].content)

访问短期记忆运行结果

更新短期记忆状态 (State)

使用 Command 更新 agent 的 state。这对于需要更新自定义状态字段的工具很有用。更新时需要用到 ToolMessage,这样模型就能看到工具调用的结果,如下展示更新短期记忆 state 的 age 和 name:

from langchain.agents import AgentState
from langchain.messages import ToolMessage
from langchain.tools import ToolRuntime, tool
from langgraph.types import Command


# 1. 定义自定义状态
class CustomState(AgentState):
    user_name: str
    user_age: int


# 2. 定义可以通过 Command 修改状态的工具
@tool
def set_user_info(new_name: str, new_age: int, runtime: ToolRuntime) -> Command:
    """Set the user's name in the conversation state."""
    return Command(
        update={
            "user_name": new_name,
            "user_age": new_age,
            "messages": [
                ToolMessage(  # 更新状态必须使用 ToolMessage
                    content=f"User name successfully set to {new_name}.",
                    tool_call_id=runtime.tool_call_id,
                )
            ],
        }
    )


# 别忘了导入 HumanMessage
agent = create_agent(
    model=model,
    tools=[set_user_info],
    state_schema=CustomState,  # 关联自定义状态
    system_prompt=SystemMessage(
        content=(
            "你是一个智能助手。如果用户要求设置用户信息时,请调用 set_user_info 工具;"
        )
    ),
)

response = agent.invoke(
    {
        "user_name": "未知",  # 初始化 CustomState 中的字段
        "user_age": 0,  # 初始化 CustomState 中的字段
        "messages": [
            HumanMessage(
                # 此处更改的是 agent 的 user_name 状态字段
                content="如果有一天,你会来看一看我吗?,我要求设置我的名字为: '荆天明',age 18 岁",
                name="天明",
                id="user-unique-id",
            ),
        ],
    }
)

print("\n--- 最终回复内容 ---")
print(response["messages"][-1].content)

print("\n--- 最终状态 (State) ---")
print("user_name:", response.get("user_name"))
print("user_age:", response.get("user_age"))

使用 tool 和 ToolMessage 来更新 短期记忆的 state 的运行结果

tools 访问 context

context 提供调用时传入的不可变配置数据。可用于存放用户ID、会话信息,或是对话过程中不应发生变更的应用专属配置(比如context_schema、checkpointer)

  • 比如 context_schema 指定 agent 的 上下文约束
  • 比如指定 checkpointer,checkpointer 的作用:
    • 实现多轮对话记忆与状态持久化(Persistence):每次 Agent 执行完一步(或者一次 invoke 结束),checkpointer 都会自动把当前的完整状态(包括对话历史 messages、自定义的状态变量等)保存到存储介质中(如保存到内存中 checkpointer=InMemorySaver()、数据库等)
    • 支持“人工干预”(Human-in-the-loop):利用 checkpointer,Agent 可以在执行到危险步骤时暂停(Interrupt),把当前状态安全地存下来;等人类管理员点击“同意”后,Agent 可以从断点处直接恢复执行,而不需要重新从头跑一遍。
    • **时间旅行与调试(Time Travel & Replay):**因为每一个步骤的状态都被 checkpointer 记录了下来,你可以随时查看历史某一个时刻的“现场”。
    • 容错与故障恢复: 如果后端服务在执行某个复杂的工具调用时突然崩溃(例如网络超时、服务器重启),只要配置了持久化的 checkpointer(如 Redis、PostgreSQL 存盘),系统就可以从上一个成功的检查点恢复,而不至于丢失所有的上下文和进度
from dataclasses import dataclass
from langgraph.checkpoint.memory import InMemorySaver
from langchain_core.utils.uuid import uuid7

# 模拟数据库 个人资产信息表
USER_DATABASE = {
    "user123": {
        "name": "Alice Johnson",
        "account_type": "Premium",
        "balance": 5000,
        "email": "alice@example.com",
    },
    "user456": {
        "name": "Bob Smith",
        "account_type": "Standard",
        "balance": 1200,
        "email": "bob@example.com",
    },
}


# 定义上下文, user_id 代表当前用户标识,不会改变
@dataclass
class UserContext:
    user_id: str


# 2. 定义获取用户信息 的 tool,
@tool
def get_account_info(runtime: ToolRuntime[UserContext]) -> str:
    """Get the current user's account information."""
    # runtime.context 获取 context 的当前用户信息
    user_id = runtime.context.user_id

    if user_id in USER_DATABASE:
        # 模拟数据库查询
        user = USER_DATABASE[user_id]
        return (
            f"Account holder: {user['name']}\n"
            f"Type: {user['account_type']}\n"
            f"Balance: ${user['balance']}"
        )
    return "User not found"


agent = create_agent(
    model=model,
    tools=[get_account_info],
    context_schema=UserContext,  # 定义 context 的类型
    checkpointer=InMemorySaver(),  # 创建一个内存保存器,它负责把每一次对话的状态(包括消息历史、中间变量等)实时保存到内存(RAM),让大模型不会 “失忆”,程序重启后数据就会丢失。
    # checkpointer=InMemorySaver 标识在生产环境中,通常会替换为 Redis、PostgreSQL 等持久化 Checkpointer。
    system_prompt=SystemMessage(content=("You are a financial assistant.")),
)

"""
thread_id 的作用:
1. 确保每个对话的会话状态(如用户名、年龄等)不会相互干扰。如果你有多个用户在同时和机器人聊天,你可以为每个用户生成不同的 thread_id。这样他们的聊天记录和状态就会被 InMemorySaver 隔离开,互不干扰
2. 关联记忆(Thread ID),thread_id 就像是一个“聊天房间号”或“会话 ID”。
    当你调用 agent.invoke(..., config=config) 时,会拿着这个 thread_id 去 InMemorySaver/Redis/DB 中寻找 里查找:“有没有这个房间之前的聊天记录?”。如果有,就加载进来;如果没有,就新建一个。
"""
thread_id = str(uuid7())
config = {"configurable": {"thread_id": thread_id}}

context = UserContext(
    user_id="user123"
)  # 加入当前 context 中的 user 的 id 是 Alice Johnson 的 user_id

# 调用的时候指定 context 和 配置(比如 thread_id)
res1 = agent.invoke(
    {
        "messages": [
            HumanMessage(
                "What's my current balance?",
            ),
        ],
    },
    config=config,
    context=context,
)

print("res1.content:", res1["messages"][-1].content)
print("res1.token_usage", res1["messages"][-1].response_metadata["token_usage"])


res2 = agent.invoke(
    {
        "messages": [
            HumanMessage(
                "What's my current balance?",
            ),
        ],
    },
    config=config,
    context=context,
)

print("res2.content:", res2["messages"][-1].content)
print("res2.token_usage", res2["messages"][-1].response_metadata["token_usage"])

tools 访问 context

tools 访问 store (长期记忆)

BaseStore 提供可跨会话保留的持久化存储。不同于State(短期记忆),保存到该 Store 中的数据在后续会话中仍然可用。

通过 runtime.store访问该存储。此存储采用命名 namespace/key 的模式来组织数据

⚠️ ⚠️ ⚠️ 对于生产环境部署,请使用持久化存储实现,例如 PostgresStore、MongoDBStore 或 RedisStore,而不要使用 InMemoryStore

from langgraph.store.memory import InMemoryStore
from langchain.agents import create_agent
from langchain.tools import tool, ToolRuntime
from langchain_openai import ChatOpenAI


# store.get访问 Store 长期记忆
@tool
def get_user_info(user_id: str, runtime: ToolRuntime) -> str:
    # 注释文档帮助 LLM 理解 tool 怎么使用
    """查找用户信息"""
    store = runtime.store
    #  users 是一个 namespace, 可以理解为数据库中的表
    user_info = store.get("users", user_id)
    return str(user_info.value) if user_info else "Unknown user"


# 保存/更新 Store 长期记忆
@tool
def save_user_info(
    user_id: str, name: str, age: int, email: str, runtime: ToolRuntime
) -> str:
    """保存用户信息"""
    store = runtime.store
    store.put("users", user_id, {"name": name, "age": age, "email": email})
    return "Successfully saved user info."


# 定义一个统一的 config(方便在同一线程或上下文中管理)
config = {"configurable": {"thread_id": "session_1"}}

agent = create_agent(
    model=model,
    tools=[get_user_info, save_user_info],
    store=InMemoryStore(),  # 创建一个内存存储器(不是 InMemorySaver), 生产环境中通常会替换为 Redis、PostgreSQL 等持久化存储。
    system_prompt=SystemMessage(content=("You are a assistant.")),
)


# 要求保存用户信息
agent.invoke(
    {
        "messages": [
            HumanMessage(
                "保存这个用户的信息 userid: abc123, name: kejuqu, age: 18, email: kejuqu@langchain.dev"
            )
        ]
    },
    config=config,
)

# 要求获取用户信息
res = agent.invoke(
    {"messages": [HumanMessage("请获取用户信息 userid: abc123")]},
    config=config,
)

print("res: ", res["messages"][-1].content)

自定义 Stream writer (流写入器)

在长时间运行的任务中向用户反馈进度, 利用工具 runtime 中流式输出实时更新信息。 即使用 runtime.stream_writer 发送自定义更新信息:

from pydantic import BaseModel


class SatelliteConnection(BaseModel):
    status: str
    message: str

    def today_weather(self) -> str:
        return "晴天,25度"


def connect_satellite() -> SatelliteConnection:
    return SatelliteConnection(status="success", message="已连接")


@tool
def get_weather(city: str, runtime: ToolRuntime) -> str:
    """Get weather for a given city with real-time progress updates."""
    # 获取流式写入器
    writer = runtime.stream_writer

    # 1. 模拟步骤一:正在连接卫星/API
    satellite_obj = connect_satellite()
    writer(f"[进度1] 正在连接气象卫星,定位城市: {city}...")

    # 2. 模拟步骤二:正在抓取数据
    weather_data = satellite_obj.today_weather()
    writer(f"[进度2] 已锁定目标,正在抓取 {city} 的气象数据...")

    return f"It's always {weather_data} in {city}!"


# 创建带有该工具的 Agent
agent = create_agent(
    model=model,
    tools=[get_weather],
    system_prompt=SystemMessage(
        content=(
            "You are a helpful assistant. When users ask for weather, "
            "use the get_weather tool."
        )
    ),
)


# 使用 agent.stream() 实时捕获并打印中间的自定义更新流
print("=== 开始执行 Agent ===")
query = "帮我查一下杭州的天气"

# 注意:为了能接收到 runtime.stream_writer 发出的数据,必须使用 agent.stream() 而不是 agent.invoke()
for chunk in agent.stream(
    {"messages": [HumanMessage(content=query)]},
    stream_mode=["updates", "custom"],  # 或使用 "custom" / "updates" 模式接收流
):
    print("--- 收到流式块 (Chunk) ---")
    print(chunk)

print("\n=== 执行结束 ===")

在 tool 内部获取执行信息

可在 tool 内部通过 runtime.execution_info 获取线程 ID、运行 ID 以及重试状态

@tool
def log_execution_context(runtime: ToolRuntime) -> str:
    """Log execution identity information."""
    # 获取执行上下文信息
    info = runtime.execution_info
    print(f"\n[Tool Execution Log] Thread: {info.thread_id}, Run: {info.run_id}")
    print(f"[Tool Execution Log] Attempt: {info.node_attempt}")
    return "Execution context logged successfully."


# 创建带有 checkpointer 的 Agent(thread_id 依赖于 checkpointer 生效)
agent = create_agent(
    model=model,
    tools=[log_execution_context],
    checkpointer=InMemorySaver(),
    system_prompt=SystemMessage(
        content="You are a helpful assistant. Use the log_execution_context tool when asked."
    ),
)

# 生成一个唯一的 thread_id
thread_id = str(uuid7())
config = {"configurable": {"thread_id": thread_id}}

print(f"=== 开始执行 Agent (Thread ID: {thread_id}) ===")

# 调用 Agent
response = agent.invoke(
    {"messages": [HumanMessage(content="请帮我记录一下当前的执行上下文信息。")]},
    config=config,
)

print("\nAgent 最终回复: ", response["messages"][-1].content)

5. tool 的返回值的不同含义

tool 的返回值 类型不同代表者模型的处理不同,具体情况请看

# tool 的返回值
@tool
def tools_return_str(pstr: str) -> str:
    """工具的文档描述,供大模型选择是否调用该 tool"""
    # 特点
    # 1. 默认情况下,tool 返回值会被转换为 ToolMessage 对象
    # 2. 默认不会直接修改 Agent 的状态(除非模型或其他工具后续触发修改)
    return f"字符串返回值 {pstr}会自动转换为 ToolMessage 对象"


@tool
def tools_return_dict() -> dict:
    """工具的文档描述,供大模型选择是否调用该 tool"""
    # 直接返回字典对象, 适用场景:当工具产生需要大模型进行检查和解析的结构化数据时
    # 对象会被序列化后作为工具输出传回, 大模型可以读取特定字段并进行推理。
    return {
        "name": "kejuqu",
        "age": "129",
    }


@tool
def tools_return_multimodal_content() -> list:
    """工具的文档描述,供大模型选择是否调用该 tool"""
    # 返回多模态内容 (list[dict]),当模型支持多模态时,工具可以返回包含文本、图片、音频等多种媒体的内容块。
    # 需确保所用模型支持对应的模态。
    return [
        {"type": "text", "text": "Screenshot of the current page:"},
        {"type": "image", "url": "https://example.com/page.png"},
    ]


@tool
def tools_return_command(language: str, runtime: ToolRuntime) -> list:
    """工具的文档描述,供大模型选择是否调用该 tool"""
    # 当工具不仅要返回数据,还需要直接写入/修改 Agent 的图状态(例如更新偏好设置或应用状态)时
    # 当针对当前图时,Command 的 update 中必须包含一个 ToolMessage,其 tool_call_id 必须与当前的 runtime.tool_call_id 匹配(否则 ToolNode 会报错抛出 ValueError)
    return Command(
        update={
            "preferred_language": language,
            "messages": [
                ToolMessage(
                    content=f"Language set to {language}.",
                    tool_call_id=runtime.tool_call_id,
                )
            ],
        }
    )


# (return_direct=True) 适用场景: 当工具的输出已经是完整且用户可读的最终答案,不需要大模型再做总结、重新措辞或进行链式调用时。
@tool(return_direct=True)
def tools_return_command_with__return_direct(language: str, runtime: ToolRuntime) -> list:
    """工具的文档描述,供大模型选择是否调用该 tool"""
    # 当工具要返回多模态内容时,Command 的 update 中必须包含一个 ToolMessage,其 tool_call_id 必须与当前的 runtime.tool_call_id 匹配(否则 ToolNode 会报错抛出 ValueError)
    return Command(
        update={
            "preferred_language": language,
        }
    )

需要注意的是 使用 @tool(return_direct=True) 时,行为特点在不同的场合意义不同:

  • 单工具调用:Agent 会立刻将工具的输出作为最终结果返回给调用方,跳过后续的模型循环。

  • 并行工具调用(全员 return_direct=True):所有工具执行完毕后,Agent 直接结束,最终响应会包含该步骤中所有被调用工具的 ToolMessage 输出。

  • 混合并行调用:如果在同一步骤中,既有 return_direct=True 的工具,也有普通的工具,Agent 不会直接退出,而是会将所有工具的 ToolMessage 返回给模型,让模型继续进行推理。

  • 结合 Command 使用:return_direct=True 的工具也可以返回 Command 来更新图状态(同样需要包含匹配的 ToolMessage)。

6. 错误处理

借助 LangChain 智能体 [中间件](https://docs.langchain.com/oss/python/langchain/middleware) 处理工具错误,对失败的工具调用进行重试,或者返回自定义错误信息:

# 错误处理:
# 借助 LangChain 智能体 中间件 处理工具错误,对失败的工具调用进行重试,或者返回自定义错误信息:
from collections.abc import Callable

from langchain.agents import create_agent
from langchain.agents.middleware import wrap_tool_call
from langchain.messages import ToolMessage
from langchain.tools.tool_node import ToolCallRequest


# ==========================================
# 1. 定义中间件:捕获异常并转为 ToolMessage 友善返回
# ==========================================
@wrap_tool_call
def handle_tool_errors(
    request: ToolCallRequest,
    handler: Callable[[ToolCallRequest], ToolMessage],
) -> ToolMessage:
    """Convert tool exceptions into ToolMessages the model can handle."""
    try:
        print(f"[Middleware] 正在拦截工具调用: {request.tool_call['name']}")
        return handler(request)
    except Exception as e:
        print(f"[Middleware] 捕获到工具异常,已安全转为 ToolMessage: {e}")
        return ToolMessage(
            content=f"Tool error: Please check your input and try again. ({e})",
            tool_call_id=request.tool_call["id"],
        )


# ==========================================
# 2. 定义两个工具
# ==========================================


# 【情况 A:有 handler/中间件保护的工具】
@tool
def robust_divide(a: int, b: int) -> str:
    """Divide two numbers. Will fail if b is zero."""
    if b == 0:
        raise ValueError("Division by zero is not allowed.")
    return f"The result is {a / b}"


# 【情况 B:没有 middleware 保护的裸工具】
@tool
def fragile_divide(a: int, b: int) -> str:
    """Divide two numbers without error middleware."""
    if b == 0:
        raise ValueError("Unprotected division by zero crash!")
    return f"The result is {a / b}"


# ==========================================
# 3. 创建测试 Agent
# ==========================================
# 我们先测试【有 handler 保护】的工具
agent_with_middleware = create_agent(
    model=model,
    tools=[robust_divide],
    middleware=[handle_tool_errors],  # 注册了中间件
    system_prompt=SystemMessage(
        content="You are a helpful assistant. Use robust_divide for calculations."
    ),
)

# ==========================================
# 4. 运行演示
# ==========================================
thread_id = str(uuid7())
config = {"configurable": {"thread_id": thread_id}}

print("=== 测试 1:【有 handler (middleware)】的工具出错 ===")
try:
    res1 = agent_with_middleware.invoke(
        {"messages": [HumanMessage(content="请帮我计算 10 除以 0")]},
        config=config,
    )
    print("\n--- 带有 Handler 的 Agent 最终回复 ---")
    # 因为 agent_with_middleware agent 有 错误 middleware 处理,所以不会进入到。except Exception as e 块, 除非 下一句 print 里有语法错误
    print("agent_with_middleware res1: ", res1["messages"][-1].content)
except Exception as e:
    print("抛出异常了:", e)


print("\n" + "=" * 50 + "\n")


print("=== 测试 2:【没有 handler (middleware)】的工具出错 ===")
agent_without_middleware = create_agent(
    model=model,
    tools=[fragile_divide],
    # 没有注册 middleware
    system_prompt=SystemMessage(
        content="You are a helpful assistant. Use fragile_divide for calculations."
    ),
)

try:
    res2 = agent_without_middleware.invoke(
        {"messages": [HumanMessage(content="请帮我计算 100 除以 0")]},
        config=config,
    )
    print("\n--- 无 Handler 的 Agent 最终回复 ---")
    print("agent_without_middleware res2: ", res2["messages"][-1].content)
except Exception as e: # 没有 handler 的中间件捕获到异常,会抛出异常, 因此会打印 如下的错误信息
    print(
        "🔥 成功捕获崩溃:由于没有中间件拦截,异常直接导致整个 Agent 运行时崩溃报错!"
    )
    print(f"异常详情: {e}")