上一篇介绍了 Deep Agents 的 Planning 机制。通过任务拆解,Agent 能够明确目标、安排步骤,并跟踪执行进度。

但规划本身并不能完成任务。

Planning 仅负责安排任务,而 Tools 则负责执行具体操作。

例如,一个故障处理 Agent 可以制定这样的计划:查询未解决的工单、检查故障信息、更新工单状态,最后汇总处理结果。

要完成这些操作,它必须具备查询数据、访问系统和修改状态的能力。这些能力主要通过 Tools 提供。

本文介绍 Deep Agents 的工具机制、设计方法和实践要点,并通过一个完整示例,说明如何开发可用于实际业务的工具。

一、工具的基本定位

Tool 是 Agent 调用外部能力的接口。

它可以是一个普通的 Python 函数,也可以封装数据库查询、HTTP API、内部业务系统,或者通过 MCP 连接外部服务。

例如:

  • search_web:搜索网络资料。
  • get_order:查询订单信息。
  • update_ticket:修改工单状态。
  • send_email:发送邮件。

这些工具虽然连接不同的系统,但对 Agent 来说,都包含几个基本要素:工具名称、功能描述、输入参数和返回结果。

需要分清的是,模型只负责选择工具并生成参数,实际执行由 Agent 的运行环境完成。

整个过程如下:

代码片段Text
LLM
 │
 │ 选择工具、生成参数
 ▼
Tool Calling
 │
 │ 参数校验
 ▼
Tool Runtime
 │
 │ 执行函数
 ▼
External System
 │
 │ 返回结果
 ▼
LLM
 │
 ▼
继续推理或完成任务

例如,模型决定调用:

代码片段JSON
{
  "name": "get_order",
  "args": {
    "order_id": "ORD-1001"
  }
}

这只是一次工具调用请求。

框架根据名称找到对应工具,校验参数并执行函数,再将执行结果作为 ToolMessage 返回给模型。

模型读取结果,决定继续调用其他工具,还是生成最终回答。

这就是 Tool Calling 的基本过程。

二、Deep Agents 的工具体系

Deep Agents 建立在 LangChain 的 Agent 机制之上,通过 create_deep_agent() 组织模型、工具和运行环境。

工具主要分为两类。

1. 内置工具

Deep Agents 提供了一组用于执行复杂任务的基础工具。

类别工具作用
文件管理ls、read_file、write_file、edit_file、glob、grep、delete文件操作
命令执行execute执行 Shell 命令
任务管理write_todos拆解任务、跟踪进度
任务委派task交给 Subagent 执行

并非所有工具在任何配置下都可用。

例如,execute 需要 Backend 提供命令执行能力,而 write_todos 从 Deep Agents 0.7.0 起改为按需启用。

如果需要使用 write_todos,可以通过 TodoListMiddleware 添加:

代码片段Python
from deepagents import create_deep_agent
from langchain.agents.middleware import TodoListMiddleware
 
agent = create_deep_agent(
    model="openai:gpt-5.5",
    middleware=[TodoListMiddleware()],
)

启用后,Agent 可以创建和更新任务清单,跟踪 pending、in_progress、completed 等任务状态。

关于 TodoListMiddleware 的介绍以及具体细节可查看上一篇文章。

文件管理、命令执行和任务委派将在后续文章中详细介绍。本文主要关注自定义工具。

2. 自定义工具

如果希望 Agent 访问自己的业务系统,就需要注册自定义工具。

代码片段Python
from deepagents import create_deep_agent
 
def get_order(order_id: str) -> dict:
    """根据订单编号查询订单信息。"""
    return {
        "order_id": order_id,
        "status": "paid",
    }
 
agent = create_deep_agent(
    model="openai:gpt-5.5",
    tools=[get_order],
)

这里通过 tools 注册业务工具。

Deep Agents 根据函数签名和文档字符串生成工具定义,供模型识别和调用。

需要注意,tools=[get_order] 只是增加一个业务工具,不会自动删除框架的内置工具。

