LlamaIndex 文档解析与分块策略深度解析

7 阅读14分钟

在 RAG(检索增强生成)系统中,分块(Chunking)是影响检索质量最关键的一步。文本被切割成多大、怎么切,直接决定了嵌入向量的语义完整性和检索的准确性。

一、引言

在 RAG(Retrieval-Augmented Generation)系统中,文档解析与分块(Chunking)是决定检索质量和生成效果的关键环节。LlamaIndex 提供了一套层次分明、功能丰富的文档解析与分块体系,从最基础的文本切分到语义感知的智能分块,再到结构化文档的层级解析,覆盖了几乎所有常见的文档处理场景。 LlamaIndex 文档解析与分块 — 引言 本文基于 LlamaIndex 源码,深入剖析其文档解析与分块策略的架构设计、核心组件和最佳实践。

二、核心架构:从 Document 到 Node

Document → Node 核心架构

2.1 数据模型层次

LlamaIndex 的文档处理遵循 Document → Node 的转换管线:

原始文件 → SimpleDirectoryReader → Document → NodeParser → Node(TextNode / ImageNode / IndexNode)
  • Document:代表一个完整的文档,是数据加载的输出单位
  • Node:代表文档的一个片段,是索引和检索的基本单位

schema.py 中,核心数据模型定义如下:

class NodeRelationship(str, Enum):
    SOURCE = auto()    # 指向源文档
    PREVIOUS = auto()  # 前驱节点
    NEXT = auto()      # 后继节点
    PARENT = auto()    # 父节点(层级分块)
    CHILD = auto()     # 子节点(层级分块)

class BaseNode(BaseModel):
    excluded_embed_metadata_keys: List[str] = Field(default_factory=list)
    excluded_llm_metadata_keys: List[str] = Field(default_factory=list)
    relationships: Dict[NodeRelationship, RelatedNodeType] = Field(default_factory=dict)

关键设计思想:每个 Node 都通过 relationships 维护与其他节点的关联关系,这为层级分块、上下文窗口、自动合并检索等高级特性奠定了基础。

2.2 元数据排除机制

base.py 中,SimpleDirectoryReader 会自动将文件元数据排除在嵌入和 LLM 处理之外:

def _exclude_metadata(self, documents: list[Document]) -> list[Document]:
    for doc in documents:
        doc.excluded_embed_metadata_keys.extend([
            "file_name", "file_type", "file_size",
            "creation_date", "last_modified_date", "last_accessed_date"
        ])
        doc.excluded_llm_metadata_keys.extend([
            "file_name", "file_type", "file_size",
            "creation_date", "last_modified_date", "last_accessed_date"
        ])
    return documents

这确保了文件系统元数据不会干扰语义嵌入或 LLM 推理,同时仍保留在节点中供元数据过滤使用。


三、文档加载:SimpleDirectoryReader

SimpleDirectoryReader 文档加载器

SimpleDirectoryReader 是 LlamaIndex 内置的文档加载器,支持从目录批量读取文件:

class SimpleDirectoryReader(BaseReader, ResourcesReaderMixin, FileSystemReaderMixin):
    def __init__(
        self,
        input_dir: Optional[Union[Path, str]] = None,
        input_files: Optional[list] = None,
        exclude: Optional[list] = None,           # 排除的文件模式
        exclude_hidden: bool = True,               # 排除隐藏文件
        exclude_empty: bool = False,               # 排除空文件
        recursive: bool = False,                   # 递归读取子目录
        required_exts: Optional[list[str]] = None, # 限定文件扩展名
        num_files_limit: Optional[int] = None,     # 限制文件数量
        file_metadata: Optional[Callable[[str], dict]] = None,
        filename_as_id: bool = False,              # 使用文件名作为文档ID
        encoding: str = "utf-8",
        errors: str = "ignore",
        raise_on_error: bool = False,
        file_extractor: Optional[dict[str, BaseReader]] = None,  # 按文件类型指定解析器
        fs: Optional[fsspec.AbstractFileSystem] = None,
    ) -> None: ...

使用示例

from llama_index.core import SimpleDirectoryReader

