第四节:文档分割策略实战

文档分割,说白了就是给大文本“切块”。

我刚开始做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 如何选择?

我一般按这个优先级选:

  1. Markdown/代码:如果文档有明确结构,优先用
  2. 语义分割:问答精度要求高,预算充足
  3. 递归分割:通用场景,性价比最高
  4. 固定分割:只有纯文本且对质量要求不高

嗯,最后说一句:没有银弹。我见过有人花一周调分割参数,不如换个策略来得快。先跑通,再优化,别一开始就追求完美。

下一节我们讲向量化,把切好的块变成机器能理解的数字。到时候你就知道,分割做得好,后面省一半功夫。