Pathway LLM Xpack 文本分块指南:TokenCountSplitter、RecursiveSplitter 与 DoclingParser 的深度解析
将整篇文档嵌入为单个向量往往导致检索效果不佳:模型被迫把文档的全部信息压缩进一个向量表示,难以捕获细粒度细节,重要上下文随之丢失,检索召回质量下降。Pathway 的 LLM xpack 为此提供了三种互补的分块(chunking/splitter)方案——基于 token 计数的 TokenCountSplitter、基于递归分隔符的 RecursiveSplitter,以及基于文档结构本身的 DoclingParser 内置分块器。本文基于 splitters 官方指南 与 splitters 源码 展开,讲清每种分块策略的原理、参数默认值与在 RAG 管道中的接线方式。
为什么按 token 分块优于按字符切分
朴素的字符切片(每 n 个字符切一刀)有两个硬伤:
- 破坏语义边界:可能把一个句子或短语拦腰切断,产生不完整甚至语义扭曲的 chunk;
- 尺寸不可控:token 与字符之间不存在固定比例(一个 token 可能是若干字符、一个单词甚至一串标点),用字符数管理 chunk 大小无法保证各 chunk 的 token 数一致,而 LLM 上下文窗口和 embedding 截断都是以 token 为单位的。
更好的做法是按 token 分块,并尽量在句读、段落等逻辑断点处切分。Pathway xpack 的所有 splitter 都遵循这一设计哲学,区别只在于"用什么策略确定切分点"。
统一抽象:BaseSplitter 的输入输出契约
所有 splitter 继承自 BaseSplitter,它是一个 pw.UDF,因此可以直接作为列表达式用在任何 Pathway 表操作中。从源码看,它的契约非常明确:
- 输入:字符串列,或
(text, metadata)二元组列(metadata 可以是dict或pw.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_tokens 到 max_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 方法):
- 先做
unicodedata.normalize("NFKC", text)归一化,消除连字等 Unicode 差异(见_normalize_unicode); - 用
tokenizer.encode_ordinary(text)得到 token 序列,然后按max_tokens滑窗截取; - 在每个窗口内查找最后一个标点位置(
PUNCTUATION = [".", "?", "!", "\n"],L213)。若该位置超过了CHARS_PER_TOKEN * min_tokens(CHARS_PER_TOKEN = 3,即以"每 token 约 3 字符"粗略换算最小长度阈值),则把 chunk 截断到标点之后,使断点落在语义边界上; - 截断后的 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_name、model_name 或 hf_tokenizer 三者之一时,chunk 长度才按 token 数度量;否则 chunk_size/chunk_overlap 的单位退化为字符数。构造时的优先级为 encoding_name > model_name > hf_tokenizer,见 L141-L154 的分支:分别调用 RecursiveCharacterTextSplitter.from_tiktoken_encoder 或 from_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 = True(L401):默认始终开启分块;如需关闭,在构造函数中设置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 之后再叠加 TokenCountSplitter 或 RecursiveSplitter 做二次切分,两者组合使用即可兼顾"结构完整性"与"长度可控"。
在 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_tokencount(L25-L31):短文本在默认参数下不触发切分,原样输出;test_recursive_from_encoding/test_recursive_from_model_name(L34-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 依赖 tiktoken,RecursiveSplitter 依赖 langchain_text_splitters,DoclingParser 依赖 docling;这些均以可选依赖形式通过 optional_imports("xpack-llm") / optional_imports("xpack-llm-docs") 在首次构造时加载(见 splitters.py#L126-L130 与 parsers.py#L405-L414),使用时需确保已安装对应的 xpack 扩展包。
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 StartedRust0623
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