文本分割器:按字符、按段落、按语义进行文本分割的策略与实战

做RAG系统这么久,我踩过最大的坑就是文本分割。你想想看,一个知识库文档,如果切得太碎,上下文全丢了;切得太粗,检索又不够精准。说白了,文本分割就是RAG系统的「第一道关卡」,过不了这关,后面检索、生成全是白搭。

今天我就把三种主流分割策略——按字符、按段落、按语义——掰开揉碎了讲清楚。每种策略我都附上了实战代码和避坑指南,都是我亲手填过的坑。

一、为什么文本分割这么重要?

先看一个真实场景。我去年帮一家金融公司做智能客服,他们的产品手册有300多页。一开始我偷懒,直接用固定字符数切分,结果用户问「年化收益率怎么算」,系统检索出来的片段里只有「年化」两个字,上下文全丢了。嗯,这就是典型的「切碎了」。

文本分割的核心目标就两个:

  • 保持语义完整性:每个片段应该是一个相对独立的知识单元
  • 控制片段长度:既要满足LLM的上下文窗口限制,又要保证检索效率

我个人习惯把分割策略分成三个层次,从简单到复杂:

核心原则:宁可多切几刀,也别让语义断在奇怪的地方。

二、按字符分割:最基础,也最容易踩坑

按字符分割,说白了就是固定长度切分。比如每500个字符切一段,或者每1000个token切一段。这是最原始的方法,但也是最容易出问题的。

我曾经在一个法律文档项目中用过这种方法,结果把「中华人民共和国」从中间切开了,检索出来的片段变成了「中华人」和「民共和国」——这谁看得懂?

实战代码:按字符分割

from langchain.text_splitter import CharacterTextSplitter

# 基础字符分割器
text_splitter = CharacterTextSplitter(
    chunk_size=500,        # 每段500字符
    chunk_overlap=50,      # 重叠50字符,避免边界信息丢失
    separator="\n",        # 优先按换行符分割
    length_function=len
)

text = """第一章 总则
第一条 为了保护消费者权益...
第二条 经营者应当遵守...
第三条 消费者享有知情权..."""

chunks = text_splitter.split_text(text)
for i, chunk in enumerate(chunks[:3]):
    print(f"片段 {i+1}: {chunk[:50]}...")

我的经验:chunk_overlap 一定要设置。我一般设 chunk_size 的10%-20%。这样即使边界被切到,重叠部分也能把上下文带回来。

什么时候用字符分割?

  • 文档结构非常规整,比如每行就是一条记录
  • 对性能要求极高,字符分割最快
  • 作为其他分割方法的兜底方案

避坑指南:千万不要对中文文档直接用空格或标点分割。中文没有天然的分隔符,按字符硬切大概率会切碎语义。我曾经吃过这个亏,后来改用段落分割才解决。

三、按段落分割:更符合人类阅读习惯

按段落分割,说白了就是尊重文档的自然结构。一个段落通常表达一个完整的意思,按段落切分能最大程度保留语义。

我记得有一次处理技术文档,文档里每个段落都是一个独立的知识点。用段落分割后,检索准确率直接从65%飙到了89%。

实战代码:按段落分割

from langchain.text_splitter import RecursiveCharacterTextSplitter

# 递归段落分割器
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000,
    chunk_overlap=100,
    separators=["\n\n", "\n", "。", "!", "?", ";", ",", " ", ""],
    # 优先按段落分割,不行再按句子,最后按字符
)

text = """第一章 总则

第一条 为了保护消费者权益,制定本法。

第二条 经营者应当遵守诚实信用原则。

第三条 消费者享有知情权和自主选择权。"""

chunks = text_splitter.split_text(text)
for i, chunk in enumerate(chunks):
    print(f"片段 {i+1}: {chunk[:60]}...")

关键点:separators 的顺序很重要。我习惯把「\n\n」(空行)放在最前面,因为空行通常就是段落边界。如果一段太长,再按句子切。

段落分割的优缺点

优点 缺点
语义完整,符合阅读习惯 段落长度可能差异很大
实现简单,性能好 对非结构化文档效果差
适合大多数技术文档 无法处理跨段落的语义关联

四、按语义分割:最智能,也最复杂

按语义分割,说白了就是让模型自己判断哪里该切。比如用BERT或者Sentence Transformer计算句子之间的相似度,相似度低的地方就是分割点。

我去年做一个学术论文检索系统,论文里经常出现「然而」「但是」这种转折词。按段落分割会把转折前后的内容切到不同片段里,导致检索结果自相矛盾。后来改用语义分割,这个问题就解决了。

实战代码:按语义分割

from langchain.text_splitter import SemanticChunker
from langchain.embeddings import HuggingFaceEmbeddings

# 语义分割器
embeddings = HuggingFaceEmbeddings(
    model_name="shibing624/text2vec-base-chinese"
)

text_splitter = SemanticChunker(
    embeddings=embeddings,
    breakpoint_threshold_type="percentile",  # 按百分位阈值分割
    breakpoint_threshold_amount=0.3          # 相似度低于30%就切
)

text = """机器学习是人工智能的核心。
它通过数据训练模型,让计算机学会预测。
深度学习是机器学习的一个分支。
它使用多层神经网络处理复杂问题。"""

chunks = text_splitter.split_text(text)
for i, chunk in enumerate(chunks):
    print(f"片段 {i+1}: {chunk}")

我的建议:语义分割的阈值需要反复调。我一般先用0.3试跑一遍,然后人工检查分割结果,再微调。阈值太高会切得太碎,太低又切不开。

语义分割的三种阈值策略

策略 说明 适用场景
百分位阈值 按相似度分布的百分位切分 文档长度均匀时
标准差阈值 按相似度标准差切分 文档内容波动大时
四分位阈值 按四分位数切分 有异常值时

五、三种策略对比与选型指南

你可能会问:到底该用哪种?我直接给你一个决策树:

文本分割策略选型决策树 文档来了 结构规整? 结构不规整? 按段落分割 按语义分割 段落长度均匀 → 直接切 段落长度不均 → 递归切 内容连贯 → 百分位阈值 内容跳跃 → 标准差阈值 兜底方案:按字符分割

六、实战中的避坑指南

最后,我把自己踩过的坑总结成几条铁律:

  1. 先看文档结构:拿到文档先扫一眼,看看有没有明显的段落、标题、列表。有结构就用段落分割,没有就用语义分割。
  2. 重叠是必须的:不管用哪种分割,chunk_overlap 一定要设。我见过太多人因为忘了设重叠,导致检索结果缺胳膊少腿。
  3. 测试再测试:分割策略没有银弹。我每次都会拿10-20个典型问题做测试,看看检索结果是否合理。
  4. 中文要特殊处理:中文没有空格分词,按字符分割时一定要用中文标点作为分隔符。我习惯把「。!?」放在 separators 的前面。

我曾经踩过的坑:有一次做医疗知识库,文档里全是「患者出现...」「医生建议...」这种短句。我用语义分割,结果模型把「患者」和「医生」分到了不同片段里,导致检索结果完全对不上。后来我改成按段落分割,配合自定义的 separators,才解决问题。

嗯,文本分割就讲到这里。记住一句话:没有最好的分割策略,只有最适合你文档的策略。动手试试,你会发现很多意想不到的问题。

我的个人习惯:先用段落分割跑一遍,如果效果不好再换语义分割。字符分割只作为兜底方案。这样既保证了效率,又兼顾了质量。

专注资料整理