系列第二篇。本文解析 Hermes 工具注册表的架构哲学:注册表模式不只让代码更优雅,它是一个关于"谁对谁负责"的宣言,深刻影响了代码库的变更模式、工具扩展的摩擦结构,以及整个系统对依赖倒置原则的贯彻方式。
在理解 Hermes 工具注册表的设计之前,先想象另一种方式——枚举模式,也就是早期实现里常见的做法。
枚举模式长这样:
# tools/__init__.py
from tools.web_tools import web_search, web_extract
from tools.file_operations import read_file, write_file, patch, search_files
from tools.terminal_tool import terminal, process
from tools.vision_tools import vision_analyze
# ... 继续列出所有工具
每次新增工具,必须在这个文件里添加一行 import。每次删除工具,必须在这里删除对应行。这个文件对整个工具库的每个成员都有依赖——它知道所有人,所有人都被它知道。
注册表模式长这样:
# tools/registry.py
# 什么都不 import,什么工具也不知道
# tools/web_tools.py
from tools.registry import registry
registry.register(name="web_search", ...)
registry.register(name="web_extract", ...)
注册表文件对工具实现文件一无所知。工具实现文件主动找到注册表报到。这是一个方向的完全反转。
软件工程教科书里的依赖倒置原则(Dependency Inversion Principle)通常这样表述:高层模块不应依赖低层模块,两者都应依赖抽象。在 Hermes 里,这个原理具体体现为:
枚举模式的依赖方向:tools/__init__.py(高层)→ 每个工具文件(低层)。高层直接依赖低层。新增一个工具,必须修改高层文件;删除一个工具,同样必须修改高层文件;每次 merge PR,如果两个人同时新增工具,会在 tools/__init__.py 产生 merge conflict。
注册表模式的依赖方向:每个工具文件(低层)→ tools/registry.py(抽象层)。高层(model_tools.py)依赖注册表接口,不依赖任何具体工具。新增工具只需要创建新文件和修改 toolsets.py;删除工具只需要删除文件;两个人同时新增工具,merge conflict 发生在 toolsets.py 而不是中央索引文件——而且 toolsets.py 里的冲突通常是两行追加,而不是交错修改,解决起来几乎是机械的。
这个反转改变了整个代码库的变更模式。理解这一点,是理解为什么注册表模式是一个架构哲学宣言而非单纯的代码组织技巧的关键。
tools/registry.py 里的 discover_builtin_tools() 函数实现了自动发现:
def discover_builtin_tools(tools_dir: Optional[Path] = None) -> List[str]:
tools_path = Path(tools_dir) if tools_dir is not None else Path(__file__).resolve().parent
module_names = [
f"tools.{path.stem}"
for path in sorted(tools_path.glob("*.py"))
if path.name not in {"__init__.py", "registry.py", "mcp_tool.py"}
and _module_registers_tools(path)
]
...
关键的 _module_registers_tools() 函数用 AST 解析(而不是 import)来判断一个文件是否包含顶层的 registry.register() 调用: