首页
/ Pathway xpacks LLM 文本切块实战指南:TokenCountSplitter、RecursiveSplitter 与 DoclingParser 的 RAG 分块机制

Pathway xpacks LLM 文本切块实战指南:TokenCountSplitter、RecursiveSplitter 与 DoclingParser 的 RAG 分块机制

2026-09-06 17:48:54作者:霍妲思

本文基于 Pathway Live Data Framework(下称 Pathway)的开发者文档 docs/2.developers/4.user-guide/50.llm-xpack/.splitters/splitters.md 展开,系统讲解 RAG(检索增强生成)流水线中文档分块(chunking)的三种实现——TokenCountSplitterRecursiveSplitterDoclingParser——并深入源码 python/pathway/xpacks/llm/splitters.py 剖析其切分算法、参数默认值与测试验证方式。读完后,你将能够在自己的实时 RAG 管道中正确选型、配置分块器,并理解每个参数在底层是如何生效的。

为什么 RAG 必须先分块

将整篇文档作为一个向量嵌入,往往会导致检索质量下降:嵌入模型被迫把整篇文档的信息压缩进单一向量表示,难以捕获细粒度细节,重要上下文可能丢失,检索效果随之变差。

文档中还指出,简单的“每 n 个字符切一刀”策略存在两个问题:

  1. 切分点生硬:会截断句子或短语,产生不完整、语义扭曲的块;
  2. 粒度不一致:token 的粒度不一(一个 token 可能是一个字符、一个单词或标点),按字符数无法保证各块 token 规模一致。

更好的做法是按 token 分块,让每个块既语义完整,又对齐句子或段落边界,即在句号、逗号、换行等逻辑断点处切分。Pathway 的 LLM xpack 正是围绕这一思路提供了下面三类分块组件。

分块器模块的整体设计:BaseSplitter 与元数据传播

所有分块器的实现在 splitters.py 中,它们共同继承自抽象基类 BaseSplitter源码 L21-L81),该基类有两个关键设计:

  • 它是一个 Pathway UDF(继承 pw.UDF),因此可以直接作为列表达式挂到表格列上,例如 t += t.select(chunks = splitter(pw.this.text)),在流式管道中对不断更新的文本列做增量分块;
  • 统一的输入输出约定__wrapped__ 方法接受 str(str, dict | pw.Json) 元组(L44-L77),即“文本 + 元数据”对。切分后,同一输入文本派生出的所有 chunk 都会继承同一份 metadata;若输入没有元数据,则用空字典兜底。返回类型统一为 list[tuple[str, dict]]

这意味着分块器与上游 parser(如 UnstructuredParserDoclingParser)天然衔接:parser 产出的每个文档块都带有来源、标题等元数据,splitter 会把这些元数据原样复制到每个子块上,供后续嵌入与检索阶段使用。

此外,模块还提供一个极简的 NullSplitterL161-L174):原样返回输入文本和元数据,不做任何切分,适合调试管道或确认无需分块的场景(单测 test_null 即验证了这一点,见 test_splitters.py)。

TokenCountSplitter:基于 token 计数的分块器

文档给出的标准用法(Python 形式):

from pathway.xpacks.llm.splitters import TokenCountSplitter

text_splitter = TokenCountSplitter(
    min_tokens=100,
    max_tokens=500,
    encoding_name="cl100k_base"
)

在模板(template)工程中则通过 YAML 声明:

splitter: pw.xpacks.llm.splitters.TokenCountSplitter
  min_tokens: 100
  max_tokens: 500
  encoding_name: "cl100k_base"

注意:原关联文档的 YAML 示例中把第一个参数写成了 min_tokes,这是一个拼写错误;从 splitters.py L215-L227 的构造函数签名看,正确的关键字是 min_tokens

这组配置的含义是:使用 cl100k_base tokenizer(与 OpenAI 嵌入模型兼容)生成 100–500 token 的块。可用编码名的完整列表可查阅 tiktoken 的官方文档。

参数与默认值

构造函数(L215-L227)的完整参数:

参数 默认值 说明
min_tokens 50 每个块的 token 数下限
max_tokens 500 每个块的 token 数上限
encoding_name "cl100k_base" tiktoken 编码名,决定 token 化方式

由于 BaseSplitter 把构造参数存入 self.kwargschunk() 支持 **kwargs 覆盖,所有默认参数都可以在 UDF 调用时逐次覆盖,例如 splitter(pw.this.text, max_tokens=300)。传入未知参数会直接抛出 ValueErrorL251-L252),有助于尽早发现配置笔误。