# 基础用法
documents = SimpleDirectoryReader("data").load_data()

# 高级用法:指定文件类型、递归读取、自定义解析器
documents = SimpleDirectoryReader(
    input_dir="data",
    recursive=True,
    required_exts=[".pdf", ".md", ".txt"],
    file_extractor={".pdf": PDFReader(), ".md": MarkdownReader()}
).load_data()

四、分块策略体系总览

四大分块策略体系

LlamaIndex 的分块策略可以分为 四大类别

类别解析器核心思想
文本分块SentenceSplitter, TokenTextSplitter, CodeSplitter基于规则和 Token 计数切分
语义分块SemanticSplitterNodeParser, SemanticDoubleMergingSplitterNodeParser基于语义相似度切分
结构化分块MarkdownNodeParser, HTMLNodeParser, JSONNodeParser, SimpleFileNodeParser基于文档结构切分
层级/关系分块HierarchicalNodeParser, SentenceWindowNodeParser, MarkdownElementNodeParser, UnstructuredElementNodeParser维护节点间层级/上下文关系

4.1 类继承体系

NodeParser (抽象基类)
├── TextSplitter (抽象,纯文本切分)
│   ├── MetadataAwareTextSplitter (元数据感知)
│   │   ├── SentenceSplitter (句子级切分,默认解析器)
│   │   └── TokenTextSplitter (Token级切分)
│   ├── CodeSplitter (代码切分)
│   └── LangchainNodeParser (LangChain适配器)
├── SemanticSplitterNodeParser (语义分块)
├── SemanticDoubleMergingSplitterNodeParser (语义双重合并)
├── SentenceWindowNodeParser (句子窗口)
├── HierarchicalNodeParser (层级分块)
├── MarkdownNodeParser (Markdown结构切分)
├── HTMLNodeParser (HTML结构切分)
├── JSONNodeParser (JSON结构切分)
├── SimpleFileNodeParser (自动文件类型检测)
├── MarkdownElementNodeParser (Markdown元素提取)
├── UnstructuredElementNodeParser (非结构化元素提取)
└── LlamaParseJsonNodeParser (LlamaParse JSON元素提取)

五、文本分块策略详解

文本分块三大策略

5.1 SentenceSplitter(句子级分块器)

这是 LlamaIndex 的默认分块器,在 settings.py 中可以看到:

class Settings(BaseSettings):
    _node_parser: Optional[NodeParser] = None

    def node_parser(self) -> NodeParser:
        if self._node_parser is None:
            self._node_parser = SentenceSplitter()
        return self._node_parser

核心参数(源码来自 sentence.py):

参数默认值说明
chunk_size1024每个 Chunk 的最大 Token 数
chunk_overlap200相邻 Chunk 之间的重叠 Token 数
separator" "默认分词分隔符(空格)
paragraph_separator"\n\n\n"段落分隔符
secondary_chunking_regex"[^,.;。?!]+[,.;。?!]?[,.;。?!]"句子级正则表达式

分块算法流程

1. 按段落分隔符(\n\n\n)拆分
2. 如果段落超过 chunk_size,按句子正则拆分
3. 如果句子仍超过 chunk_size,按默认分隔符(空格)拆分
4. 合并小片段到 chunk_size 以内的块中
5. 处理 chunk_overlap 重叠区域

源码中的核心方法:

def _split(self, text: str, chunk_size: int) -> List[_Split]:
    # 1. split by paragraph separator
    # 2. split by chunking regex (sentence level)
    # 3. split by default separator (" ")
    # 4. split by character (fallback)

def _merge(self, splits: List[_Split], chunk_size: int) -> List[str]:
    # 合并小片段,处理重叠区域

设计哲学:优先保持句子和段落的完整性,避免出现"悬挂句子"(hanging sentences),这是相比 TokenTextSplitter 的核心优势。

使用示例

from llama_index.core.node_parser import SentenceSplitter

# 自定义参数
splitter = SentenceSplitter(
    chunk_size=512,
    chunk_overlap=50,
    separator=" ",
    paragraph_separator="\n\n\n",
)

nodes = splitter.get_nodes_from_documents(documents)