Agent 最终能够使用哪些工具,还取决于框架配置和运行环境。

3. @tool 装饰器

除了直接注册普通 Python 函数,还可以使用 LangChain 提供的 @tool 装饰器。

代码片段Python
from langchain.tools import tool
 
@tool
def get_order(order_id: str) -> dict:
    """根据订单编号查询订单信息。"""
    return {
        "order_id": order_id,
        "status": "paid",
    }

@tool 不是必需的。两种写法都可以注册到 Deep Agents。

区别在于,普通函数由框架在注册时转换为工具;使用 @tool 则会直接创建一个 LangChain Tool 对象。

因此,可以查看工具的定义:

代码片段Python
print(get_order.name)
print(get_order.description)
print(get_order.args)

也可以通过 invoke() 单独执行:

代码片段Python
result = get_order.invoke({
    "order_id": "ORD-1001"
})

使用 @tool 后,还可以明确指定工具名称和描述:

代码片段Python
@tool(
    "lookup_order",
    description="根据订单编号查询订单当前状态"
)
def get_order(order_id: str) -> dict:
    return {
        "order_id": order_id,
        "status": "paid",
    }

此时,模型看到的工具名称是 lookup_order,而不是 Python 函数名 get_order。

对于较复杂的输入,还可以通过 Pydantic 定义参数结构:

代码片段Python
from pydantic import BaseModel, Field
 
class OrderInput(BaseModel):
    order_id: str = Field(
        description="订单编号,例如 ORD-1001"
    )
 
@tool(args_schema=OrderInput)
def get_order(order_id: str) -> dict:
    """查询指定订单的信息。"""
    return {
        "order_id": order_id,
        "status": "paid",
    }

这里的 args_schema 用于定义工具的输入参数,包括字段类型、说明和校验规则。

简单工具可以直接使用普通函数。需要更明确地控制工具名称、描述和参数时,@tool 更方便。

三、工具的设计原则

普通程序中,函数的调用时机和参数通常由开发者确定。Agent 则需要模型自行判断,有时还会重复调用。

因此,工具需要明确职责、约束输入,并提供清晰的执行结果。

1. 职责明确

一个不太合理的工具示例:

代码片段Python
def process_data(data: str) -> str:
    ...

process_data 无法说明具体用途,data 也缺乏明确含义。

当 Agent 拥有多个类似工具时,模型很难准确选择。

更好的方式是:

代码片段Python
def get_order_status(order_id: str) -> str:
    """根据订单编号查询当前状态,不修改订单。"""
    ...

名称说明操作对象,描述补充具体用途和限制。

工具设计应遵循 SOLID 原则中的单一职责原则(SRP),让每个工具只负责一项明确的操作。

例如,查询订单与取消订单应分别定义为独立工具,避免将不同职责混在一个通用工具中。

2. 参数清晰

模型生成的参数不能直接当作可信输入。

例如:

代码片段Python
def search_orders(
    keyword: str,
    limit: int = 10,
) -> dict:
    ...

即使 limit 声明为整数,模型仍可能传入极大的数值。因此,工具内部还需要限制查询数量。

对于固定选项,可以使用 Literal:

代码片段Python
from typing import Literal
 
def get_orders(
    status: Literal["pending", "paid", "closed"],
    limit: int = 10,
) -> dict:
    ...

这样可以明确告诉模型,status 只能是三个值之一,并由工具框架校验。

复杂参数可以通过前面介绍的 Pydantic 定义取值范围和校验规则。

对于实际业务,还需要检查用户身份、操作权限和数据状态,不能只依赖参数类型。

3. 结果明确

工具执行后,应当让模型准确判断操作结果。

例如,查询失败时,不要只返回:

代码片段Text
Something went wrong.

更合理的方式是返回明确的错误信息:

代码片段JSON
{
  "ok": false,
  "code": "NOT_FOUND",
  "message": "订单不存在",
  "retryable": false
}

成功时返回:

代码片段JSON
{
  "ok": true,
  "order_id": "ORD-1001",
  "status": "paid"
}