底层切分算法

chunk() 的实现(L229-L272)可以读出具体流程:

  1. 先用 _normalize_unicode 对文本做 NFKC 规范化(L14-L18),消除连字(ligatures)等 Unicode 变体,保证 token 计数稳定;
  2. tiktoken.get_encoding(...) 得到编码,encode_ordinary 把全文切成 token 序列;
  3. max_tokens 依次截取 token 窗口并解码回文本;
  4. 标点回退:在块内查找最后一个标点位置(PUNCTUATION = [".", "?", "!", "\n"],见 L213),若其位置超过 CHARS_PER_TOKEN * min_tokens(其中 CHARS_PER_TOKEN = 3,即按“约 3 字符/token”粗略估计),就把块截断到该标点处,避免在句中被拦腰截断;
  5. 用截断后文本重新编码的 token 数推进指针,并把 (chunk, metadata) 追加到输出。

也就是说,min_tokens 并非硬性下限,而是标点回退的阈值系数;max_tokens 才是真正的硬上限。

测试与示例印证

  • 单测 test_splitters.py 用一句波兰语(含特殊字符和 emoji)验证短文本不会被切碎;
  • 仓库中的 RAG 示例 examples/projects/question-answering-rag/main.py 正是按文档同款参数 min_tokens=100, max_tokens=500, encoding_name="cl100k_base" 构造 TokenCountSplitter,接入 DocumentStoreOpenAIEmbedder
  • examples/projects/conf42/main.py 则演示了更省事的写法:TokenCountSplitter(max_tokens=400)(其余取默认值),并把 splitter 直接传给 VectorStoreServer,实现“入参即分块”的向量化服务。

RecursiveSplitter:按分隔符递归下钻的分块器

文档给出的用法:

splitter = RecursiveSplitter(
    chunk_size=400,
    chunk_overlap=200,
    separators=["\n#", "\n##", "\n\n", "\n"],  # separators for markdown documents
    model_name="gpt-4o-mini",
)

对应的模板 YAML:

splitter: pw.xpacks.llm.splitters.RecursiveSplitter
  chunk_size: 400
  chunk_overlap: 200
  separators:
    - "\n#"
    - "\n##"
    - "\n\n"
    - "\n"
  model_name: "gpt-4o-mini"

工作原理

RecursiveSplitterTokenCountSplitter 一样以 token 数度量块长,但切分点的确定方式不同:它持有一个有序分隔符列表 separators,从列表中第一个(粒度最细)分隔符开始尝试切分;只要某段子文本仍超过 chunk_size,就落到列表中的下一个分隔符继续切,直到所有块都小于 chunk_size 为止。以文档示例为例,它会先尝试在 \n#(Markdown 一级标题)处切,必要时退回 \n##\n\n(段落)、\n(行)。

参数与默认值

构造函数见 splitters.py L114-L154

参数 默认值 说明
chunk_size 500 块的最大长度(字符数或 token 数,取决于 tokenizer 配置)
chunk_overlap 0 相邻块之间的重叠量
separators SEPARATORS = ["\n\n", "\n", " ", ""]L84 按优先级排序的分隔符列表,默认从段落、行、空格逐层下钻
is_separator_regex False 分隔符是否按正则解释
encoding_name None tiktoken 编码名,提供后按该编码的 token 数度量块长
model_name None tiktoken 模型名(如 gpt-4o-mini),提供后按该模型的 token 化度量
hf_tokenizer None Hugging Face PreTrainedTokenizerBase,提供后按其 token 化度量

构造函数中的选择逻辑值得注意(L141-L154):

  • 给了 encoding_namemodel_name → 走 RecursiveCharacterTextSplitter.from_tiktoken_encoder,块长按 token 数 计算;
  • 给了 hf_tokenizer → 走 from_huggingface_tokenizer,块长按该 HF tokenizer 的 token 数计算;
  • 三者都不给 → 退化为按字符数 计算块长。

也就是说,文档中“以 token 数度量”的表述成立的前提是你在构造时传入了 tokenizer 配置之一;否则它是纯字符级切分。

关于 chunk_overlap 的取舍

文档特别提醒:chunk_overlap 能引入重叠块、帮助不同块捕获不同上下文(例如跨越切分点的长句子),但重叠会增加总块数,从而增大嵌入与检索开销。上面的示例把 overlap 设为 200(约为 chunk_size 的一半),是典型的“语义连续性优先”配置。

