本节摘要:本节预装管线第③段的分块半段。
semantica/split/共 11 个文件 4683 行,注册表里躺着 22 种分块方法:固定/递归/token/句子/段落这类标准路线之外,有一组「图原生」分块——entity_aware(实体感知)、relation_aware(关系感知)、graph_based(图结构)、ontology_aware(本体感知)、community_detection(社区)、centrality_based(中心性)、subgraph(子图)等。本节精读统一分块器TextSplitter的回退链机制,逐段拆解 entity_aware 与 graph_based 的实现,回答核心问题:GraphRAG 为什么要原生分块——按实体和关系的边界切,图谱检索质量的关键就在这里。
内容来源:原项目源码 semantica/split/(methods.py、splitter.py、registry.py)
⚠️ 注意:图原生分块方法内部要先跑 NER/关系抽取(
split_entity_aware会现场构造NERExtractor,ml 方法要加载 spaCy 模型),依赖缺失或抽取失败时自动回退到split_recursive并打 warning——不会崩,但如果你以为在用图分块而实际一直在走递归兜底,检索质量会悄悄打折,留意日志里的「falling back」。
TextSplitter 的统一接口与回退链(method 参数传列表即多级兜底)。split_entity_aware:实体边界如何约束切分点。split_relation_aware 的三元组完整性约束与 split_graph_based 的社区/中心性两种策略。methods.py(1719 行)是 split 模块的发动机,文件头先给全部方法分类(methods.py:8-32):标准文本切分(recursive/token/sentence/paragraph/character/word/semantic_transformer/llm/huggingface/nltk)、KG 与图分析方法(entity_aware/relation_aware/graph_based/ontology_aware/embedding_semantic/hierarchical/community_detection/centrality_based/subgraph/topic_based)、专用分块器(structural/sliding_window)。注册表是一个平铺字典(methods.py:1647):
_SPLIT_METHODS = { # 标准方法 "recursive": split_recursive, "token": split_by_tokens, "sentence": split_by_sentences, ... # KG/本体方法 "entity_aware": split_entity_aware, "relation_aware": split_relation_aware, "graph_based": split_graph_based, "ontology_aware": split_ontology_aware, "embedding_semantic": split_embedding_semantic, "hierarchical": split_hierarchical, "community_detection": split_community_detection, "centrality_based": split_centrality_based, "subgraph": split_subgraph, "topic_based": split_topic_based, # 专用方法 "structural": split_structural, "sliding_window": split_sliding_window, }
其中 GraphRAG 相关的图原生路线正好十三个:注册表里的 entity_aware、relation_aware、graph_based、ontology_aware、embedding_semantic、hierarchical、community_detection、centrality_based、subgraph、topic_based 十种,加上三个专用分块器——structural(structural_chunker.py,349 行,按文档结构切)、table(table_chunker.py,343 行,表格不切散)、以及 kg_chunkers.py(438 行,专管「已建好的图→块」的反向切分)。这就是宣传页「13 种 GraphRAG 原生分块」的出处。查找顺序是先查 registry.py 的用户注册、再查内置表(methods.py:1676)。
统一入口是 TextSplitter(splitter.py:54),两个设计点。其一,method 参数收字符串或列表——列表即回退链:
def __init__(self, method="recursive", chunk_size=1000, chunk_overlap=200, **kwargs): if isinstance(method, str): self.methods = [method] else: self.methods = method if isinstance(method, list) else ["recursive"] ... def split(self, text, **override_options) -> List[Chunk]: for method_name in self.methods: # 逐个尝试回退链 try: method_func = get_split_method(method_name) chunks = method_func(text, **options) if chunks: return chunks # 第一个成功的方法直接返回 except Exception as e: last_error = e continue # 失败换下一个方法 raise ProcessingError(error_msg) # 全军覆没才报错
于是 TextSplitter(method=["graph_based", "entity_aware", "recursive"]) 是一条「图结构→实体感知→递归兜底」的降级链。其二,输出统一为 Chunk(semantic_chunker.py 定义):text、start_index、end_index、metadata——块级元数据是后面溯源与 GraphRAG 扩展的挂载点。
split_entity_aware(methods.py:852)的思路:先用 NER 抽出实体及其字符区间,把所有边界点记进集合,切分时不让块边界落在实体内部:
# methods.py:878 ner_extractor = NERExtractor(method=ner_method, **kwargs) entities = ner_extractor.extract(text) # 实体边界图:所有实体的 start/end 字符位置 entity_boundaries = set() for entity in entities: entity_boundaries.add(entity.start_char) entity_boundaries.add(entity.end_char)
随后按句子累积、按 chunk_size 切,但有个关键反转(methods.py:904):如果这个句子含有实体边界且 preserve_entities=True,即使超了尺寸也不切——宁可让块变大,也不把「Apple Inc.」拦腰斩断。切完的块把落在自己区间内的实体写进 metadata:
chunks.append(Chunk( text=chunk_text, start_index=text_pos, end_index=text_end, metadata={ "method": "entity_aware", "entity_count": len([e for e in entities if text_pos <= e.start_char < text_end]), "entities": [e for e in entities if text_pos <= e.start_char < text_end], # 块内实体清单 }, ))
这个 metadata["entities"] 就是图原生的第一份红利:检索时不需要再跑一遍 NER,块级实体清单直接可用于图扩展的种子节点。任何异常(依赖缺失、抽取失败)都会落入 methods.py:983 的兜底,退回 split_recursive。
split_relation_aware(methods.py:990)比实体感知更进一步:抽取关系后算出每个三元组的跨度和(subject 与 object 中较靠前的 start、较靠后的 end),切分时保证一个三元组的头尾实体落在同一个块里(methods.py:1026):
# 三元组边界:(subject, relation, object) 必须同块 triplet_boundaries = [] for relation in relations: start = min(relation.subject.start_char, relation.object.start_char) end = max(relation.subject.end_char, relation.object.end_char) triplet_boundaries.append((start, end))
split_graph_based(methods.py:1150)则是真正的「按图切」:先抽实体和关系建 networkx 图(实体为节点、关系为边),然后两种策略二选一。community 策略(默认,louvain 算法,没有 python-louvain 包时退化为连通分量)把图切成社区,每个社区一个块,块文本取社区内实体各自前后 100 字符的上下文拼接;centrality 策略按中心性排序,围绕高中心性节点做 k 跳邻域成块(methods.py:1275 附近,k_hop 默认 2,已用过的节点进 used_nodes 集合避免重复成块)。metadata 里带着 community_id/node_count,检索端可按社群过滤。
为什么 GraphRAG 必须原生分块?看一组因果链:传统固定分块把「Acme 公司 2024 年签了 Acme China 的合同」从中间切断——Acme 与 Acme China 的关系边在两个块里都不完整,向量检索召回了半个事实,多跳图扩展也从残缺节点出发。而 relation_aware 保证三元组完整落块,graph_based 保证一个社区的上下文聚在一起。分块方案决定了图的边在语料块中的完整性,而边的完整性直接决定检索质量——这就是「分块即注入图结构先验」。
选型速查:
| 场景 | 推荐方法 | 理由 |
|---|---|---|
| 通用文档、快速起步 | recursive | 零依赖、效果稳定 |
| 长文档保持层次 | hierarchical / structural | 按标题层级切 |
| GraphRAG 入门 | entity_aware | 块级实体清单,检索可扩展 |
| 关系密集语料 | relation_aware | 三元组完整性 |
| 主题聚集型语料 | graph_based(community) | 社区同块,主题内聚 |
| 成本敏感 + 高质量 | entity_aware 起、graph_based 兜 | 组合回退链按可用性降级 |
支柱页建议的微管线练手(ingest→parse→split)现在可以闭环了:拿 FileObject.text 喂 TextSplitter(method="entity_aware", chunk_size=1000),观察 Chunk.metadata["entities"] 里每个块带出的实体清单——第 3 章的抽取器将直接以这些块为输入。
💡 装配要点:本节装上管线的「切料机」。三个记忆点:① 注册表 22 种方法,13 种图原生;
TextSplitter(method=[...])列表即回退链,Chunk.metadata是图先验的挂载点;② entity_aware 的红线是实体边界、relation_aware 的红线是三元组跨度、graph_based 干脆按社群/中心性成块——三者的「图浓度」递增,计算成本也递增;③ 回退是双刃剑:保住了可用性,但 «falling back to recursive» 的 warning 值得盯,否则你以为的图分块可能从未生效。
_SPLIT_METHODS 注册 22 种方法,其中 GraphRAG 原生 13 种。TextSplitter:method 传列表即回退链,逐个尝试直到成功;输出统一 Chunk(text/start_index/end_index/metadata)。下一节:进入第 3 章★发动机章——
01 semantic_extract:NER/关系/事件/三元组抽取,看 ML/pattern/LLM 三种方法如何一行参数切换。