5.2 TokenTextSplitter(Token 级分块器)

核心参数(源码来自 token.py):

参数默认值说明
chunk_size1024每个 Chunk 的最大 Token 数
chunk_overlap20相邻 Chunk 之间的重叠 Token 数
separator" "分词分隔符

与 SentenceSplitter 的区别

  • TokenTextSplitter 以 Token 为单位严格切分,不考虑句子边界
  • SentenceSplitter 优先保持句子完整性,chunk_overlap 默认更大(200 vs 20)
  • SentenceSplitter 有段落感知和句子正则,TokenTextSplitter 更简单粗暴

适用场景:当对文本完整性要求不高、需要精确控制 Token 数量时使用(如严格受 Token 限制的 API 调用)。

5.3 CodeSplitter(代码分块器)

核心参数(源码来自 code.py):

参数默认值说明
language必填编程语言(如 "python", "javascript")
chunk_lines40每个 Chunk 的行数
chunk_lines_overlap15相邻 Chunk 的重叠行数
max_chars1500每个 Chunk 的最大字符数

核心特性:基于 tree-sitter AST 解析器进行代码感知分块,确保代码块在语法结构上是完整的(如不会在函数定义中间切断)。

from llama_index.core.node_parser import CodeSplitter

splitter = CodeSplitter(
    language="python",
    chunk_lines=40,
    chunk_lines_overlap=15,
    max_chars=1500,
)

nodes = splitter.get_nodes_from_documents(documents)

注意:需要安装 tree_sitter_language_pack 依赖。


六、语义分块策略详解

语义感知分块策略

6.1 SemanticSplitterNodeParser(语义分块器)

核心思想:利用嵌入模型计算相邻句子的语义相似度,在语义"断裂点"处进行分块。

核心参数(源码来自 semantic_splitter.py):

参数默认值说明
buffer_size1评估语义相似度时组合的句子数
embed_modelOpenAIEmbedding()嵌入模型
sentence_splittersplit_by_sentence_tokenizer句子切分函数
breakpoint_percentile_threshold95语义断裂点百分位阈值(越低产生越多节点)

算法流程

1. 将文本按句子切分
2. 将相邻 buffer_size 个句子组合,计算嵌入向量
3. 计算相邻组合句子的语义相似度(余弦距离)
4. 找到相似度的百分位断点(低于阈值的点即为语义断裂点)
5. 在断裂点处进行分块

使用示例

from llama_index.core.node_parser import SemanticSplitterNodeParser
from llama_index.core.embeddings import resolve_embed_model

embed_model = resolve_embed_model("local:BAAI/bge-small-zh-v1.5")

parser = SemanticSplitterNodeParser(
    buffer_size=1,
    breakpoint_percentile_threshold=95,
    embed_model=embed_model,
)

nodes = parser.get_nodes_from_documents(documents)

6.2 SemanticDoubleMergingSplitterNodeParser(语义双重合并分块器)

核心思想:在语义分块的基础上增加"双重合并"机制——先按语义相似度初始化分块,再对相邻分块进行二次合并。

核心参数(源码来自 semantic_double_merging_splitter.py):

参数默认值说明
language_configLanguageConfig("english")Spacy 语言配置
embed_modelNone嵌入模型(设置后替代 Spacy)
initial_threshold0.6初始化新 Chunk 的相似度阈值
appending_threshold0.8追加句子到现有 Chunk 的阈值
merging_threshold0.8合并相邻 Chunk 的阈值
max_chunk_size1000Chunk 最大字符数
merging_range1合并时向前查看的 Chunk 数(1 或 2)

算法流程

阶段一:初始化分块
  - 逐句扫描,若当前句子与 Chunk 的相似度 < initial_threshold,则开新 Chunk
  - 若相似度 >= appending_threshold 且未超 max_chunk_size,则追加到当前 Chunk

阶段二:合并分块
  - 检查相邻 Chunk 的相似度,若 >= merging_threshold 且合并后不超 max_chunk_size,则合并
  - merging_range=2 时,还会检查隔一个 Chunk 的相似度

LanguageConfig 支持的语言:目前仅支持 englishgermanspanish。若需处理中文,应使用 embed_model 参数。

