第 8 章 · 03 MethodRegistry 与 PluginRegistry 可插拔机制


第 8 章 · 03 MethodRegistry 与 PluginRegistry 可插拔机制

本节摘要:01、02 两节的"一行切换"不是魔法,机关是注册表。Semantica 有两套机关:其一 MethodRegistry——几乎每个子模块(vector_store/graph_store/kg/ontology/export/…共 14 处)都自带一个 registry.py,同一能力多种实现按名注册、按名取用(如抽取的 ml/pattern/llm 三方法);其二 core/plugin_registry.pyPluginRegistry——从目录动态发现、加载、递归解依赖、初始化外部插件。本节拆注册表模式的实现(注册→发现→分发),回答"polyglot 为什么可行",并示范自己写扩展的两条正门,最后与 vnpy/Reasonix 的注册表模式对照。

内容来源:原项目源码 semantica/vector_store/registry.pysemantica/core/registry.pysemantica/core/plugin_registry.pysemantica/core/orchestrator.pysemantica/core/methods.pysemantica/semantic_extract/ner_extractor.pysemantica/vector_store/methods.py

⚠️ 注意MethodRegistry 有两种变体——vector_store 版是实例单例method_registry = MethodRegistry(),vector_store/registry.py:155),core 版是类方法@classmethod 直接挂在类上,core/registry.py:56)。写扩展前先看清你面对的是哪一种,注册姿势并不相同。

学习目标

阅读完本节,你应当能够:

  1. 手写一个 30 行内的 MethodRegistry:task 二级字典 + register/get/list_all/has。
  2. 说出 PluginRegistry 的插件准入条件(initialize/execute 两方法)与四步加载流程。
  3. 解释发现插件的两种类名嗅探策略与模块级元数据约定。
  4. 复述注册表如何支撑"抽取三方法切换"与"存储多后端切换"。
  5. 用"函数注册表 vs 插件生命周期管理"区分 Semantica 与 vnpy/Reasonix 的注册表用法。

一、MethodRegistry:30 行的按名分发

先看最小实现。vector_store/registry.py:45-79:

45 class MethodRegistry: 46 """Registry for custom vector store methods.""" 48 def __init__(self): 49 self._registry: Dict[str, Dict[str, Callable]] = { 50 "store": {}, "search": {}, "index": {}, 51 "hybrid_search": {}, "metadata": {}, "namespace": {}, 52 } 59 def register(self, task, method_name, method_func, **metadata): 71 if task not in self._registry: 71 raise ValueError(f"Unknown task type: {task}") 74 if not callable(method_func): 74 raise ValueError("method_func must be callable") 78 method_func._registry_metadata = metadata 79 self._registry[task][method_name] = method_func

数据结构就是二级字典:第一级按任务类型(store/search/index/hybrid_search/metadata/namespace 六类),第二级按方法名,值是可调用对象。register 有两道校验(task 白名单 + callable 检查),外加一个巧思——78 行把 **metadata 直接挂到函数对象属性 _registry_metadata 上,之后 get_metadata(137-151 行)原样取回。查询侧全家福:get(92 行,找不到返回 None 不抛错)、list_all(107 行,支持按 task 过滤)、has(124 行)、unregister(81 行)。文件尾造出模块级单例(155 行):

154 # Global method registry instance 155 method_registry = MethodRegistry()

同一个类名在全仓出现十余次:conflicts、deduplication、core、embeddings、export、graph_store、kg、normalize、ontology、ingest、semantic_extract、split、vector_store、triplet_store 各有一份 registry.py——每个管线段都有自己的方法注册表,task 分类随领域变(如 core 版是 pipeline/knowledge_base/orchestration/lifecycle,core/registry.py:49-54),骨架一模一样。默认实现也在加载时自注册进表(core/methods.py:425-427):

425 method_registry.register("knowledge_base", "default", build_knowledge_base) 426 method_registry.register("pipeline", "default", run_pipeline) 427 method_registry.register("orchestration", "default", initialize_framework)

二、按名切换的实战:抽取的三种方法

注册表最有感的应用是同一能力的多实现切换。第 3 章的 NER 抽取器就是活例(ner_extractor.py:53-66,docstring):