这种结构可以让模型知道操作是否成功、失败原因是什么,以及是否需要重试。

对于修改类工具,还应返回修改前后的状态,方便判断操作是否真正完成。

4. 操作受控

不同类型的工具,需要不同的执行限制。

类型示例主要考虑
查询类查询订单权限、查询范围
修改类更新工单状态检查、重复修改
外部操作类付款、发送邮件人工确认、重复执行

例如,付款工具不能仅仅因为模型提供了金额和账户就直接执行。

程序必须检查用户权限、金额限制和付款状态。

模型负责提出操作请求,业务程序负责判断能否执行。

四、实战:可靠的工单工具

下面构建一个简单的故障工单 Agent。

它需要完成两个操作:查询指定工单,以及在权限和状态允许的情况下修改工单状态。

示例重点展示工具设计,其中的用到的数据会使用内存字典,以模拟从数据库获取。

安装依赖:

代码片段Bash
pip install -U deepagents "langchain[openai]"

完整代码如下:

代码片段Python
from dataclasses import dataclass
from typing import Literal
 
from deepagents import create_deep_agent
from langchain.tools import tool, ToolRuntime
 
 
# 模拟业务数据库
INCIDENTS = {
    "INC-1024": {
        "title": "API 响应延迟升高",
        "status": "open",
    }
}
 
 
# 应用程序提供的用户信息
@dataclass
class AppContext:
    user_id: str
    can_update: bool
 
 
@tool
def get_incident(incident_id: str) -> dict:
    """根据编号查询故障工单,不修改数据。"""
    incident = INCIDENTS.get(incident_id)
 
    if incident is None:
        return {
            "ok": False,
            "code": "NOT_FOUND",
        }
 
    return {
        "ok": True,
        "incident_id": incident_id,
        **incident,
    }
 
 
@tool
def update_incident_status(
    incident_id: str,
    new_status: Literal["investigating", "resolved"],
    runtime: ToolRuntime[AppContext],
) -> dict:
    """更新故障工单状态,仅在用户明确要求修改时调用。"""
 
    context = runtime.context
 
    # 检查操作权限
    if context is None or not context.can_update:
        return {
            "ok": False,
            "code": "FORBIDDEN",
        }
 
    incident = INCIDENTS.get(incident_id)
 
    if incident is None:
        return {
            "ok": False,
            "code": "NOT_FOUND",
        }
 
    old_status = incident["status"]
 
    # 状态相同,不重复修改
    if old_status == new_status:
        return {
            "ok": True,
            "changed": False,
            "status": old_status,
        }
 
    # 检查状态能否变更
    transitions = {
        "open": {"investigating"},
        "investigating": {"resolved"},
        "resolved": set(),
    }
 
    if new_status not in transitions[old_status]:
        return {
            "ok": False,
            "code": "INVALID_TRANSITION",
        }
 
    incident["status"] = new_status
 
    return {
        "ok": True,
        "changed": True,
        "old_status": old_status,
        "new_status": new_status,
        "updated_by": context.user_id,
    }
 
 
agent = create_deep_agent(
    model="openai:gpt-5.5",
    tools=[get_incident, update_incident_status],
    context_schema=AppContext,
    system_prompt=(
        "你是故障工单助手。"
        "修改前先核实工单信息。"
        "只有工具返回成功后,才能报告操作成功。"
    ),
)
 
result = agent.invoke(
    {
        "messages": [{
            "role": "user",
            "content": (
                "查询 INC-1024。"
                "如果状态是 open,"
                "就改为 investigating。"
            ),
        }]
    },
    context=AppContext(
        user_id="operator-01",
        can_update=True,
    ),
)
 
print(result["messages"][-1].content)
print(INCIDENTS)

这段代码有几个值得关注的地方。

首先,查询与修改分开。

get_incident 负责读取,update_incident_status 负责修改。两个函数都使用 @tool 定义,然后通过 tools 注册到 Agent。

其次,权限由程序提供。

