首页
/ Pathway LLM Xpack 文本分块指南:TokenCountSplitter、RecursiveSplitter 与 DoclingParser 的深度解析

Pathway LLM Xpack 文本分块指南:TokenCountSplitter、RecursiveSplitter 与 DoclingParser 的深度解析

2026-09-04 19:01:42作者:董宙帆

将整篇文档嵌入为单个向量往往导致检索效果不佳:模型被迫把文档的全部信息压缩进一个向量表示,难以捕获细粒度细节,重要上下文随之丢失,检索召回质量下降。Pathway 的 LLM xpack 为此提供了三种互补的分块(chunking/splitter)方案——基于 token 计数的 TokenCountSplitter、基于递归分隔符的 RecursiveSplitter,以及基于文档结构本身的 DoclingParser 内置分块器。本文基于 splitters 官方指南splitters 源码 展开,讲清每种分块策略的原理、参数默认值与在 RAG 管道中的接线方式。

为什么按 token 分块优于按字符切分

朴素的字符切片(每 n 个字符切一刀)有两个硬伤:

  1. 破坏语义边界:可能把一个句子或短语拦腰切断,产生不完整甚至语义扭曲的 chunk;
  2. 尺寸不可控:token 与字符之间不存在固定比例(一个 token 可能是若干字符、一个单词甚至一串标点),用字符数管理 chunk 大小无法保证各 chunk 的 token 数一致,而 LLM 上下文窗口和 embedding 截断都是以 token 为单位的。

更好的做法是按 token 分块,并尽量在句读、段落等逻辑断点处切分。Pathway xpack 的所有 splitter 都遵循这一设计哲学,区别只在于"用什么策略确定切分点"。

统一抽象:BaseSplitter 的输入输出契约

所有 splitter 继承自 BaseSplitter,它是一个 pw.UDF,因此可以直接作为列表达式用在任何 Pathway 表操作中。从源码看,它的契约非常明确:

  • 输入:字符串列,或 (text, metadata) 二元组列(metadata 可以是 dictpw.Json);
  • 输出list[tuple[str, dict]] 列——每个 chunk 与其 metadata 配对;
  • metadata 传播:从同一输入字符串产生的所有 chunk 共享同一份 metadata;若未提供 metadata,则使用空字典(见 BaseSplitter.call)。

这意味着分块不会丢失上游 parser 附加的文件路径、页码等元信息,检索命中 chunk 后仍能回溯到原始文档来源。此外还有一个 NullSplitter,它把整段文本原样作为一个 chunk 返回,适合用于禁用分块或调试管道。

TokenCountSplitter:token 区间 + 标点感知切分

TokenCountSplitter 使用 tiktoken 编码器把文本编码为 token 序列,保证每个 chunk 的 token 数落在 min_tokensmax_tokens 区间内,同时尽量在标点处断句。

Python 用法:

from pathway.xpacks.llm.splitters import TokenCountSplitter

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

在 Pathway templates 框架中,等价地以 YAML 声明:

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

该配置使用与 OpenAI embedding 模型兼容的 cl100k_base 分词器,产出 100–500 token 的 chunk。完整参数与默认值(来自源码构造函数 splitters.py#L215-L227):

参数 默认值 说明
min_tokens 50 单个 chunk 的最小 token 数
max_tokens 500 单个 chunk 的最大 token 数
encoding_name "cl100k_base" tiktoken 编码名,可用编码列表可参考 tiktoken 官方文档

切分算法细节chunk 方法):

  1. 先做 unicodedata.normalize("NFKC", text) 归一化,消除连字等 Unicode 差异(见 _normalize_unicode);
  2. tokenizer.encode_ordinary(text) 得到 token 序列,然后按 max_tokens 滑窗截取;
  3. 在每个窗口内查找最后一个标点位置(PUNCTUATION = [".", "?", "!", "\n"]L213)。若该位置超过了 CHARS_PER_TOKEN * min_tokensCHARS_PER_TOKEN = 3,即以"每 token 约 3 字符"粗略换算最小长度阈值),则把 chunk 截断到标点之后,使断点落在语义边界上;
  4. 截断后的 chunk 重新编码计算实际 token 数,推进滑窗起点,避免重复切出同一段文本。

由于所有参数都存入 self.kwargs,你也可以在 UDF 调用时逐次覆盖构造参数(splitter(text, max_tokens=...)),这是 BaseSplitter 文档中明确支持的用法。

RecursiveSplitter:递归分隔符分块

RecursiveSplitter 同样按 token 度量 chunk 长度,但切分点由一组有序分隔符递归决定:从最粗粒度的分隔符开始尝试,若片段仍超过 chunk_size 就用下一个更细的分隔符继续切,直到所有 chunk 都满足大小约束。默认分隔符为 SEPARATORS = ["\n\n", "\n", " ", ""]L84),即段落 → 换行 → 空格 → 逐字符逐级回退。

Python 用法(针对 Markdown 文档的分隔符配置):

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

对应 templates YAML:

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

完整参数(默认值来自 构造函数):

