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_size10%~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 的工作逻辑:

  1. 先按 separator 将文本拆成小段
  2. 再将小段合并,直到达到 chunk_size
  3. 超出 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)
工作原理
  1. 先尝试用第一个分隔符 \n\n(段落)切分
  2. 如果某段仍然超过 chunk_size,退而用第二个分隔符 \n(换行)再切
  3. 还超长?继续用 (句号)切……以此类推
  4. 直到所有 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)
工作原理
  1. 将文本按句子拆分
  2. 对每个句子计算 Embedding 向量
  3. 计算相邻句子的余弦相似度
  4. 当相似度低于阈值时,认为语义发生了转折,在此处切分
句子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