context_schema=AppContext 声明上下文类型。执行时,应用程序通过 context 传入用户信息。

工具通过 runtime.context 读取这些信息,并检查 can_update。

ToolRuntime 用于在工具内部获取运行时信息,例如当前用户、Agent State 和其他应用数据。

context_schema 用于定义应用程序传给 Agent 的上下文结构。

它们通常配合使用。应用程序通过 context 传入用户信息,工具通过 runtime.context 读取。

与普通工具参数不同,runtime: ToolRuntime[...] 由框架自动提供,不会暴露给模型。模型无法通过工具调用参数直接填写其中的用户身份或权限。

需要注意,示例中的用户身份和权限是模拟数据。实际项目应根据登录状态和权限系统生成,不能直接相信客户端提交的内容。

最后,状态变更受到限制。

工单只允许按以下顺序变化:

代码片段Text
open
  ↓
investigating
  ↓
resolved

不符合规则的操作会返回 INVALID_TRANSITION。如果工单已经处于目标状态,也不会重复修改。

可以看到,模型负责判断何时调用工具,具体的权限和业务规则则由工具代码执行。

五、实际项目中的注意事项

上面的示例已经包含基本的权限和状态检查。真正接入业务系统时,还需要解决几个问题。

1. 超时与重复执行

外部 API 可能超时,但超时不一定意味着操作失败。

例如,付款请求可能已经执行,只是响应没有正常返回。此时直接重试,可能造成重复付款。

对于这类操作,应使用唯一请求编号记录执行结果。收到重复请求时,先检查之前的处理情况。

对于数据库修改,还需要使用事务和条件更新,避免多个请求同时修改同一条数据。

2. 工具范围与人工确认

不要向 Agent 提供超出任务需要的工具。

例如,只负责资料研究的 Agent,通常不需要删除数据或修改订单的能力。

对于删除、付款等重要操作,可以使用 Deep Agents 的 interrupt_on 配置执行前确认:

代码片段Python
from langgraph.checkpoint.memory import MemorySaver
 
agent = create_deep_agent(
    model="openai:gpt-5.5",
    tools=[delete_order],
    interrupt_on={
        "delete_order": True,
    },
    checkpointer=MemorySaver(),
)

这里的 delete_order 是预先定义好的工具。

interrupt_on 指定哪些工具需要人工确认;checkpointer 保存执行状态,以便审核后继续运行。实际调用和恢复时,还需要使用相应的 thread_id。

示例使用内存保存状态。生产环境应使用可靠的持久化存储。

人工确认不能代替业务权限检查。即使审核通过,工具仍应检查操作是否合法。

3. 返回数据的数量

工具查询结果可能进入模型上下文。

如果一次返回几万条记录,不仅消耗大量 Token,也会增加模型处理信息的难度。

因此,查询工具应支持筛选、分页和数量限制。

例如,先返回工单编号、标题和状态,确定目标后再读取完整信息。

六、工具的测试与验证

工具开发完成后,需要同时检查函数本身和 Agent 的调用行为。

函数测试主要关注参数错误、权限不足、数据不存在、状态不合法以及重复执行等情况。

Agent 测试则关注另外三个问题:

工具是否选对? 用户只要求查询时,Agent 不应该调用修改工具。

参数是否正确? 调用时是否使用了正确的工单编号和目标状态。

结果是否可信? 工具返回失败后,Agent 不能错误地报告操作成功。

在实际项目中,最好准备一组固定任务,每次修改工具定义或提示词后重新测试,观察任务完成情况是否发生变化。

总结

Tools 让 Agent 具备访问外部系统和执行操作的能力。

在 Deep Agents 中,开发者可以直接注册普通 Python 函数,也可以通过 @tool 定义工具。模型负责选择工具并生成参数,实际操作由运行环境完成。

设计工具时,重点是职责明确、参数清晰、结果可判断。涉及数据修改时,还需要落实权限和业务规则。

模型可以决定尝试做什么,但真正能做什么、怎样执行,必须由程序控制。