RAG之文件分片的几种方式
RAG 文件分片的几种方式
一、为什么分片如此重要?
在 RAG 流程中,分片(Chunking)是检索质量的上游瓶颈——分片没做好,检索再精准也白搭。
原因很简单:
● 太长:一个 Chunk 塞进几千字,检索时噪声太多,LLM 也难以聚焦关键信息
● 太短:一个 Chunk 只有一句话,丢失上下文,检索到的片段拼不出完整语义
● 切错位置:把一个完整段落从中间截断,前半句和后半句各丢失一半语义
所以分片的核心目标是:让每个 Chunk 既包含足够的上下文,又不混入无关信息。
二、分片的核心参数
在讲具体方式之前,先理解两个最关键的参数:
chunk_size(分片大小)
每个 Chunk 的最大字符数(或 Token 数)。
| 设置 | 优点 | 缺点 |
|---|---|---|
| 小(200~300) | 检索精准,噪声少 | 上下文不完整,可能断章取义 |
| 大(800~1500) | 上下文完整,语义连贯 | 检索噪声多,可能混入无关内容 |
经验值:中文场景 300-800 字符,英文场景 500-1000 Token。
chunk_overlap(重叠大小)
相邻 Chunk 之间重叠的字符数,防止语义在边界处被截断。
Chunk 1: [AAAAA][BBB]
Chunk 2: [BBB][CCCCC]
Chunk 3: [CCC][DDDDD]
● 一般设为 chunk_size 的 10%~20%
● 重叠太大 → 冗余增多,存储和检索成本上升
● 重叠太小 → 边界语义丢失
三、固定长度分片
最简单粗暴的方式:按固定字符数切分,不够就补齐或丢弃。
text = "这是一段很长的文本..." * 100
chunk_size = 200
overlap = 50
chunks = []
start = 0
while start < len(text):
chunks.append(text[start:start + chunk_size])
start += chunk_size - overlap
优点
● 实现简单,无需理解文本结构
● Chunk 大小均匀,便于控制
缺点
● 完全不考虑语义边界,可能把句子、段落从中间截断
● 适合对质量要求不高的快速验证场景
适用场景
● 纯日志、无结构的纯文本
● 快速原型验证
四、基于分隔符的分片
按照预设的分隔符(换行、句号、段落标记等)进行切分,尊重文本的自然边界。
from langchain_text_splitters import CharacterTextSplitter
splitter = CharacterTextSplitter(
separator="\n\n", # 优先按双换行(段落)切分
chunk_size=500,
chunk_overlap=50
)
chunks = splitter.split_text(text)
CharacterTextSplitter 的工作逻辑:
- 先按
separator将文本拆成小段 - 再将小段合并,直到达到
chunk_size - 超出
chunk_size的部分拆出新 Chunk
优点
● 保留段落/句子完整性,不会从中间截断
● 实现仍然简单
缺点
● 依赖分隔符的正确性,如果原文格式不规范(如缺少换行),效果会大打折扣
● 只能按单一分隔符切分,无法兼顾多种语义边界
适用场景
● 格式规范的 Markdown、HTML、纯文本
● 段落结构清晰的文档
五、递归字符分片(推荐)
LangChain 中最常用的分片方式,按多个分隔符优先级递归切分,是目前最通用的方案。
from langchain_text_splitters import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
separators=["\n\n", "\n", "。", "!", "?", ".", " ", ""],
chunk_size=500,
chunk_overlap=50
)
chunks = splitter.split_text(text)
工作原理
- 先尝试用第一个分隔符
\n\n(段落)切分 - 如果某段仍然超过
chunk_size,退而用第二个分隔符\n(换行)再切 - 还超长?继续用
。(句号)切……以此类推 - 直到所有 Chunk 都不超过
chunk_size
段落切分 → 换行切分 → 句号切分 → 空格切分 → 字符切分
(粗粒度)────────────────────────────────→(细粒度)
优点
● 自动适配不同粒度的语义边界,兼顾完整性和大小控制
● 中文场景下加入 。!? 等标点,效果显著优于纯按换行切分
● 通用性强,绝大多数文档都能处理
缺点
● 仍然基于字符级规则,无法理解深层语义
● 对于代码、表格等特殊结构,效果一般
适用场景
● 大多数 RAG 项目的默认选择
● 技术文档、文章、书籍等长文本
六、基于文档结构的分片
针对特定格式的文档(Markdown、HTML、代码),利用其结构标签进行智能切分。
Markdown 分片
from langchain_text_splitters import MarkdownHeaderTextSplitter
markdown_text = """
# 第一章 概述
这是概述内容。
## 1.1 背景
这是背景内容。
## 1.2 目标
这是目标内容。
"""
headers_to_split_on = [
("#", "h1"),
("##", "h2"),
("###", "h3"),
]
splitter = MarkdownHeaderTextSplitter(headers_to_split_on)
chunks = splitter.split_text(markdown_text)
切分结果会保留标题层级作为元数据:
# Chunk 1
page_content="这是概述内容。"
metadata={"h1": "第一章 概述"}
# Chunk 2
page_content="这是背景内容。"
metadata={"h1": "第一章 概述", "h2": "1.1 背景"}
# Chunk 3
page_content="这是目标内容。"
metadata={"h1": "第一章 概述", "h2": "1.2 目标"}
● 元数据可以在检索时用于过滤(如只检索某个章节的内容)
● 还可以配合 RecursiveCharacterTextSplitter 二次切分过长的段落
HTML 分片
from langchain_text_splitters import HTMLSectionSplitter
splitter = HTMLSectionSplitter(
sections_to_split_on=[("h1", "h1"), ("h2", "h2"), ("h3", "h3")]
)
chunks = splitter.split_text(html_text)
代码分片
from langchain_text_splitters import Language, RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter.from_language(
language=Language.PYTHON,
chunk_size=500,
chunk_overlap=50
)
chunks = splitter.split_text(python_code)
代码分片会按类定义、函数定义等语法结构切分,不会把一个函数从中间截断。
优点
● 充分利用文档结构,切分粒度最合理
● 保留元数据,支持检索过滤
缺点
● 只适用于对应格式的文档,通用性差
● 需要文档格式规范,结构混乱的文档效果不佳
适用场景
● Markdown 文档 → MarkdownHeaderTextSplitter
● 网页内容 → HTMLSectionSplitter
● 代码文件 → RecursiveCharacterTextSplitter.from_language()
七、语义分片
前面所有方式都是基于规则和字符的,语义分片则利用 Embedding 模型来判断句子间的语义相关性,在语义转折处切分。
from langchain_experimental.text_splitter import SemanticChunker
from langchain_openai import OpenAIEmbeddings
splitter = SemanticChunker(
embeddings=OpenAIEmbeddings(),
breakpoint_threshold_type="percentile" # 断点判定方式
)
chunks = splitter.split_text(text)
工作原理
- 将文本按句子拆分
- 对每个句子计算 Embedding 向量
- 计算相邻句子的余弦相似度
- 当相似度低于阈值时,认为语义发生了转折,在此处切分
句子1 ←相似度高→ 句子2 ←相似度高→ 句子3 ←相似度低→ 句子4
|_________ Chunk 1 __________| |___ Chunk 2 ___|
断点判定方式
| 方式 | 说明 |
|---|---|
percentile |
默认值,相似度低于第 75 百分位时切分 |
standard_deviation |
低于均值减 1 个标准差时切分 |
interquartile |
基于四分位距判定 |
gradient |
基于相似度变化的梯度判定 |
优点
● 真正基于语义切分,Chunk 内语义高度相关
● 不依赖固定字符数和分隔符规则
缺点
● 需要调用 Embedding 模型,成本高、速度慢
● Chunk 大小不可控,可能出现过短或过长的 Chunk
● 仍处于实验阶段(langchain_experimental),稳定性待验证
适用场景
● 对检索质量要求极高的场景
● 文档结构混乱、无法用规则切分的情况
八、分片方式对比总结
| 分片方式 | 原理 | Chunk 大小可控 | 语义完整性 | 成本 | 推荐场景 |
|---|---|---|---|---|---|
| 固定长度 | 按字符数切 | ✅ | ❌ | 低 | 快速验证 |
| 分隔符切分 | 按单一分隔符切 | ✅ | ⚠️ | 低 | 格式规范文本 |
| 递归字符 | 多分隔符递归切 | ✅ | ✅ | 低 | 通用首选 |
| 文档结构 | 按标题/标签切 | ⚠️ | ✅ | 低 | Markdown/HTML/代码 |
| 语义分片 | Embedding 相似度切 | ❌ | ✅✅ | 高 | 高精度需求 |
九、实际项目中的选择建议
第一优先级:递归字符分片
绝大多数项目用 RecursiveCharacterTextSplitter 就够了,成本低、效果好、通用性强。
组合策略:结构分片 + 递归字符分片
对于 Markdown 等结构化文档,先用 MarkdownHeaderTextSplitter 按标题切出大段,再对过长的段落用 RecursiveCharacterTextSplitter 二次切分,兼顾结构和大小控制。
from langchain_text_splitters import MarkdownHeaderTextSplitter, RecursiveCharacterTextSplitter
# 第一步:按标题结构切分
md_splitter = MarkdownHeaderTextSplitter(headers_to_split_on=[("##", "h2")])
md_chunks = md_splitter.split_text(markdown_text)
# 第二步:对过长的段落二次切分
text_splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
final_chunks = text_splitter.split_documents(md_chunks)
特殊场景
● 代码文件:用 from_language() 按语法结构切分
● 表格数据:尽量整表保留,避免拆散行;或转为自然语言描述后再切分
● PDF 文档:先提取文本(注意表格和图片丢失问题),再按常规方式切分
● 极高精度需求:尝试语义分片,但要做好成本和不可控大小的心理准备
十、分片之外的优化
分片只是 RAG 质量的一环,还有几个配套优化方向:
● Chunk 元数据增强:在 Chunk 前追加文档标题、章节路径,提升检索命中率
● Parent-Child 检索:检索时用小 Chunk 精准匹配,返回时用大 Chunk 提供完整上下文
● 重排序(Reranker):检索后用交叉编码器对结果重新排序,把最相关的排到前面
● 混合检索:结合关键词检索(BM25)和向量检索,取长补短
分片是基础,但不是全部——好的分片 + 好的检索策略 = 高质量 RAG。