53 >>> # Using ML method (default) 54 >>> extractor = NERExtractor(method="ml", model="en_core_web_sm") ... 57 >>> # Using LLM method 58 >>> extractor = NERExtractor(method="llm", provider="openai", llm_model="gpt-4") ... 62 >>> extractor = NERExtractor(method="huggingface", ...) ... 66 >>> extractor = NERExtractor(method=["llm", "ml", "pattern"], ensemble_voting=True)

method 参数接收 ml(spaCy 统计模型)/pattern(规则)/llm(大模型)/huggingface 之一,甚至接收一个列表做集成投票(ensemble_voting)。构造时分发逻辑按名分派(382 行 elif method_name == "llm" 一类分支)。对调用方来说,NERExtractor(method="ml")NERExtractor(method="llm") 返回同构结果——注册表的收益正是这个:实现的可变性被"名字"这层间接性吸收。方法注册表又把这层间接性开放给社区:get_vector_store_method(task, name)(vector_store/methods.py:435)与 list_available_methods(449 行)是官方出口,任何人 method_registry.register("store", "my_store", my_func) 后即可按名取用、按名发现。

三、PluginRegistry:从文件系统到生命周期

MethodRegistry 管函数,PluginRegistry(core/plugin_registry.py:61)管有生命的类实例。配套数据类先行:PluginInfo(38 行,name/version/plugin_class/dependencies/capabilities)与 LoadedPlugin(52 行,info/instance/config/loaded_at)。准入门槛(register_plugin,145-158 行):

147 if not inspect.isclass(plugin_class): 148 raise ValidationError( 149 f"Plugin {...} must be a class, got {type(plugin_class)}") 152 required_methods = ["initialize", "execute"] 153 for method in required_methods: 154 if not hasattr(plugin_class, method): 155 raise ValidationError( 156 f"Plugin {...} must have {method}() method")

插件必须是带 initialize()/execute() 方法的类——duck typing 版接口约定,注册时即校验依赖可发现性(_validate_dependencies,643 行)。load_plugin(183-318 行)四步走:

216 if plugin_name in self.loaded_plugins: 217 # 已加载直接返回 —— 天然单例 219 return self.loaded_plugins[plugin_name].instance ... 247 for dep_name in plugin_info.dependencies: 248 if dep_name not in self.loaded_plugins: 250 self.load_plugin(dep_name) # 递归加载依赖 ... 259 plugin_instance = plugin_class(**config) ... 274 if hasattr(plugin_instance, "initialize"): 276 plugin_instance.initialize()

已加载直接返回(幂等)→ 未注册则现场发现(223-237 行)→ 递归加载依赖(243-250 行,load_plugin 自调用,依赖的依赖照办)→ 实例化 + 调 initialize()(254-280 行,带 config 失败还会退化成无参重试一次)。卸载(unload_plugin,320 行)也讲礼貌:先调插件的 cleanup()/close() 再摘除。

发现机制(_load_plugin_from_file,541-641 行)用 importlib.util.spec_from_file_location 动态导入 .py 文件,然后两种策略嗅探插件类:

583 # Strategy 1: Try common naming patterns 584 class_names = [ 585 plugin_name.capitalize(), # "my_plugin" -> "My_plugin" 586 plugin_name.capitalize() + "Plugin", # -> "My_pluginPlugin" 587 plugin_name.replace("_", "").capitalize(), 588 ] ... 600 # Strategy 2: Look for any class with "Plugin" in name 601 if plugin_class is None: 602 for name, obj in inspect.getmembers(module): 603 if inspect.isclass(obj) and "Plugin" in name:

元数据(description/author/version/dependencies/capabilities)从模块级属性提取(619-627 行)。结论:写插件只要一个 .py 文件加命名约定,零注册代码。编排器在构造时持有全局注册表(orchestrator.py:82):self.plugin_registry = PluginRegistry()

四、polyglot 为什么可行:两层机关的合作