测试覆盖

仓库为三条构造路径都提供了测试:

  • test_recursive_from_encodingtests/test_splitters.py L34-L47):encoding_name="cl100k_base"chunk_size=30,用 5 段以 \n\n 连接的 26-token 文本断言恰好切出 5 块,且每块携带空元数据 pw.Json({})——这正是 BaseSplitter 元数据传播约定的直接验证;
  • test_recursive_from_model_nameL50-L61):用 model_name="gpt-4" 走同一断言;
  • 集成测试 integration_tests/xpack/test_splitters.py:用 transformers.AutoTokenizer 加载 bert-base-uncased,以 hf_tokenizer 构造并验证同样切出 5 块。

另外,底层实际是 langchain_text_splitters.RecursiveTextSplitter(MIT 许可)的封装,这一点在 类 docstring L97 中有明确声明;且 langchain_text_splitters 通过 optional_imports("xpack-llm") 延迟导入(L126-L130),只有在安装了 xpack-llm 可选依赖组时才需要。

DoclingParser:基于文档结构语义的分块

文档把 DoclingParser 归入分块主题,因为它走的是完全不同的路线:不依赖 token 或字符计数,而是利用文档固有结构(标题、段落、列表、表格等标记)决定块边界。这样切出的块能保留逻辑章节,每个块内部上下文连贯——例如不会把一张表从中间劈开。

实现位于 parsers.py 中的 DoclingParser(L342 起),其 docstring 与构造参数给出了比文档更细的实现事实:

  • 它是一个 pw.UDF,内部封装 doclingDocumentConverter,并额外支持用视觉 LLM 解析 PDF 中的图片与表格;
  • chunk: bool = True:默认开启分块。文档中“想关掉分块就把构造参数设为 chunk=False”的说法与此对应——置 False 时整篇文档作为单个块返回(docstring L379-L380);
  • 底层 chunker:分块由经过改造的 HybridChunker(源自 docling)完成(L369-L378)。改造点包括:正确处理视觉 LLM 解析图片/表格的功能;表格不再转成“行/列/值”三元组,而是直接转成 Markdown 文本。图片与表格会各自成为独立块,并携带 caption(标题/说明文字)
  • 长度不敏感:文档说“这种切法会产生长度不一的块”的根源在此——该 chunker 只按结构切块,不感知字符或 token 长度;
  • 相似 metadata 的块会被合并(merge),避免碎片化;
  • 附加上下文:每个块会包裹标题、caption 等附加元素(图片、表格尤其如此),这为检索提供了额外语境,提升命中率。

与 token 级分块器组合

由于 DoclingParser 的块长不均匀,文档建议:若需要更均匀的块,可以在 DoclingParser 之上再套一层 TokenCountSplitterRecursiveSplitter。从类型约定看这是天然可行的——parser 输出 (text, metadata) 元组列表,splitter 的输入恰好接受这种元组,且元数据会逐层传播到最细粒度的子块。即“结构化粗切 + token 级精切”的两级方案。

另外,DoclingParser 还支持 table_parsing_strategy"docling""llm")、image_parsing_strategy"llm")、pdf_pipeline_options 等参数来调节解析行为(如 pdf_pipeline_options={"table_structure_options": {"mode": "accurate"}}),默认管道选项见 L441-L461:默认表格结构解析为 fast 模式且开启 do_cell_matching

三种方案选型小结

方案 切分依据 长度控制 元数据 适用场景
TokenCountSplitter token 窗口 + 标点回退 硬上限 max_tokens,标点回退阈值 min_tokens 全块继承 纯文本、需要均匀 token 块(贴合嵌入模型上下文窗口)
RecursiveSplitter 分隔符列表递归下钻 chunk_size 硬上限,可配 chunk_overlap 全块继承 Markdown/结构化长文、需要重叠上下文、可指定 tiktoken/HF tokenizer
DoclingParser(chunk=True) 文档语义结构(标题/段落/表格/图片) 不敏感,块长不一 块合并 + 标题/caption 包裹 PDF 等复杂版式文档、表格/图片不可劈开

三者的公共契约由 BaseSplitter 保证:输入接受 str(str, metadata),输出统一为 (chunk, metadata) 元组列表,metadata 全量传播。因此在 Pathway 的流式 RAG 管道中,它们可以像积木一样串接在 parser 与 embedder 之间,并随源数据更新自动增量重算分块结果。

参考文件

登录后查看全文
热门项目推荐
相关项目推荐