文档分割位于“文档解析”和“向量化”之间。它决定知识以什么粒度进入向量数据库,也直接影响检索召回率、上下文完整性和最终回答质量。
一、为什么需要文档分割?
原始文档通常篇幅较长,直接将整篇文档向量化会带来几个问题:
- 单个向量包含多个主题,语义表达不够聚焦;
- 检索命中后会携带大量无关内容;
- 文本可能超过 Embedding 模型或大模型的上下文限制;
- 无法精确定位答案所在的段落、页码或章节。
因此,需要把文档拆成若干较小且语义相对完整的Chunk:
原始文档 ↓ 清洗与解析 结构化文本 ↓ 文档分割 Chunk 1 / Chunk 2 / Chunk 3 / ... ↓ Embedding 向量 + 原文 + 元数据二、五类分割方式对比
| 分割方式 | 核心做法 | 优点 | 局限 |
|---|---|---|---|
| 按句子分割 | 一个或多个完整句子组成一个 Chunk | 边界自然、实现简单 | 句子过短时上下文不足 |
| 按字符数切分 | 达到固定长度后直接切开 | Chunk 大小稳定 | 容易从句子或词语中间截断,语义不连贯 |
| 固定分隔符 + 重叠 | 按换行、段落等边界切分,并保留相邻重叠内容 | 能缓解边界信息丢失 | 分隔符不稳定时效果有限 |
| 递归分割 | 依次尝试段落、换行、句子、空格等分隔符 | 兼顾长度控制与语义完整性 | 需要根据语言和文档结构配置分隔符 |
| 语义分割 | 根据 Embedding 相似度或语义变化点确定边界 | 更贴近真实主题 | 计算成本更高,阈值和模型会影响结果 |
实际 RAG 项目中,递归分割通常是更稳妥的默认方案;固定字符切分更适合格式简单、结构稳定的文本。
三、三个关键参数
1.chunk_size
表示单个 Chunk 的目标最大长度。设置过大会让一个 Chunk 混入多个主题;设置过小则会丢失上下文。
2.chunk_overlap
表示相邻 Chunk 之间重复保留的内容,用于避免答案恰好落在切分边界上。
Chunk 1:ABCDEFGHIJ Chunk 2: GHIJKLMNOP └─ overlap ─┘重叠并非越大越好。重叠过大会增加向量数量、存储成本,并让检索结果出现大量重复内容。
3.separators
递归分割会按照分隔符优先级逐级尝试。中文文本可优先保留段落和完整句子:
separators=["\n\n","\n","。","!","?",";",","," ",""]最后的空字符串""是兜底分隔符,确保超长文本最终仍能被切开。
四、固定字符分割示例
CharacterTextSplitter适用于分隔规则比较明确的文本:
fromlangchain_text_splittersimportCharacterTextSplitter text="""RAG 包含检索和生成两个阶段。文档需要先完成清洗、分割和向量化。用户提问后,系统会召回相关片段并交给大模型生成答案。"""splitter=CharacterTextSplitter(separator="\n\n",chunk_size=50,chunk_overlap=10,length_function=len,)documents=splitter.create_documents([text])forindex,documentinenumerate(documents,start=1):print(f"Chunk{index}:{document.page_content!r}")参数含义:
separator:优先使用的切分边界;chunk_size:目标块大小;chunk_overlap:相邻块的重叠长度;length_function:长度计算方式,这里使用 Python 的len。
五、递归分割示例
RecursiveCharacterTextSplitter会从前到后尝试多个分隔符;只有文本仍然过长时,才继续使用更细粒度的分隔符。
fromlangchain_text_splittersimportRecursiveCharacterTextSplitter text="""第一章 RAG 的基本流程 RAG 会先从知识库检索与问题相关的内容,再让大模型基于检索结果生成答案。 第二章 文档分割 合理的 Chunk 应尽量表达一个完整主题,同时满足 Embedding 模型的长度限制。"""splitter=RecursiveCharacterTextSplitter(separators=["\n\n","\n","。","!","?",";",","," ",""],chunk_size=80,chunk_overlap=15,length_function=len,is_separator_regex=False,)documents=splitter.create_documents(texts=[text],metadatas=[{"source":"rag_notes.md","chapter":"文档分割"}],)forindex,documentinenumerate(documents,start=1):print(f"Chunk{index}")print(document.page_content)print(document.metadata)print("-"*40)这段代码还展示了一个重要实践:切分时同步保留source、章节、页码等元数据,方便后续引用和溯源。
六、递归分割的工作过程
假设分隔符优先级如下:
[段落边界, 换行, 句号, 逗号, 空格, 字符]处理流程为:
- 先尝试按段落切分;
- 如果某个段落仍超过
chunk_size,继续按换行切分; - 仍然过长时,再依次尝试句号、逗号和空格;
- 最后才按字符强制切开;
- 合并较短片段,并保留设定的
chunk_overlap。
这样既能限制 Chunk 长度,又能尽可能保留自然语言结构。
七、场景选择建议
没有适用于所有项目的固定数值,应根据数据和问题类型进行评估:
| 场景 | 推荐策略 |
|---|---|
| FAQ、短问答 | 较小 Chunk,突出单一问题与答案 |
| 产品手册、技术文档 | 按标题和段落递归切分,保留适度重叠 |
| 合同、制度文件 | 优先按条款切分,并保留条款编号 |
| 代码文档 | 按类、函数或代码块切分,不要从函数中间截断 |
| 扫描 PDF | 先进行 OCR 和版面恢复,再按章节或段落切分 |
调参时重点观察:
- 检索结果是否包含完整答案;
- Top-K 结果中是否出现大量重复 Chunk;
- 一个 Chunk 内是否混入多个无关主题;
- 标题、页码和来源信息是否正确保留;
- 召回内容拼接后是否超过模型上下文限制。
八、常见问题与优化方向
问题 1:答案被切在两个 Chunk 中
适当增加chunk_overlap,或优先使用句子、段落等自然边界。
问题 2:检索结果重复度高
减小重叠比例,并在检索后增加去重或重排步骤。
问题 3:表格和代码被切碎
先识别内容类型,再使用表格、Markdown 标题、函数或代码块专用的切分规则。
问题 4:Chunk 语义仍然混乱
采用“结构化切分 + 递归切分”的组合策略;对复杂文档可进一步使用语义分割。
九、核心结论
- 文档分割的目标不是机械地把文本切短,而是生成大小可控、语义完整、能够追溯的知识单元。
chunk_size决定信息粒度,chunk_overlap用于补偿边界信息丢失。- 固定长度切分简单,但容易破坏语义;递归切分更适合作为通用默认方案。
- 中文文本应补充中文标点分隔符,避免只配置英文句号和空格。
- 最终参数必须结合真实问题集,通过召回结果和回答质量持续评估。
一句话总结:好的文档分割,要在“长度可控”和“语义完整”之间取得平衡。