乐于分享
好东西不私藏

RAG 文档切片策略:固定长度 vs 递归 vs 语义切分

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)

中文场景建议加上中文标点 "。", "!", "?", ";",效果远好于默认的英文分隔符。

优点:尽可能保留完整段落/句子,成熟稳定。缺点:仍然基于字符规则,不理解语义。

适用场景:大多数通用文档,推荐作为默认方案。


策略三:语义切片

最”智能”的方案——不按字符数切,而是按语义相似度切。

原理

  1. 先把文本按句子分割
  2. 对每个句子生成 Embedding 向量
  3. 计算相邻句子的相似度
  4. 在相似度突然下降的地方(语义断裂点)切分
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(有成本),速度最慢。

适用场景:高质量要求的知识库(法律、医疗、金融)。


三种策略对比

对比项
固定长度
递归切片
语义切片
实现复杂度
★★
★★★★
切片质量
★★
★★★★
★★★★★
速度
★★★★★
★★★★
★★
成本
需要 Embedding API
推荐场景
快速原型
通用默认
高精度场景

实际结论:90% 的场景用递归切片就够了。只有对准确率有极致要求的垂直领域,才上语义切片。


不同内容类型的切片参数

场景
chunk_size
chunk_overlap
通用文档
800
100
FAQ 知识库
300-500
50
法律合同
500-700
150
代码文件
1500
200

代码文件要用代码专用分隔符("\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)。


总结

  1. 切片质量直接决定 RAG 效果——garbage chunk in, garbage answer out。
  2. 递归切片是 90% 场景的最佳选择,用 RecursiveCharacterTextSplitter 加上中文标点。
  3. 不同内容类型需要不同参数:代码大 chunk,FAQ 小 chunk,合同大 overlap。
  4. 切片要带元数据(来源、序号、总数),方便检索溯源。
  5. 用量化指标评估:平均长度、边界完整性,不要凭感觉。

关注本公众号,持续更新【AI 专题精讲】系列。下一篇:RAG 大文件上传,分片上传 + 断点续传 + 实时进度追踪完整方案。