文本分割器:按字符、按段落、按语义进行文本分割的策略与实战
做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试跑一遍,然后人工检查分割结果,再微调。阈值太高会切得太碎,太低又切不开。
语义分割的三种阈值策略
| 策略 | 说明 | 适用场景 |
|---|---|---|
| 百分位阈值 | 按相似度分布的百分位切分 | 文档长度均匀时 |
| 标准差阈值 | 按相似度标准差切分 | 文档内容波动大时 |
| 四分位阈值 | 按四分位数切分 | 有异常值时 |
五、三种策略对比与选型指南
你可能会问:到底该用哪种?我直接给你一个决策树:
六、实战中的避坑指南
最后,我把自己踩过的坑总结成几条铁律:
- 先看文档结构:拿到文档先扫一眼,看看有没有明显的段落、标题、列表。有结构就用段落分割,没有就用语义分割。
- 重叠是必须的:不管用哪种分割,chunk_overlap 一定要设。我见过太多人因为忘了设重叠,导致检索结果缺胳膊少腿。
- 测试再测试:分割策略没有银弹。我每次都会拿10-20个典型问题做测试,看看检索结果是否合理。
- 中文要特殊处理:中文没有空格分词,按字符分割时一定要用中文标点作为分隔符。我习惯把「。!?」放在 separators 的前面。
我曾经踩过的坑:有一次做医疗知识库,文档里全是「患者出现...」「医生建议...」这种短句。我用语义分割,结果模型把「患者」和「医生」分到了不同片段里,导致检索结果完全对不上。后来我改成按段落分割,配合自定义的 separators,才解决问题。
嗯,文本分割就讲到这里。记住一句话:没有最好的分割策略,只有最适合你文档的策略。动手试试,你会发现很多意想不到的问题。
我的个人习惯:先用段落分割跑一遍,如果效果不好再换语义分割。字符分割只作为兜底方案。这样既保证了效率,又兼顾了质量。