from llama_index.core.node_parser import SemanticDoubleMergingSplitterNodeParser

parser = SemanticDoubleMergingSplitterNodeParser(
    embed_model=embed_model,
    initial_threshold=0.6,
    appending_threshold=0.8,
    merging_threshold=0.8,
    max_chunk_size=1000,
    merging_range=1,
)

nodes = parser.get_nodes_from_documents(documents)

七、结构化分块策略详解

结构化文档分块策略

7.1 MarkdownNodeParser

核心思想:基于 Markdown 标题层级(#, ##, ### 等)进行分块,每个节点包含其文本内容和标题路径。

核心参数(源码来自 markdown.py):

参数默认值说明
include_metadataTrue是否在节点中包含元数据
include_prev_next_relTrue是否包含前后节点关系
header_path_separator"/"标题路径分隔符

效果示例

# 第一章
## 1.1 概述
这是概述内容。
## 1.2 详情
这是详情内容。

将被解析为:

  • Node 1:标题路径 = "第一章/1.1 概述",内容 = "这是概述内容。"
  • Node 2:标题路径 = "第一章/1.2 详情",内容 = "这是详情内容。"
from llama_index.core.node_parser import MarkdownNodeParser

parser = MarkdownNodeParser(
    header_path_separator="/",
    include_metadata=True,
)

nodes = parser.get_nodes_from_documents(documents)

7.2 HTMLNodeParser

核心思想:基于 HTML 标签进行分块,从指定标签中提取文本。

核心参数(源码来自 html.py):

参数默认值说明
tags["p", "h1"~"h6", "li", "b", "i", "u", "section"]要提取文本的 HTML 标签列表
from llama_index.core.node_parser import HTMLNodeParser

parser = HTMLNodeParser(
    tags=["p", "h1", "h2", "h3", "table", "li"],
)

nodes = parser.get_nodes_from_documents(documents)

7.3 JSONNodeParser

核心思想:基于 JSON 结构进行分块,将 JSON 文档按逻辑结构拆分为独立节点。

from llama_index.core.node_parser import JSONNodeParser

parser = JSONNodeParser(
    include_metadata=True,
    include_prev_next_rel=True,
)

nodes = parser.get_nodes_from_documents(documents)

7.4 SimpleFileNodeParser

核心思想:根据文件类型自动选择合适的 NodeParser,是"智能路由"入口。

from llama_index.core.node_parser import SimpleFileNodeParser

parser = SimpleFileNodeParser()
nodes = parser.get_nodes_from_documents(documents)

八、层级/关系分块策略详解

层级关系分块策略

8.1 HierarchicalNodeParser(层级分块器)

核心思想:将文档按多个粒度递归分块,形成父子层级关系。这是 AutoMergingRetriever 的基础。

核心参数(源码来自 hierarchical.py):

参数默认值说明
chunk_sizes[2048, 512, 128]各层级的 Chunk 大小列表

层级结构示例

Level 0: chunk_size=2048 (顶层大块)
  ├── Level 1: chunk_size=512 (中等子块)
  │     ├── Level 2: chunk_size=128 (最小叶节点)
  │     ├── Level 2: chunk_size=128
  │     └── Level 2: chunk_size=128
  ├── Level 1: chunk_size=512
  │     └── ...

辅助函数

from llama_index.core.node_parser import (
    HierarchicalNodeParser,
    get_leaf_nodes,
    get_root_nodes,
    get_child_nodes,
    get_deeper_nodes,
)

parser = HierarchicalNodeParser.from_defaults(
    chunk_sizes=[2048, 512, 128]
)
nodes = parser.get_nodes_from_documents(documents)

leaf_nodes = get_leaf_nodes(nodes)     # 获取叶节点(最小粒度)
root_nodes = get_root_nodes(nodes)     # 获取根节点(最大粒度)
child_nodes = get_child_nodes(some_nodes, all_nodes)  # 获取子节点

与 AutoMergingRetriever 配合

from llama_index.core.retrievers import AutoMergingRetriever
from llama_index.core import VectorStoreIndex, StorageContext

# 仅索引叶节点
leaf_nodes = get_leaf_nodes(nodes)
storage_context = StorageContext.from_defaults()
storage_context.docstore.add_documents(nodes)  # 存储所有层级节点

index = VectorStoreIndex(leaf_nodes, storage_context=storage_context)

retriever = AutoMergingRetriever(
    vector_retriever=index.as_retriever(similarity_top_k=6),
    storage_context=storage_context,
    simple_ratio_thresh=0.5,  # 若超过50%的子节点被检索,则合并为父节点
)

AutoMergingRetriever 的合并逻辑:当检索到的叶节点中,同一父节点下的子节点占比超过 simple_ratio_thresh(默认 0.5),则自动用父节点替换这些子节点,提供更完整的上下文。

8.2 SentenceWindowNodeParser(句子窗口分块器)

核心思想:每个节点只包含一个句子,但在元数据中存储周围句子的"窗口",检索时用单句嵌入,生成时用窗口上下文。

核心参数(源码来自 sentence_window.py):

参数默认值说明
sentence_splittersplit_by_sentence_tokenizer句子切分函数
window_size3窗口大小(前后各 N 个句子)
window_metadata_key"window"窗口文本的元数据键
original_text_metadata_key"original_text"原始句子的元数据键

工作原理

原文: [S1] [S2] [S3] [S4] [S5] [S6] [S7]

节点 S4 的元数据:
  window = "S1 S2 S3 S4 S5 S6 S7"  (window_size=3, 前后各3句)
  original_text = "S4"

嵌入时: 仅使用 S4 的文本
LLM 生成时: 使用 window 中的完整上下文

源码中的关键逻辑:

for i, node in enumerate(nodes):
    window_nodes = nodes[
        max(0, i - self.window_size) : min(i + self.window_size + 1, len(nodes))
    ]
    node.metadata[self.window_metadata_key] = " ".join([n.text for n in window_nodes])
    node.metadata[self.original_text_metadata_key] = node.text
    # 排除窗口元数据,不参与嵌入和 LLM
    node.excluded_embed_metadata_keys.extend([self.window_metadata_key, self.original_text_metadata_key])
    node.excluded_llm_metadata_keys.extend([self.window_metadata_key, self.original_text_metadata_key])

与 MetadataReplacementPostProcessor 配合

from llama_index.core.node_parser import SentenceWindowNodeParser
from llama_index.core.postprocessor import MetadataReplacementPostProcessor

parser = SentenceWindowNodeParser.from_defaults(
    window_size=3,
    window_metadata_key="window",
    original_text_metadata_key="original_text",
)

nodes = parser.get_nodes_from_documents(documents)

# 查询时替换为窗口上下文
postprocessor = MetadataReplacementPostProcessor(target_metadata_key="window")
query_engine = index.as_query_engine(
    node_postprocessors=[postprocessor],
)

8.3 MarkdownElementNodeParser / UnstructuredElementNodeParser

核心思想:将文档拆分为 TextNode(普通文本)和 IndexNode(嵌入对象,如表格),并自动为嵌入对象生成摘要。

源码来自 base_element.py

class BaseElementNodeParser(NodeParser):
    llm: Optional[LLM] = Field(default=None, description="LLM model to use for summarization.")
    summary_query_str: str = Field(default=DEFAULT_SUMMARY_QUERY_STR)
    num_workers: int = Field(default=DEFAULT_NUM_WORKERS)
  • MarkdownElementNodeParser:从 Markdown 中提取表格等元素
  • UnstructuredElementNodeParser:使用 Unstructured 库提取元素
  • LlamaParseJsonNodeParser:从 LlamaParse 的 JSON 输出中提取元素
from llama_index.core.node_parser import MarkdownElementNodeParser

parser = MarkdownElementNodeParser()
nodes = parser.get_nodes_from_documents(documents)
# nodes 中包含 TextNode(文本)和 IndexNode(表格摘要)
# IndexNode 通过 PARENT/CHILD 关系关联到原始表格内容

九、分块策略选型指南

分块策略选型指南

9.1 决策流程图

文档类型是什么?
├── 代码 → CodeSplitter
├── MarkdownMarkdownNodeParserMarkdownElementNodeParser
├── HTMLHTMLNodeParser
├── JSONJSONNodeParser
├── 混合格式 → SimpleFileNodeParser
└── 纯文本
    ├── 需要语义完整性?
    │   ├── 是 → SemanticSplitterNodeParser
    │   └── 否
    │       ├── 需要句子完整性? → SentenceSplitter(默认)
    │       └── 严格 Token 控制? → TokenTextSplitter
    └── 需要层级检索?
        ├── 是 → HierarchicalNodeParser + AutoMergingRetriever
        └── 需要上下文窗口?
            └── 是 → SentenceWindowNodeParser

9.2 关键参数调优建议

参数建议值说明
chunk_size256~1024太小丢失上下文,太大引入噪声。嵌入模型维度越低,chunk_size 应越小
chunk_overlapchunk_size 的 10%~20%保证跨块信息的连续性
breakpoint_percentile_threshold90~95越低产生越多分块,适合主题变化频繁的文档
window_size2~5句子窗口大小,取决于句子平均长度和所需上下文量
simple_ratio_thresh0.3~0.5AutoMerging 的合并阈值,越低越容易合并

9.3 性能对比

策略分块精度语义完整性计算开销适用场景
TokenTextSplitter极低快速原型、Token 限制严格
SentenceSplitter通用场景(默认选择)
SemanticSplitter高(需嵌入计算)主题变化明显的长文档
HierarchicalParser需要多粒度检索
SentenceWindow高(检索时)需要精确匹配+上下文扩展
MarkdownNodeParser结构化 Markdown 文档

十、全局配置与自定义

全局配置与自定义解析器

10.1 通过 Settings 全局配置

from llama_index.core import Settings
from llama_index.core.node_parser import SentenceSplitter

Settings.chunk_size = 512
Settings.chunk_overlap = 50

# 或直接设置解析器
Settings.node_parser = SentenceSplitter(
    chunk_size=512,
    chunk_overlap=50,
)

10.2 在索引构建时指定

from llama_index.core import VectorStoreIndex
from llama_index.core.node_parser import SemanticSplitterNodeParser

parser = SemanticSplitterNodeParser(
    buffer_size=1,
    breakpoint_percentile_threshold=95,
    embed_model=Settings.embed_model,
)

# 方式一:通过 transformations 参数
index = VectorStoreIndex.from_documents(
    documents,
    transformations=[parser],
)

# 方式二:先解析再构建
nodes = parser.get_nodes_from_documents(documents)
index = VectorStoreIndex(nodes)

10.3 自定义 NodeParser

from llama_index.core.node_parser import NodeParser
from llama_index.core.schema import BaseNode, TextNode
from typing import List, Sequence

class CustomParagraphParser(NodeParser):
    """按自定义段落标记分块的解析器"""

    def _parse_nodes(
        self,
        nodes: Sequence[BaseNode],
        show_progress: bool = False,
        **kwargs,
    ) -> List[BaseNode]:
        result = []
        for node in nodes:
            text = node.get_content()
            paragraphs = text.split("===PARA===")
            for para in paragraphs:
                if para.strip():
                    new_node = TextNode(
                        text=para.strip(),
                        metadata=node.metadata.copy(),
                    )
                    result.append(new_node)
        return result

十一、总结

LlamaIndex 分块体系总结

LlamaIndex 的文档解析与分块体系提供了从简单到复杂的完整解决方案:

  1. 基础层SentenceSplitter(默认)和 TokenTextSplitter 提供快速可靠的文本切分
  2. 语义层SemanticSplitterNodeParserSemanticDoubleMergingSplitterNodeParser 实现语义感知的智能分块
  3. 结构层MarkdownNodeParserHTMLNodeParserJSONNodeParser 针对结构化文档
  4. 关系层HierarchicalNodeParser + AutoMergingRetrieverSentenceWindowNodeParser 提供层级检索和上下文扩展能力

选择合适的分块策略,是构建高质量 RAG 系统的第一步,也是最关键的一步。建议从默认的 SentenceSplitter 开始,根据检索效果逐步尝试更高级的策略。