4、文本分割策略:固定长度分割、递归字符分割、语义分割的原理与代码实现
文本分割,说白了就是给大模型喂饭前的「切菜」环节。
你想想看,大模型的上下文窗口再大,也装不下一整本书。所以我们必须把长文本切成合适的小块,再送进去检索。这块做不好,RAG 系统的召回率直接崩盘。
我个人习惯把文本分割比作「切西瓜」——切得太碎,语义全碎了;切得太整,又塞不进模型嘴里。今天咱们就聊聊三种主流的切法:固定长度、递归字符、语义分割。
4.1 固定长度分割:最朴素,也最粗暴
固定长度分割,就是按字符数或 token 数硬切。比如每 512 个字符一刀切下去,不管句子完不完整。
原理:设定一个 chunk_size 和一个 overlap(重叠窗口),然后像切香肠一样逐段切割。
代码实现:
def fixed_length_split(text, chunk_size=512, overlap=50):
chunks = []
start = 0
while start < len(text):
end = start + chunk_size
chunk = text[start:end]
chunks.append(chunk)
start += chunk_size - overlap
return chunks
# 示例
text = "这是一段很长的文本..." * 100
chunks = fixed_length_split(text, chunk_size=200, overlap=30)
print(f"切成了 {len(chunks)} 块")
print(f"第一块内容: {chunks[0][:50]}...")
⚠️ 避坑指南:我曾经在做一个法律文档检索项目时,直接用固定长度切。结果一个「不构成犯罪」被切成了「不构成」和「犯罪」两块。检索时用户问「是否构成犯罪」,系统只召回后半段,直接翻车。所以,固定长度只适合纯技术文档或日志,千万别用在法律、医疗等敏感领域。
4.2 递归字符分割:更聪明一点
递归字符分割,是 LangChain 里最常用的方法。它不按固定长度硬切,而是按「优先级」找分割点。
原理:先尝试用段落分隔符(\n\n)切,如果切出来的块太大,再用句号(。)切,还大就用逗号(,),最后才用空格或字符硬切。说白了,就是「先保语义,再保长度」。
代码实现:
def recursive_character_split(text, chunk_size=500, chunk_overlap=50):
separators = ["\n\n", "\n", "。", ",", " ", ""]
chunks = []
def _split(text, sep_idx):
if len(text) <= chunk_size or sep_idx >= len(separators):
return [text]
sep = separators[sep_idx]
parts = text.split(sep)
result = []
current = ""
for part in parts:
if len(current) + len(part) + len(sep) <= chunk_size:
current += (sep if current else "") + part
else:
if current:
result.append(current)
current = part
if current:
result.append(current)
# 如果切出来的块还是太大,递归用下一个分隔符
final = []
for chunk in result:
if len(chunk) > chunk_size:
final.extend(_split(chunk, sep_idx + 1))
else:
final.append(chunk)
return final
return _split(text, 0)
# 示例
text = "第一章:概述\n\n这是第一段内容。这是第二句。这是第三句,后面还有更多。\n\n第二章:方法\n\n这里开始讲方法了。"
chunks = recursive_character_split(text, chunk_size=50, chunk_overlap=10)
for i, chunk in enumerate(chunks):
print(f"块{i+1}: {chunk}")
💡 我的经验:递归字符分割是我在项目中用得最多的方法。它比固定长度好很多,但也不是万能的。比如处理 Markdown 或代码时,它会把代码块从中间切开,导致语法错误。我建议在分割前先做一层「结构化预处理」,比如把代码块、表格先提取出来单独处理。
4.3 语义分割:最智能,也最贵
语义分割,不是按字符切,而是按「意思」切。它用 embedding 模型判断句子之间的语义相似度,相似度低的地方就是分割点。
原理:把文本切成句子,然后计算相邻句子的 embedding 余弦相似度。如果相似度突然下降(比如从 0.9 降到 0.3),说明话题变了,这里就是一刀。
代码实现:
from sentence_transformers import SentenceTransformer
import numpy as np
def semantic_split(text, model_name='paraphrase-multilingual-MiniLM-L12-v2',
threshold=0.5, min_chunk_len=100, max_chunk_len=500):
# 1. 先按句子切分
sentences = text.replace('。', '。\n').replace('!', '!\n').replace('?', '?\n').split('\n')
sentences = [s.strip() for s in sentences if s.strip()]
# 2. 加载 embedding 模型
model = SentenceTransformer(model_name)
# 3. 计算句子 embedding
embeddings = model.encode(sentences)
# 4. 计算相邻句子相似度
similarities = []
for i in range(len(embeddings) - 1):
sim = np.dot(embeddings[i], embeddings[i+1]) / (
np.linalg.norm(embeddings[i]) * np.linalg.norm(embeddings[i+1])
)
similarities.append(sim)
# 5. 找到分割点(相似度低于阈值的地方)
split_points = [0]
for i, sim in enumerate(similarities):
if sim < threshold:
split_points.append(i + 1)
split_points.append(len(sentences))
# 6. 合并成 chunk,同时控制长度
chunks = []
for i in range(len(split_points) - 1):
start = split_points[i]
end = split_points[i + 1]
chunk = ''.join(sentences[start:end])
# 如果 chunk 太短,合并到上一个
if chunks and len(chunk) < min_chunk_len:
chunks[-1] += chunk
# 如果 chunk 太长,递归切分
elif len(chunk) > max_chunk_len:
sub_chunks = fixed_length_split(chunk, chunk_size=max_chunk_len, overlap=20)
chunks.extend(sub_chunks)
else:
chunks.append(chunk)
return chunks
# 示例
text = "今天天气真好。我们去公园散步吧。\n\n机器学习是一种人工智能技术。它通过数据训练模型。深度学习是机器学习的一个分支。"
chunks = semantic_split(text, threshold=0.4)
for i, chunk in enumerate(chunks):
print(f"语义块{i+1}: {chunk}")
🔑 核心要点:语义分割的效果取决于 embedding 模型的质量。我建议用
BAAI/bge-large-zh-v1.5 或 shibing624/text2vec-base-chinese 这类中文专用模型。阈值一般设在 0.3~0.6 之间,具体要看你的数据分布。嗯,这里要注意:语义分割的计算成本比前两种高很多,不适合实时处理超长文本。
4.4 三种方法对比
| 方法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 固定长度分割 | 速度快、实现简单 | 破坏语义、容易切碎关键信息 | 日志分析、纯技术文档 |
| 递归字符分割 | 兼顾语义和长度、可控性好 | 对结构化内容(代码、表格)不友好 | 通用文档、新闻文章 |
| 语义分割 | 语义完整、检索精度高 | 计算成本高、依赖模型质量 | 学术论文、法律文书、长篇小说 |
💡 实战建议:我一般会先用递归字符分割做 baseline,然后根据召回率评估结果,决定是否升级到语义分割。如果预算有限,可以先用固定长度分割 + 适当 overlap(比如 10%~20%)来兜底。记住,没有完美的分割策略,只有最适合你数据的策略。
好了,文本分割这块就聊到这儿。下一章咱们聊聊 embedding 模型的选择和优化,那可是 RAG 系统的「翻译官」——选对了,检索效率翻倍;选错了,嗯,你懂的。