本节摘要:01、02 两节的"一行切换"不是魔法,机关是注册表。Semantica 有两套机关:其一
MethodRegistry——几乎每个子模块(vector_store/graph_store/kg/ontology/export/…共 14 处)都自带一个registry.py,同一能力多种实现按名注册、按名取用(如抽取的 ml/pattern/llm 三方法);其二core/plugin_registry.py的PluginRegistry——从目录动态发现、加载、递归解依赖、初始化外部插件。本节拆注册表模式的实现(注册→发现→分发),回答"polyglot 为什么可行",并示范自己写扩展的两条正门,最后与 vnpy/Reasonix 的注册表模式对照。
内容来源:原项目源码
semantica/vector_store/registry.py、semantica/core/registry.py、semantica/core/plugin_registry.py、semantica/core/orchestrator.py、semantica/core/methods.py、semantica/semantic_extract/ner_extractor.py、semantica/vector_store/methods.py。
⚠️ 注意:
MethodRegistry有两种变体——vector_store 版是实例单例(method_registry = MethodRegistry(),vector_store/registry.py:155),core 版是类方法(@classmethod直接挂在类上,core/registry.py:56)。写扩展前先看清你面对的是哪一种,注册姿势并不相同。
阅读完本节,你应当能够:
先看最小实现。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) 后即可按名取用、按名发现。
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()。
回头看本章标题的问题——8 向量库、4 图库、5 RDF 库为何能一行切换?答案是"可插拔"被拆成了两层互补的机关:
VectorStore/GraphStore/TripletStore 规定 add/search/delete/query 的形状,用 hasattr/inspect 做鸭子适配(vector_store.py:507-529)。这是调用侧的稳定性。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 用注册表接能力(工具),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 收尾。
下一节:第 8 章装完了存储的三翼。第 9 章转向检索——向量存进去了,怎么查得又快又准?01 节拆 HybridSearch 的 RRF 融合与 GraphRAG 的多跳推理路径。