参数 默认值 说明
chunk_size 500 chunk 上限,单位由 tokenizer 决定(见下)
chunk_overlap 0 相邻 chunk 的重叠量(字符或 token)
separators ["\n\n", "\n", " ", ""] 有序分隔符列表,可替换为面向特定格式(如 Markdown)的列表
is_separator_regex False 分隔符是否按正则解释
encoding_name None tiktoken 编码名
model_name None tiktoken 模型名(如 gpt-4o-mini
hf_tokenizer None HuggingFace tokenizer 实例

长度度量的关键细节:源码 docstring(L93-L95)指出,只有当提供了 encoding_namemodel_namehf_tokenizer 三者之一时,chunk 长度才按 token 数度量;否则 chunk_size/chunk_overlap 的单位退化为字符数。构造时的优先级为 encoding_name > model_name > hf_tokenizer,见 L141-L154 的分支:分别调用 RecursiveCharacterTextSplitter.from_tiktoken_encoderfrom_huggingface_tokenizer,均未提供则构造纯字符模式的 RecursiveCharacterTextSplitter

chunk_overlap 的权衡值得注意:重叠能把跨边界的上下文带到多个 chunk 中,提高召回鲁棒性;但代价是 chunk 总数变多,索引与检索成本随之上升。

底层实现RecursiveSplitter 是对 langchain_text_splitters.RecursiveCharacterTextSplitter 的薄封装(MIT 许可,见 L97 注释),实际切分由 self._splitter.split_text(text) 完成(L156-L158),Pathway 侧负责输入校验与 metadata 传播。

DoclingParser:按文档结构切分

DoclingParser 走的是另一条路线:不依赖 token 或字符计数,而是利用文档固有结构(标题、段落、列表、表格等)确定 chunk 边界,从而保住逻辑章节的完整性——例如不会把一张表格从中间切开。

parsers.py 源码 可以看到关键设计:

  • chunk: bool = TrueL401):默认始终开启分块;如需关闭,在构造函数中设置 chunk=False,此时整份文档作为单个 chunk 返回(L486-L488 中才会创建 _HybridChunker);
  • 分块器为 docling HybridChunker 的定制版 _HybridChunker(merge_peers=True),元数据相似的 chunk 会合并;图片与表格各自独立成 chunk,并附带各自的 caption(chunk 参数 docstring);
  • chunk 的文本包装format_chunk 会把 chunk 组织为 HEADINGS:(逐级标题,带相应数量的 #)+ CONTENT:(正文)+ 可选 CAPTION: 的结构。这种包装把章节标题等上下文直接附加到 chunk 文本上,检索命中后即使不看 metadata 也能知道内容出处,能明显提升检索质量;同时页码会被聚合进 metadata 的 pages 字段便于回溯。

一个需要注意的限制(与官方文档表述一致,源码 docstring 亦有说明):结构分块器对 chunk 长度不敏感,只按结构切分,因此产出的 chunk 长度参差不齐。如果需要大小更均匀的 chunk,可以在 DoclingParser 之后再叠加 TokenCountSplitterRecursiveSplitter 做二次切分,两者组合使用即可兼顾"结构完整性"与"长度可控"。

在 RAG 管道中接线

splitter 的典型落点是向量索引服务。VectorStoreServer 的构造函数直接接受 splitter 参数(vector_store.py#L49),用于把 parser 产出的长文档切成 chunk 后再送入 embedder。question_answering.py 的官方 doctest 给出了完整接线范式:

import pathway as pw
from pathway.xpacks.llm import embedders, splitters, llms, parsers, rerankers
from pathway.xpacks.llm.vector_store import VectorStoreServer
from pathway.udfs import DiskCache, ExponentialBackoffRetryStrategy

my_folder = pw.io.fs.read(path="/PATH/TO/MY/DATA/*", format="binary", with_metadata=True)
parser = parsers.UnstructuredParser()
text_splitter = splitters.TokenCountSplitter(max_tokens=400)
embedder = embedders.OpenAIEmbedder(cache_strategy=DiskCache())

vector_server = VectorStoreServer(
    *sources,
    embedder=embedder,
    splitter=text_splitter,
    parser=parser,
)

数据流为:文件输入 → parser 解析为文本(+metadata)→ splitter 切成 (chunk, metadata) 列表 → embedder 向量化 → 存入向量库。由于 BaseSplitter 的 metadata 传播契约,with_metadata=True 读入的文件路径等信息会一路跟随到每个 chunk。

测试验证

仓库中的 test_splitters.py 覆盖了三种 splitter 的行为:

  • test_tokencountL25-L31):短文本在默认参数下不触发切分,原样输出;
  • test_recursive_from_encoding / test_recursive_from_model_nameL34-L61):用 26 token 的波兰语文本(含 emoji 与变音符号)重复 5 段、以 chunk_size=30 切分,断言恰好产出 5 个 chunk 且 metadata 为空 pw.Json({}),验证了 token 计数与 metadata 传播语义;
  • test_null:验证 NullSplitter 的透传行为。

策略选型小结

场景 推荐方案 理由
纯文本/网页,chunk 大小需严格可控 TokenCountSplitter token 区间硬约束,标点感知断句
Markdown、代码等格式明确的文本 RecursiveSplitter 可定制分隔符列表 + 重叠窗口,粒度逐级回退
PDF、报告等带标题/表格的富文档 DoclingParser(可叠加前两者二次切分) 结构感知,表格/图片独立成块并带 caption

三者都实现统一的 BaseSplitter 接口(字符串或 (text, metadata) 进,(chunk, metadata) 列表出),因此可以在同一管道中自由替换、组合或级联使用。若你还需要更均匀的 chunk 长度而又不想牺牲结构信息,DoclingParser + RecursiveSplitter 的级联是最直接的组合方式。

适用前提TokenCountSplitter 依赖 tiktokenRecursiveSplitter 依赖 langchain_text_splittersDoclingParser 依赖 docling;这些均以可选依赖形式通过 optional_imports("xpack-llm") / optional_imports("xpack-llm-docs") 在首次构造时加载(见 splitters.py#L126-L130parsers.py#L405-L414),使用时需确保已安装对应的 xpack 扩展包。

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