第四节:文档分割策略实战
文档分割,说白了就是给大文本“切块”。
我刚开始做RAG时,觉得这步很简单——按字数切不就行了?结果被现实狠狠教育了一回。切得太碎,语义断了;切得太粗,检索精度又不够。今天我把四种主流策略掰开揉碎讲清楚。
4.1 固定大小分割
这是最朴素的方法。按固定字符数切,比如每512个字符一段。
from langchain.text_splitter import CharacterTextSplitter
splitter = CharacterTextSplitter(
chunk_size=512, # 每段512字符
chunk_overlap=50, # 重叠50字符
separator="\n" # 优先按换行切
)
chunks = splitter.split_text(long_text)
我的经验:重叠很重要。我试过不设重叠,结果“深度学习”被切成“深度学”和“习”,检索直接崩了。一般重叠设10%-20%比较稳。
适用场景:
- 纯文本日志、新闻稿
- 对语义连贯性要求不高的场景
- 快速原型验证
注意:固定分割会暴力切断句子。我曾经把“我不认为这是对的”切成“我不认为”和“这是对的”,意思完全反了。慎用。
4.2 递归字符文本分割
这个策略聪明多了。它按优先级尝试不同分隔符:段落、句子、单词、字符。
from langchain.text_splitter import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=50,
separators=["\n\n", "\n", "。", "!", "?", ",", " ", ""]
)
chunks = splitter.split_text(long_text)
为什么会这样设计?你想想看,如果一段文本有自然段落,肯定优先按段落切。段落太长,再按句子切。这样切出来的块,语义完整性好很多。
核心优势:自适应。遇到不同文本结构,它能自动调整切割粒度。我在处理混合格式文档时,90%的情况都用它。
避坑指南:我曾经把中文标点漏了,只写了英文句号。结果中文文档全按字符切了,惨不忍睹。记得把中文标点也加进去。
4.3 语义分割
这是最“聪明”的方法。它用嵌入模型判断语义边界。
from langchain_experimental.text_splitter import SemanticChunker
from langchain_openai.embeddings import OpenAIEmbeddings
splitter = SemanticChunker(
embeddings=OpenAIEmbeddings(),
breakpoint_threshold_type="percentile", # 按百分位断点
breakpoint_threshold_amount=95 # 相似度低于95%就切
)
chunks = splitter.split_documents(docs)
原理很简单:计算相邻句子的语义相似度。相似度突然下降,说明话题变了,就在这切一刀。
我的习惯:阈值设95%比较激进,适合技术文档。如果是小说或散文,我会降到85%,因为文学文本话题过渡更平滑。
优缺点对比:
| 维度 | 语义分割 | 递归分割 |
|---|---|---|
| 语义完整性 | ★★★★★ | ★★★★ |
| 速度 | ★★ | ★★★★★ |
| 成本 | 高(需调用模型) | 低 |
| 适用场景 | 高精度问答 | 通用场景 |
注意:语义分割每次都要调嵌入模型,成本不低。我一般只在生产环境用,开发调试先用递归分割。
4.4 Markdown/代码分割
这两种是“特化”分割器,专为结构化文本设计。
Markdown分割
from langchain.text_splitter import MarkdownHeaderTextSplitter
headers_to_split_on = [
("#", "H1"),
("##", "H2"),
("###", "H3"),
]
splitter = MarkdownHeaderTextSplitter(
headers_to_split_on=headers_to_split_on
)
chunks = splitter.split_text(md_document)
它会按标题层级切分。比如一个H2标题下的所有内容,会被当作一个语义块。我在处理技术博客时,这个特别好用——每个章节天然就是一个独立知识点。
代码分割
from langchain.text_splitter import (
Language,
RecursiveCharacterTextSplitter
)
python_splitter = RecursiveCharacterTextSplitter.from_language(
language=Language.PYTHON,
chunk_size=2000,
chunk_overlap=200
)
code_chunks = python_splitter.split_text(python_code)
它知道Python的函数定义、类定义、if语句块。不会把def foo()和里面的代码切到两个块里。
实战建议:我处理GitHub仓库时,会先用语言检测判断是Python还是Java,再选对应的分割器。代码分割+语义分割组合,效果最好。
4.5 如何选择?
我一般按这个优先级选:
- Markdown/代码:如果文档有明确结构,优先用
- 语义分割:问答精度要求高,预算充足
- 递归分割:通用场景,性价比最高
- 固定分割:只有纯文本且对质量要求不高
嗯,最后说一句:没有银弹。我见过有人花一周调分割参数,不如换个策略来得快。先跑通,再优化,别一开始就追求完美。
下一节我们讲向量化,把切好的块变成机器能理解的数字。到时候你就知道,分割做得好,后面省一半功夫。