RAG 文档切片策略:固定长度 vs 递归 vs 语义切分
RAG 文档切片策略:固定长度 vs 递归 vs 语义切分
本文是【AI 专题精讲】系列第 02 篇。
引言
拿到纯文本后,你不能直接把一整篇文档丢给 AI——10 万字的技术文档光 Token 就超了,而且检索时相关性评分根本没法用。
所以 RAG 第二步是切片(Chunking):把长文本切成合适大小的段落,每段独立做 Embedding 和存储。
text.slice(0, 1000) 不就行了?——不行。切得太粗暴,一句话被拦腰截断,AI 只看到半句话,回答质量直接崩。
切片质量有多重要
假设知识库里有一段年假制度:
年假需提前 3 个工作日提交申请,经直属上级审批后生效。
未使用的年假不可跨年累积,但可在当年 12 月折算为加班工资。
用户问”年假可以跨年吗?“:
-
切得好:检索到包含”不可跨年累积”这句话的片段 → AI 准确回答。 -
切得差:恰好在”不可跨年”中间截断 → AI 回答出错或说”信息不足”。
三个核心参数
chunk_size(切片大小):每段最大字符数。通用场景建议从 800 字符开始。
chunk_overlap(重叠区域):相邻切片之间重叠的字符数,避免关键信息被切断。一般设 chunk_size 的 10%-20%。
separators(分隔符):按优先级使用:\n\n > \n > 。 > . > 空格 > 逐字符
策略一:固定长度切片
最简单——每 N 个字符切一刀。
defsplit_by_fixed_length(text: str, chunk_size: int = 800, chunk_overlap: int = 100) -> list[str]:
chunks = []
start = 0
while start < len(text):
end = start + chunk_size
chunks.append(text[start:end].strip())
start = end - chunk_overlap
return [c for c in chunks if c]
优点:实现极其简单,速度最快。缺点:完全无视语义边界,经常在句子中间截断。
适用场景:快速原型、对切片质量要求不高的场景。
策略二:递归切片(推荐默认方案)
LangChain 的默认实现,也是目前最主流的方案。
核心原理:按分隔符优先级层层递归——先用 \n\n 分,太长再用 \n,再太长用句号,最后才逐字符。
from langchain_text_splitters import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=800,
chunk_overlap=100,
separators=["\n\n", "\n", "。", ".", "!", "?", " ", ""],
)
chunks = splitter.split_text(long_text)
中文场景建议加上中文标点 "。", "!", "?", ";",效果远好于默认的英文分隔符。
优点:尽可能保留完整段落/句子,成熟稳定。缺点:仍然基于字符规则,不理解语义。
适用场景:大多数通用文档,推荐作为默认方案。
策略三:语义切片
最”智能”的方案——不按字符数切,而是按语义相似度切。
原理:
-
先把文本按句子分割 -
对每个句子生成 Embedding 向量 -
计算相邻句子的相似度 -
在相似度突然下降的地方(语义断裂点)切分
defsplit_by_semantic(text: str, threshold: float = 0.5) -> list[str]:
sentences = split_into_sentences(text)
embeddings = get_embeddings(sentences)
similarities = [cosine_similarity(embeddings[i], embeddings[i+1])
for i in range(len(embeddings) - 1)]
chunks = []
current = [sentences[0]]
for i, sim in enumerate(similarities):
if sim < threshold and len("".join(current)) >= 100:
chunks.append("".join(current))
current = [sentences[i + 1]]
else:
current.append(sentences[i + 1])
if current:
chunks.append("".join(current))
return chunks
优点:切片边界最自然,语义完整性最好。缺点:需要调用 Embedding API(有成本),速度最慢。
适用场景:高质量要求的知识库(法律、医疗、金融)。
三种策略对比
|
|
|
|
|
|---|---|---|---|
| 实现复杂度 |
|
|
|
| 切片质量 |
|
|
|
| 速度 |
|
|
|
| 成本 |
|
|
|
| 推荐场景 |
|
通用默认 |
|
实际结论:90% 的场景用递归切片就够了。只有对准确率有极致要求的垂直领域,才上语义切片。
不同内容类型的切片参数
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
代码文件要用代码专用分隔符("\nclass ", "\ndef "),LangChain 也提供了语言专用切片器 RecursiveCharacterTextSplitter.from_language(Language.PYTHON, ...)。
切片要带元数据
@dataclass
classChunk:
content: str
metadata: dict # source、chunk_index、total_chunks、file_type...
这些元数据在检索后回传给 LLM 时非常有用:来源: 技术方案.pdf | 第 3/15 段
量化评估切片质量
用脚本量化,不要凭感觉:
|
|
|
|---|---|
|
|
|
|
|
|
|
|
|
递归切片的边界完整性(0.857)远高于固定长度(0.316)。
总结
-
切片质量直接决定 RAG 效果——garbage chunk in, garbage answer out。 -
递归切片是 90% 场景的最佳选择,用 RecursiveCharacterTextSplitter加上中文标点。 -
不同内容类型需要不同参数:代码大 chunk,FAQ 小 chunk,合同大 overlap。 -
切片要带元数据(来源、序号、总数),方便检索溯源。 -
用量化指标评估:平均长度、边界完整性,不要凭感觉。
关注本公众号,持续更新【AI 专题精讲】系列。下一篇:RAG 大文件上传,分片上传 + 断点续传 + 实时进度追踪完整方案。
夜雨聆风