Pathway xpacks LLM 文本切块实战指南:TokenCountSplitter、RecursiveSplitter 与 DoclingParser 的 RAG 分块机制
本文基于 Pathway Live Data Framework(下称 Pathway)的开发者文档 docs/2.developers/4.user-guide/50.llm-xpack/.splitters/splitters.md 展开,系统讲解 RAG(检索增强生成)流水线中文档分块(chunking)的三种实现——TokenCountSplitter、RecursiveSplitter 与 DoclingParser——并深入源码 python/pathway/xpacks/llm/splitters.py 剖析其切分算法、参数默认值与测试验证方式。读完后,你将能够在自己的实时 RAG 管道中正确选型、配置分块器,并理解每个参数在底层是如何生效的。
为什么 RAG 必须先分块
将整篇文档作为一个向量嵌入,往往会导致检索质量下降:嵌入模型被迫把整篇文档的信息压缩进单一向量表示,难以捕获细粒度细节,重要上下文可能丢失,检索效果随之变差。
文档中还指出,简单的“每 n 个字符切一刀”策略存在两个问题:
- 切分点生硬:会截断句子或短语,产生不完整、语义扭曲的块;
- 粒度不一致: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(如 UnstructuredParser、DoclingParser)天然衔接:parser 产出的每个文档块都带有来源、标题等元数据,splitter 会把这些元数据原样复制到每个子块上,供后续嵌入与检索阶段使用。
此外,模块还提供一个极简的 NullSplitter(L161-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.kwargs 且 chunk() 支持 **kwargs 覆盖,所有默认参数都可以在 UDF 调用时逐次覆盖,例如 splitter(pw.this.text, max_tokens=300)。传入未知参数会直接抛出 ValueError(L251-L252),有助于尽早发现配置笔误。
底层切分算法
从 chunk() 的实现(L229-L272)可以读出具体流程:
- 先用
_normalize_unicode对文本做NFKC规范化(L14-L18),消除连字(ligatures)等 Unicode 变体,保证 token 计数稳定; - 用
tiktoken.get_encoding(...)得到编码,encode_ordinary把全文切成 token 序列; - 按
max_tokens依次截取 token 窗口并解码回文本; - 标点回退:在块内查找最后一个标点位置(
PUNCTUATION = [".", "?", "!", "\n"],见 L213),若其位置超过CHARS_PER_TOKEN * min_tokens(其中CHARS_PER_TOKEN = 3,即按“约 3 字符/token”粗略估计),就把块截断到该标点处,避免在句中被拦腰截断; - 用截断后文本重新编码的 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,接入DocumentStore与OpenAIEmbedder; - 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"
工作原理
RecursiveSplitter 与 TokenCountSplitter 一样以 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_name或model_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_encoding(tests/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_name(L50-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,内部封装docling的DocumentConverter,并额外支持用视觉 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 之上再套一层 TokenCountSplitter 或 RecursiveSplitter。从类型约定看这是天然可行的——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 之间,并随源数据更新自动增量重算分块结果。
参考文件
- 文档源文件:docs/2.developers/4.user-guide/50.llm-xpack/.splitters/splitters.md(模板版同一内容位于 docs/2.developers/7.templates/40.rag-customization/40.splitters.md)
- 分块器实现:python/pathway/xpacks/llm/splitters.py
DoclingParser实现:python/pathway/xpacks/llm/parsers.py- 单元测试:python/pathway/xpacks/llm/tests/test_splitters.py
- 集成测试(HF tokenizer 路径):integration_tests/xpack/test_splitters.py
- 示例项目:examples/projects/question-answering-rag/main.py、examples/projects/conf42/main.py
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00