回头看本章标题的问题——8 向量库、4 图库、5 RDF 库为何能一行切换?答案是"可插拔"被拆成了两层互补的机关:

  1. 统一抽象接口(01、02 节的门面):VectorStore/GraphStore/TripletStore 规定 add/search/delete/query 的形状,用 hasattr/inspect 做鸭子适配(vector_store.py:507-529)。这是调用侧的稳定性
  2. 注册表分发(本节):白名单校验(SUPPORTED_BACKENDS)、按名注册/发现、按名实例化。这是实现侧的可替换性

门面对外屏蔽差异,注册表对内管理候选;新后端 = 写一个实现统一接口的类 + 登记一个名字。没有第 1 层,注册表只是花哨的字典;没有第 2 层,门面就是一坨写死的 if/elif——两层缺一不可。这也解释了为什么 Semantica 敢在 docstring 里写"Method registry for extensibility / Support for community-contributed extensions"(vector_store/registry.py:22-28):扩展入口是被设计出来的,不是事后开的洞。

五、与 vnpy/Reasonix 注册表模式的对照

三家都在用注册表,但管理对象与粒度不同:

  • vnpy(量化交易框架):注册表围绕事件与网关——事件引擎按事件类型注册回调,网关/应用类经类变量字典按名注册、由主引擎统一实例化与启停。与 Semantica 的共同点是"名字即间接层";差异是 vnpy 注册的是长连接的行情/交易通道,生命周期重(连接、订阅、断线重连)。
  • Reasonix(agent 框架):注册表面向组件能力(工具/智能体按名注册进编排器),粒度与 MethodRegistry 相当;但它是单点机关,缺少 Semantica 这种"每模块一份、task 二级分类"的横向复制——Semantica 把注册表当全仓统一惯例
  • Semantica:注册谱系最宽——函数(MethodRegistry)、类(PluginRegistry)、后端类(SUPPORTED_BACKENDS 白名单)三层都有;PluginRegistry 独有目录发现 + 依赖递归 + 生命周期(initialize/cleanup),更像微型包管理器。

一句话:vnpy 用注册表接世界(行情源),Reasonix 用注册表接能力(工具),Semantica 用注册表接存储与算法的全谱系——polyglot 只是这个惯例在存储层的显影。

💡 装配要点:写自己的扩展走两条正门——①轻量级:往对应模块的 registry.py 单例上 register(task, name, func)(带 **metadata 说明用途),随后 get_xxx_method(task, name) 可查、list_available_methods() 可被发现;②重量级:在 plugin_paths 目录丢一个 .py,类名符合三种嗅探模式之一、带 initialize/execute,模块级属性声明 version/dependencies,PluginRegistry.load_plugin 会连依赖一起递归拉起、unload_plugin 走 cleanup 收尾。

本节要点回顾

  1. MethodRegistry 骨架:task 二级字典 + register(白名单与 callable 校验,元数据挂函数属性)/get/list_all/has/unregister;模块级单例与 core 类方法版两种变体。
  2. 全仓惯例:14 个子模块各带 registry.py,task 分类随领域变;默认实现加载即自注册(core/methods.py:425-427)。
  3. 按名切换实战:NER 的 method="ml"/"llm"/"huggingface",还支持列表 + ensemble_voting 集成投票;名字是实现可变性的间接层。
  4. PluginRegistry:准入两方法(initialize/execute)+ 依赖预校验;加载四步(幂等返回→按需发现→递归依赖→实例化 initialize);类名嗅探两策略 + 模块级元数据;orchestrator 全局持有。
  5. polyglot 可行性:统一抽象接口(调用侧稳定)× 注册表分发(实现侧可替换)=一行换后端;扩展入口是设计出来的。
  6. 横向对照:vnpy 管事件与网关、Reasonix 管组件能力、Semantica 全谱系三层注册且带生命周期——是微型包管理器而不只是字典。

下一节:第 8 章装完了存储的三翼。第 9 章转向检索——向量存进去了,怎么查得又快又准?01 节拆 HybridSearch 的 RRF 融合与 GraphRAG 的多跳推理路径。


作者与出处
原作者: 灏天文库
整理: 灏天文库整理
本站整理收录,版权归原作者/开源协议所有;欢迎通过原文链接访问源仓库。
发布者: 作者: 灏天文库 转发
评论区 (0)
U