本节摘要:本节装配管线第③段的抽取半段,也是全书「发动机」的第一缸。
semantica/semantic_extract/共 19 个文件,四类抽取各由一个门面类负责:NERExtractor(实体)、RelationExtractor(关系)、EventDetector(事件)、TripletExtractor(三元组),外加CoreferenceResolver(共指消解)。本章核心是 ML/pattern/LLM 三种方法可切换:抽取器构造时收一个方法或方法列表,列表即回退链,无 API key 用 pattern 与 ML 照样出活,有 key 用 LLM 增强;再配MethodRegistry/ProviderRegistry支持自定义方法与自定义大模型端点。本节逐行读 NER 抽取器的回退链与集成投票,看 pattern 关系抽取的正则表,最后算一笔质量与成本的权衡账。
内容来源:原项目源码 semantica/semantic_extract/(ner_extractor.py、relation_extractor.py、event_detector.py、triplet_extractor.py、types.py、registry.py)
⚠️ 注意:三种方法不是等价替换而是三角取舍——pattern 确定可复现但只认预置模板;ML(spaCy)本地免费但模型小、类型粗;LLM 覆盖广但贵、慢、且输出不确定性恰好踩在 Semantica「确定性」的哲学红线上。默认值经过精心设计:NER 默认
ml、关系与三元组默认pattern,全链路无 key 可跑。
method 收字符串或列表(回退链)。NERExtractor.extract_entities 的方法循环、置信度过滤与集成投票。MethodRegistry/ProviderRegistry 注册自定义抽取方法与 LLM 端点。先立数据模型。types.py 只有 52 行,却是四个抽取器的共同语言:
@dataclass class Entity: text: str; label: str # 实体文本与类型(如 PERSON/ORG) start_char: int; end_char: int # 字符区间——分块与溯源都靠它 confidence: float = 1.0 metadata: Dict[str, Any] = field(default_factory=dict) @dataclass class Relation: subject: Entity; predicate: str; object: Entity # 头实体/谓词/尾实体 confidence: float = 1.0 context: str = "" # 关系所在上下文句子 @dataclass class Triplet: subject: str; predicate: str; object: str # RDF 形态:字符串三元组 confidence: float = 1.0 metadata: Dict[str, Any] = field(default_factory=dict)
注意三者的退化关系:Relation 持有的是 Entity 对象(带区间),Triplet 只剩字符串——从「文本中的关系」到「可入库的 RDF 三元组」,区间信息在最后一步被剥掉。start_char/end_char 是这套模型最值钱的字段:第 2 章的实体感知分块、第 7 章的事实级溯源都要靠它定位回原文。三个 dataclass 都实现了 get/__getitem__(Triplet 的 dict 风格访问),下游既可按属性也可按字典键取值——GraphBuilder 里大量 entity.get("id") 写法因此对对象与 dict 通吃。
四个抽取器各管一类语义:NERExtractor(ner_extractor.py,677 行)认「谁」;RelationExtractor(647 行)认「谁和谁有什么关系」;EventDetector(650 行)认「发生了什么」——事件触发词、参与者、时间、地点,还能做事件分类与时序排序;TripletExtractor(866 行)把前两者串成 RDF 三元组,附赠序列化(turtle/ntriples/jsonld/xml)与验证。CoreferenceResolver(693 行)解决「John went to the store. He bought milk.」里的 He 指谁——共指链消解后,实体计数不再被代词稀释。
目录里还有三个支撑件值得知道名字:providers.py(1543 行)是 LLM provider 层,OpenAI/Gemini/Groq 等端点的统一封装,base_url 参数可直接指向 Qwen/LLaMA 网关这类 OpenAI 兼容服务;extraction_validator.py(372 行)做抽取结果的质检;semantic_extract_provenance.py 与 cache.py 分别负责溯源挂载与结果缓存。门面则是 semantic_extract/__init__.py(213 行),四个抽取器全部从包顶层导出。
三个抽取器共享同一套方法机制。以 NERExtractor 为例,构造时声明方法(ner_extractor.py:87):
def __init__(self, method="ml", entity_types=None, **config): self.method = method if isinstance(method, list) else [method] ... # ml 方法需要的 spaCy 模型在构造时加载(进程级缓存,120ms 只付一次) if "ml" in self.method and SPACY_AVAILABLE: from .methods import load_spacy_model self.nlp = load_spacy_model(self.model_name)
方法的完整菜单(ner_extractor.py:97):pattern/regex/rules(确定性三兄弟)、ml(spaCy,默认)、huggingface(transformers 序列标注)、llm(OpenAI/Gemini/Groq 等多 provider)。切换就是换一个构造参数:
extractor = NERExtractor(method="ml") # 本地 spaCy extractor = NERExtractor(method="pattern") # 零依赖正则 extractor = NERExtractor(method="llm", provider="openai", llm_model="gpt-4") extractor = NERExtractor(method=["llm", "ml", "pattern"], ensemble_voting=True) # 集成投票
extract_entities 的方法循环(ner_extractor.py:362)是回退链的心脏:
# 逐个方法尝试 for method_name in methods: method_func = get_entity_method(method_name) ... entities = method_func(text, **method_options) # entity_types 提供时做加权置信度: # e.confidence = calculate_weighted_confidence(e.label, e.confidence, ...) filtered = [e for e in entities if e.confidence >= min_confidence] if filtered: all_entities.append((method_name, filtered)) if not self.ensemble_voting: return filtered # 非集成模式:首个成功的方法直接返回
三个细节值得圈点:其一,_filter_unusable_methods(ner_extractor.py:470)会在 spaCy 运行时初始化失败后把 ml 从链上摘掉,不让同一颗雷炸两次;其二,llm 分支会自动补 api_key——显式传入优先,否则按 {PROVIDER}_API_KEY 环境变量兜底(ner_extractor.py:390);其三,所有方法都失败时还有 _extract_fallback(ner_extractor.py:451)用最朴素的 pattern 兜底,保证「永远有产出」。
批量接口 extract(ner_extractor.py:165)收字符串列表或 {"id":..., "content":...} 文档列表,用线程池并发(max_workers 可配),并把 batch_index、document_id 写进每个实体的 metadata——溯源信息在抽取现场就挂上了。
RelationExtractor 的 pattern 路线(relation_extractor.py:139)是一张可读性极好的正则模板表:
self.relation_patterns = { "founded_by": [ r"(?P<subject>[\w\.\s]+?)\s+(?:was\s+)?founded\s+by\s+(?P<object>...)", r"(?P<object>...)\s+founded\s+(?P<subject>...)", ], "located_in": [...], "works_for": [ r"(?P<subject>...)\s+works?\s+for\s+(?P<object>...)", r"(?P<subject>...)\s+is\s+an?\s+employee\s+of\s+(?P<object>...)", ], "born_in": [...], }
每种谓词一组正则、命名捕获组直接产出主宾——这就是「无 LLM 也能建图」的证据:"Apple was founded by Steve Jobs." 走 pattern 路线即可产出 founded_by(Apple, Steve Jobs),结果确定、可复现、可单测。方法菜单还有 dependency(spaCy 依存句法树抽取,比正则更懂语法)、cooccurrence(共现近似,宁滥勿缺)、huggingface 与 llm。兜底策略三级:主方法失败退 pattern,pattern 也空但手里有实体时,用邻接启发式硬凑关系(relation_extractor.py:481 附近的 Last Resort)。
TripletExtractor 是组合器:调用时若未提供实体/关系,内部现场构造 NER 与关系抽取器(triplet_extractor.py:358 起,_ner_extractor/_relation_extractor 惰性缓存),把 (Entity, predicate, Entity) 转成字符串三元组,include_temporal=True 还能带时间注,serialize_triplets 一键转 turtle。它同样走方法回退链——triplet_extractor.py:435 附近的方法循环与 NER 抽取器如出一辙,huggingface 方法单独处理模型参数、llm 方法单独补 key。
三种方法的三角取舍用一张表收束:
| 维度 | pattern | ML(spaCy) | LLM |
|---|---|---|---|
| 确定性 | 完全确定 | 确定(同模型同输出) | 不确定,需温度与重试 |
| 覆盖率 | 只认模板内句式 | 中,泛化到相似句式 | 最高,开放域 |
| 成本 | 零 | 本地推理,一次模型加载 | 按 token 计费,最贵 |
| 适用 | 领域模板明确的场景 | 默认主力,无 key 跑通 | 高价值语料的增强抽取 |
工程上的正确姿势是分层组合:全量语料走 ml/pattern 保底,高价值子集再上 llm 增强;ensemble_voting=True 让多方法对同一文本投票(ner_extractor.py:443 的 _vote_entities,阈值 0.5)进一步提高精度——代价是每个方法都要跑一遍。
把四类抽取串成一段可运行的完整代码:
from semantica.semantic_extract import ( NERExtractor, RelationExtractor, EventDetector, TripletExtractor) text = "Apple was founded in 1976 by Steve Jobs in Cupertino." entities = NERExtractor(method="ml").extract(text) # 无 key:本地 spaCy relations = RelationExtractor(method="pattern").extract(text, entities) events = EventDetector().detect_events(text) # 事件+参与者+时间 triplets = TripletExtractor(method="pattern").extract_triplets( text, entities=entities, relations=relations)
注意 RelationExtractor.extract 的签名:实体是必传参数——关系抽取不做实体识别,它只负责在给定实体之间找谓词。这个接口设计强制了「先实体后关系」的流水顺序,也让上游(比如分块阶段已抽过实体的场景)可以复用实体结果、避免重复计算。
与 pipeline 的衔接在 GraphBuilder._extract_from_text(kg/graph_builder.py:336,下一节精读):build("原始文本", ner_method="ml", relation_method="pattern") 即可从纯文本直建图谱,注释里明确写着「raw-text build() must not require a provider, API key, or network access」。而 registry.py 的两个注册表给扩展留了门:method_registry.register("entity", "my_method", fn) 挂自定义抽取方法,provider_registry.register("my_llm", MyProvider) 挂自定义端点——local LLM/网关因此可以无痛接入。
💡 装配要点:本节装上发动机的第一缸。三个记忆点:①
Entity/Relation/Triplet三数据模型是四抽取器的共同语言,start_char/end_char连通分块与溯源;②method参数收列表即回退链,ml 失败自动摘除、全失败退 pattern、llm 自动读环境变量 key——「永远有产出」的三重保险;③ 默认配置(NER=ml、关系/三元组=pattern)就是「无 key 跑通」的技术兑现,LLM 只是增强而非依赖。
method,列表即回退链;min_confidence 过滤、entity_types 加权、ensemble_voting 投票。下一节:
02 GraphBuilder:构建知识图谱——抽取产物如何被装配成图:实体节点、关系边、EntityResolver 消解与关系端点重映射。