AI AgentTechnical Deep Dive

自定义 Tool:从函数封装到带状态写回的执行接口

发布时间2026/03/01
分类AI Agent
预计阅读10 分钟
作者吴长龙
*

好工具不是“能被模型调用”就够了,真正决定效果的是接口边界、参数约束、错误处理,以及它能否和运行时上下文、安全策略、状态更新配合工作。

01.Tool 的关键不在“怎么写”,而在“怎么约束”

很多人第一次做 Agent 工具,会把重点放在代码形式上:

  • 是不是要用装饰器
  • 要不要写 Pydantic schema
  • 是不是要接 ToolNode

这些都重要,但它们还不是核心。真正决定工具是否好用的,是下面四件事:

  • 这个工具职责是否单一
  • 参数是否稳定、可校验
  • 失败时是否能被系统理解和处理
  • 是否能接入运行时上下文、记忆和状态更新

如果这些边界没设计好,再多工具也只是让模型更容易选错、系统更难排障。

所以我更倾向于把 Tool 看成“面向模型的业务接口”,而不是“给模型调用的 Python 函数”。

02.最简单的工具:`@tool` 已经够很多场景起步

LangChain 当前官方文档里,最基础的工具定义方式仍然是 @tool。它适合做职责清晰、参数不复杂的动作。

python snippetpython
from langchain.tools import tool


@tool
def get_weather(city: str) -> str:
    """查询指定城市天气。"""
    return f"{city} 当前天气晴。"

这个写法的优点是直接:

  • docstring 会成为工具描述
  • 参数签名会自动转成工具 schema
  • 很适合起一个最小 demo

但如果你准备把它用进真实系统,下一步通常不是“加更多工具”,而是先把工具描述和参数边界写清楚。

03.当参数变复杂时,要尽早结构化

一旦工具开始接收多个字段、可选项或枚举值,就不该继续依赖模糊的自由文本参数了。更稳妥的方式是把输入 schema 明确下来。

python snippetpython
from typing import Literal
from pydantic import BaseModel, Field
from langchain.tools import tool


class SearchTicketsInput(BaseModel):
    query: str = Field(description="要搜索的问题描述")
    limit: int = Field(default=5, ge=1, le=20, description="返回数量")
    source: Literal["incident", "knowledge_base"] = Field(
        default="incident",
        description="查询来源"
    )


@tool(args_schema=SearchTicketsInput)
def search_tickets(query: str, limit: int = 5, source: str = "incident") -> str:
    """搜索工单或知识库中的相关记录。"""
    return f"从 {source} 中找到 {limit} 条与 {query} 相关的记录"

这里最有价值的不是“写法更高级”,而是:

  • 模型更容易知道每个字段该填什么
  • 校验失败能更早暴露
  • 工具契约更适合长期维护

经验上,只要工具开始进入真实业务链路,结构化输入几乎总是值得的。

04.不要把环境依赖硬编码进工具

工具真正进入系统后,通常需要一些运行时依赖,例如:

  • 当前用户 ID
  • 数据库连接
  • 环境配置
  • 长期记忆 store

LangChain 当前官方 runtime 文档推荐的做法,是通过 ToolRuntime 注入这些信息,而不是写全局变量或从环境里到处读。

python snippetpython
from dataclasses import dataclass
from langchain.tools import tool, ToolRuntime


@dataclass
class Context:
    user_id: str


@tool
def fetch_user_preferences(runtime: ToolRuntime[Context]) -> str:
    """读取当前用户偏好。"""
    user_id = runtime.context.user_id

    if runtime.store:
        memory = runtime.store.get(("users",), user_id)
        if memory:
            return memory.value["preferences"]

    return "用户未设置偏好"

这个模式的好处非常实际:

  • 工具更容易测试
  • 不依赖全局状态
  • 同一个工具可以服务不同用户或不同环境
  • 后续接入长期记忆会更自然

对工程项目来说,这类“可注入上下文”往往比多一个工具功能更重要。

05.工具不只会“返回字符串”,也可以参与状态更新

很多入门示例都默认工具返回一段字符串,然后交给模型继续读。但在 LangGraph 场景里,工具还有另一类更值得关注的能力:直接影响图状态。

当前官方 tools 文档提到,工具返回值可以是:

  • 纯字符串
  • 结构化对象
  • Command

当你只是想给模型看结果时,字符串就够了;当你希望工具直接修改状态或触发流程变化时,Command 会更合适。

python snippetpython
from langgraph.types import Command
from langchain.tools import tool


@tool
def classify_priority(score: int) -> Command:
    """根据分数设置工单优先级。"""
    priority = "high" if score >= 80 else "normal"
    return Command(update={"ticket_priority": priority})

这个思路对复杂工作流尤其重要,因为它意味着工具不再只是“供模型阅读的结果”,而是可以成为状态迁移的一部分。

06.在 LangGraph 里,工具执行最好交给 `ToolNode`

如果你的系统已经进入 LangGraph 编排阶段,通常不需要自己再写一层通用工具执行器。官方 tools 文档推荐直接使用 ToolNode

它的价值主要在于:

  • 自动执行模型发出的工具调用
  • 支持并行工具执行
  • 统一错误处理
  • 自动注入状态和运行时上下文

一个最小骨架如下:

python snippetpython
from langchain.tools import tool
from langgraph.prebuilt import ToolNode, tools_condition
from langgraph.graph import StateGraph, MessagesState, START


@tool
def search(query: str) -> str:
    """搜索资料。"""
    return f"Results for: {query}"


tool_node = ToolNode([search], handle_tool_errors=True)


def call_llm(state: MessagesState):
    ...


builder = StateGraph(MessagesState)
builder.add_node("llm", call_llm)
builder.add_node("tools", tool_node)
builder.add_edge(START, "llm")
builder.add_conditional_edges("llm", tools_condition)
builder.add_edge("tools", "llm")
graph = builder.compile()

如果你需要高度定制的执行逻辑,当然也可以自己写节点。但在绝大多数常规 Agent 工作流里,ToolNode 已经是一个更稳的默认起点。

07.自定义 Tool 最容易踩的几个坑

1. 工具职责太大

例如一个工具同时负责搜索、过滤、写库、发通知。模型很难选对,也很难在失败后恢复。

2. 参数自由度过高

工具表面上越“通用”,实际越容易被模型误用。能用枚举、默认值和 schema 收紧,就尽量收紧。

3. 直接暴露高风险副作用

删除数据、发正式通知、改生产配置这类操作,不能因为“工具能写出来”就直接开放给模型。必须配合权限、确认和审计。

4. 把错误处理留给模型猜

错误信息至少要做到:

  • 能区分输入错误还是执行错误
  • 能让上层工作流判断是否需要重试
  • 不把内部异常原样暴露给最终用户

这一步做不好,工具一多,系统稳定性会明显下降。

08.一个更可靠的设计顺序

如果你正在给 Agent 系统加自定义工具,我更建议按这个顺序来:

  • 先定义业务动作,而不是先想函数名
  • 再收敛参数 schema
  • 再决定是否需要运行时上下文或 store
  • 最后再接入 ToolNode 或 agent

这样做的结果通常是:工具数量少一些,但每个都更稳定、更容易解释。

09.总结

Tool 的本质不是“让模型多一个能力点”,而是给系统增加一个新的执行接口。对长期维护的 Agent 项目来说,真正重要的是:

  • 接口边界清晰
  • 输入可校验
  • 错误可处理
  • 上下文和状态可注入

只有把这些底层约束做好,工具系统才会成为 Agent 的能力扩展器,而不是新的不稳定来源。

10.参考资料