跳转至

在 LangChain 这类 Agent 框架中,新增一种 Tool 类型时需要手动到处注册,有没有更优雅的方案?

手动注册 Tool 的痛点我经历过,一个项目里对接了 20 多个工具,每次新人加一个 Tool 得改四处:工具类文件、中央注册表、init.py 的导入、以及路由分发代码。一旦漏掉一个,调半天找不到工具,非常难受。

优雅解决的核心就是:让工具类自己“报到”,而不是人工替它报名。在 Python 里想做到这个,相当于在类被定义的瞬间,偷偷执行一段注册逻辑。


🧭 原理:类定义时自动触发注册

Python 的类创建有两个钩子能帮我们截胡:

  • 元类 new:类对象生成前介入,能力最强,能改类结构。

  • init_subclass:类对象生成后立刻调用,轻量简洁。

对自动注册这种“附加动作”,init_subclass 通常更合适——它不会改变类的类型,避免多重继承冲突,而且语义就是“当有子类被定义时通知我”。

整个自动注册流程用图表示就是这样:

image.png

上层 Agent 调度时直接从 ToolRegistry 里按名字取类实例化即可,完全不碰手动注册。


🛠️ 落地实现:基于 init_subclass 的自注册 Tool 基类

我们写一个 AutoTool 基类,任何工具只要继承它,自动注册到统一的 ToolRegistry

from typing import Dict, Type

class ToolRegistry:
    """全局工具库,只读访问"""
    _tools: Dict[str, Type] = {}

    @classmethod
    def register(cls, tool_cls: Type):
        name = getattr(tool_cls, 'name', tool_cls.__name__.lower())
        if not name.isidentifier():
            raise ValueError(f"Tool name '{name}' is not a valid identifier")
        if name in cls._tools:
            raise ValueError(f"Tool '{name}' already registered")
        cls._tools[name] = tool_cls

    @classmethod
    def get(cls, name: str):
        return cls._tools.get(name)

    @classmethod
    def all(cls):
        return dict(cls._tools)

class AutoTool:
    """继承这个类,自动注册"""
    name: str = ""         # 子类定义工具名
    description: str = ""  # 可选描述

    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        # 跳过基类本身
        if cls.__name__ == 'AutoTool':
            return
        if not hasattr(cls, 'execute') or not callable(cls.execute):
            raise TypeError(f"Tool '{cls.__name__}' must implement 'execute' method")
        ToolRegistry.register(cls)

现在开发者只用写工具类,什么注册代码都不用管:

class SearchTool(AutoTool):
    name = "search"
    description = "搜索网页内容"
    def execute(self, query: str):
        return f"搜索结果: {query}..."

class CalcTool(AutoTool):
    name = "calculator"
    def execute(self, expression: str):
        return eval(expression)

# 工具就已经自动在注册表里了
print(ToolRegistry.all())   # {'search': SearchTool, 'calculator': CalcTool}

和 LangChain 的集成也很顺滑:我们可以在框架初始化时,把所有自注册工具包装成 LangChain 的 Tool 对象:

from langchain.tools import Tool as LcTool

def build_langchain_tools():
    return [
        LcTool.from_function(
            func=tool_cls().execute,
            name=tool_cls.name,
            description=tool_cls.description
        )
        for tool_cls in ToolRegistry.all().values()
    ]

这个模式把“注册”和“使用”完全解耦,新增工具只关心一个文件里的业务逻辑,不再到处改配置。