langchain tools源码解析以及扩展
a: intb: int# 支持字段校验 或 复杂嵌套"""加法定制"""方面自定义 ToolExecutor.register_tool装饰对象Python函数工具类(通常继承BaseModel)内部逻辑封装:函数、元信息、参数schema → Tool对象需显式传给Agent注册:类进全局map,参数结构自动提取调用入口由Agent调度由ToolExecutor.execute_tool统一入
提示:本章大量涉及到装饰器概念(有参装饰器),如有不清晰的地方,请看本人fluent python 闭包与装饰器章节。
一、概念提醒
@tool
的任务:
把普通 Python 函数变成能被 Agent 理解与调用的信息化对象(Tool)
整个流程包含:
- 解析函数的名字、文档、参数类型等
- 包装成标准 Tool 对象
- 实现参数校验、序列化、调用分发
二、最小例子:一步步还原其实现
我们写一个最简单的 @tool 例子:
from langchain.tools import tool
@tool
def add(a: int, b: int) -> int:
"""相加"""
return a + b
我们来追踪它背后的源码链路。
三、源码追踪与讲解
1、@tool 装饰器本身(源码解读)
langchain.tools.tool
的定义(大致代码结构和官方一致):
def tool(_func=None, *, args_schema=None, return_direct=False, **kwargs):
def decorator(func):
return Tool.from_function(
func,
args_schema=args_schema,
return_direct=return_direct,
**kwargs,
)
if _func is None:
return decorator
else:
return decorator(_func)
分析
- 支持无参数和有参数两种装饰用法
- 真正工作由
Tool.from_function
完成
2、Tool 对象创建流程
官方 Tool 定义路径:langchain/tools/base.py
及 langchain/tools/__init__.py
重点方法:Tool.from_function
class Tool(BaseTool):
# ... 其他代码
@classmethod
def from_function(
cls, func: Callable, name: Optional[str]=None,
description: Optional[str]=None,
args_schema: Optional[Type[BaseModel]]=None,
return_direct: bool=False,
**kwargs,
):
# 自动获取函数名字和文档说明
tool_name = name or func.__name__
tool_description = description or func.__doc__ or ""
# 关键点1:自动生成参数schema
if args_schema is None:
args_schema = create_schema_from_function(func)
# 关键点2:封装函数执行逻辑
return cls(
name=tool_name,
func=func,
args_schema=args_schema,
description=tool_description,
return_direct=return_direct,
**kwargs,
)
关键点解释:
-
参数定义自动生成
默认情况下,会尝试从函数的参数签名和类型注解动态构造 pydantic schema。这点很重要,决定了 agent 能不能理解参数类型。
-
函数封装
你的原始函数(例如 add)会直接挂载到 Tool 对象的
func
属性上,后续通过 Tool 对象统一调度。这一句 return cls(…) 就是标准的“封装与注册”,把你所有和Agent用工具相关的信息都集中了起来,变成LangChain Agent可用、可被AI推理时发现和交互的标准对象。
3、参数 schema 的自动构建
核心函数:create_schema_from_function
这段最难,但很本质。LangChain 依赖 pydantic 动态生成输入校验模型。
伪代码复现:
def create_schema_from_function(func):
# 获取函数参数签名与注解
sig = inspect.signature(func)
fields = {}
for name, param in sig.parameters.items():
# 判断是否有注解类型,否则用 Any
anno = param.annotation if param.annotation is not inspect.Parameter.empty else Any
default = param.default if param.default is not inspect.Parameter.empty else ...
fields[name] = (anno, default)
# 动态创建 Pydantic Model,用于参数解析/校验
schema = pydantic.create_model(
func.__name__ + "Schema",
**fields
)
return schema
直观结果:
你如果写了 def add(a: int, b: int) -> int
, 参数类型annotation会被抽出来,做出:
class AddSchema(BaseModel):
a: int
b: int
这样后面 LLM 或 Agent 只要用 json {a: 3, b: 7}
输入,就能用 Pydantic 安全校验并自动转换调用。
4、Tool 执行流程(用例演示)
假如 Agent 获得如下工具注册表:
tools = [add] # 实际是 [Tool(...)]对象
假设要调用 add 工具,并校验参数类型,怎么用呢?
模拟内部调用过程:
# 1、从 tool 列表找到 Tool 对象
t = tools[0]
# 2、让 pydantic 校验参数类型并实例化
args = {"a": 2, "b": "4"} # 注意,类型不对也没关系,pydantic 会自动转换
validated = t.args_schema(**args)
print(validated) # AddSchema(a=2, b=4)
# 3、安全调用用户函数
result = t.func(**validated.dict())
print(result) # 输出: 6
自动类型转换与校验机制:
假如用户输入的数据类型不对(如 b=“4” 是字符串),pydantic 会自动帮你转换成 int。如果语义混乱(如a不能为字符串),那会自动抛出参数校验异常。
5、结论汇总
- @tool 实际会返回一个 Tool 类对象,而非原始函数本身!
- Tool 内部自动生成 pydantic 参数Schema,提供标准化 json 参数解析/校验。
- Tool 函数调度封装,LLM/Agent 调用时无需直接解包参数。
- 可以指定返回形式,对构建复杂Agent流程很有帮助。
四、对比说明演示
不用 @tool
,你只能这样写:
def add(a: int, b: int) -> int:
...
# Agent 不知道怎么自动传参
用了 @tool
,你能获得:
t = add # 实际是 Tool 对象
args = {"a": 5, "b": 8}
validated = t.args_schema(**args)
output = t.func(**validated.dict())
而Agent内部正是这样调用你的工具!
五、扩展(自定义 args_schema)
如果你想干预参数校验过程,可以自定义 Pydantic schema 用作 Tool 的 args_schema:
from pydantic import BaseModel
class MyAddArgs(BaseModel):
a: int
b: int
# 支持字段校验 或 复杂嵌套
@tool(args_schema=MyAddArgs)
def add(a, b):
"""加法定制"""
return a + b
六、小结复盘
- 包装过程:Python函数→@tool装饰→自动/手动Pydantic schema→Tool
- 参数关键:标准化、注释和pydantic;为后续自动调用和安全校验铺路
- 源码关键位置:
tool
装饰的from_function、create_schema_from_function的动态参数schema构造 - 实用好处:代理智能推理能根据描述、参数schema自动用你的函数,无需再手写参数解析等无聊工作!**
七、LangChain @tool 与自己实现工具注册装饰器
class ToolExecutor:
"""工具执行器"""
tools = {} # 存储工具的逻辑,映射工具名称 -> 工具类
tool_metadata = {} # 存储工具的元数据(包含描述和参数模型)
@staticmethod
def register_tool(name: str, description: str):
"""注册工具的装饰器"""
def decorator(cls):
# 确保类继承自 BaseModel
if not issubclass(cls, BaseModel):
raise ValueError(f"工具 {name} 必须继承自 BaseModel!")
if not hasattr(cls, "run") or not callable(getattr(cls, "run")):
raise ValueError(f"工具 {name} 缺少有效的 `run` 方法!")
# 注册工具到工具列表中
ToolExecutor.tools[name] = cls
# 注册工具元数据,包括描述和参数结构
ToolExecutor.tool_metadata[name] = {
"介绍": description,
"需要传入的参数": cls.model_json_schema()["properties"]
}
return cls # 返回工具类
return decorator
@staticmethod
def execute_tool(func_name: str, params: Dict) -> Any:
"""执行指定工具"""
if func_name not in ToolExecutor.tools:
return f"工具 {func_name} 未注册"
# 获取工具类并执行
tool_cls = ToolExecutor.tools[func_name] # 获取工具类
validated_params = tool_cls(**params) # 验证参数(工具类本身作为参数模型)
return tool_cls.run(**validated_params.model_dump()) # 调用工具逻辑
@staticmethod
def export_metadata() -> Dict[str, Dict]:
"""导出工具元数据"""
return ToolExecutor.tool_metadata
@ToolExecutor.register_tool(
name="ip_is_external",
description="查询IP是否是属于内网IP"
)
class IPIsInternalTool(BaseModel):
"""IP查询工具"""
ips: list[str] = Field(...,
description="从输入内容与历史信息中提取出去重后要查询的IP地址列表。IP地址是由数字和点构成的格式,例如:192.168.1.1")
@staticmethod
def run(ips: list[str]) -> dict:
baidu_ips = []
external_ips = []
for ip in ips:
try:
# 将输入的IP地址转换为IPv4Address对象
ip_obj = ipaddress.ip_address(ip)
# 如果IP地址不是私有地址,则添加到public_ips列表
if not ip_obj.is_private:
external_ips.append(ip)
else:
baidu_ips.append(ip)
except ValueError:
# 如果IP地址无效,可以选择忽略或记录
print(f"{ip} 是无效的IP地址")
return {"baidu_ips": baidu_ips, "external_ips": external_ips}
1. return cls(…) 是什么?
它是【调用类构造器生成实例】的写法,可以理解为:
“把你传进来的参数,都按设计好的格式和流程,重新组合一下,打包成一个全新的‘标准件’(对象)。”
比如:
🚗“造车”类比
假设你要生产一辆小车:
class Car:
def __init__(self, brand, color, horsepower):
self.brand = brand
self.color = color
self.hp = horsepower
def make_car(brand, color, horsepower):
# 这里就类似于 return cls(...)
return Car(brand=brand, color=color, horsepower=horsepower)
car1 = make_car("特斯拉", "红", 800)
print(car1.brand, car1.color, car1.hp) # 特斯拉 红 800
make_car
传入一堆参数,内部通过Car(...)
把原始的零碎信息组合“封装注册”为一个可用的Car对象。
所以,return cls(…) 是把“原始信息”→“规范对象”这一步的实现方式。
2. @tool装饰器 和 你自定义 ToolExecutor.register_tool 的对比
二者的核心区别
方面 | LangChain @tool | 自定义 ToolExecutor.register_tool |
---|---|---|
装饰对象 | Python函数 | 工具类(通常继承BaseModel) |
内部逻辑 | 封装:函数、元信息、参数schema → Tool对象 需显式传给Agent |
注册:类进全局map,参数结构自动提取 |
调用入口 | 由Agent调度 | 由ToolExecutor.execute_tool统一入口调度 |
注册方式 | 没有全局注册(只生成对象实例) | 有全局注册(map写入) |
运行时扩展 | 适合分布式、组合 | 适合全局查找、集中执行调度 |
参数处理 | 自动抽取签名/注解/文档成Pydantic schema | 直接以BaseModel类为参数模型 |
补图说明:
@tool ToolExecutor.register_tool
def foo(x:int): ... ↓ ↓
↓ @register_tool class MyTool(BaseModel):
返回 Tool(name,func,...) (全局map写入) ... def run(...): ...
↓ ↓ ↓
显式传入Agent,列表工具 ToolExecutor.tools[name]=cls ToolExecutor.execute_tool名字调度运行
↓ ↓ ↓
agent自动分发、调用(schema校验) 统一调度,参数校验、元数据导出
实战例子:两者对比
LangChain风格
@tool
def multiply(x: int, y: int) -> int:
"""相乘"""
return x * y
agent = initialize_agent([multiply], ...)
# 提供给Agent显式调度,没有全局注册
ToolExecutor风格
@ToolExecutor.register_tool(name="multiply", description="两个数相乘")
class MultiplyTool(BaseModel):
x: int
y: int
@staticmethod
def run(x, y):
return x * y
res = ToolExecutor.execute_tool("multiply", {"x":3, "y":5}) # 结果: 15
# 注册后可统一按名字调度,无需显式传对象
总结
return cls(...)
:把各种属性参数打包成【标准对象/实例】,比如Tool、Car、Product等,“物理上的封装和标准化”- LangChain @tool:封装为对象,灵活组合(哪怕跨文件),但注册是你自己传入Agent决定的
- ToolExecutor.register_tool:自动化全局注册,像插件表,所有工具类都集中在统一map、支持按名调用和批量导出元数据
- 选择方式取决于你的“调用组织模式”和系统需求

GitCode 天启AI是一款由 GitCode 团队打造的智能助手,基于先进的LLM(大语言模型)与多智能体 Agent 技术构建,致力于为用户提供高效、智能、多模态的创作与开发支持。它不仅支持自然语言对话,还具备处理文件、生成 PPT、撰写分析报告、开发 Web 应用等多项能力,真正做到“一句话,让 Al帮你完成复杂任务”。
更多推荐
所有评论(0)