跳到主要内容

文档分块策略

文档分块(Chunking)是 RAG 系统中最关键也最容易被忽视的环节。分块质量直接影响检索精度:分块太大,噪音多、语义混杂;分块太小,上下文缺失、信息碎片化。本章将深入分析各种分块策略的原理和适用场景。

为什么需要分块?

直接将整篇文档作为检索单位存在几个问题:

上下文长度限制:大模型的上下文窗口有限,无法一次性处理所有文档。

假设知识库有 100 篇文档,每篇 5000 字
总字符数 = 500,000 字 ≈ 125,000 tokens
远超大多数 LLM 的上下文限制

检索精度问题:长文档包含多个主题,检索时难以精确定位。

查询: "公司的年假制度"

未分块: 返回整本《员工手册》50页
用户需要自己翻找相关信息

分块后: 只返回"年假"相关的 3 个段落
直接回答用户问题

嵌入质量下降:长文本的嵌入向量会"稀释"关键信息。

短文本嵌入: "年假制度规定员工每年享有5-15天假期"
→ 向量聚焦于"年假"主题

长文本嵌入: "公司简介...产品介绍...年假制度...财务制度..."
→ 向量是多个主题的平均,检索时不够精确

分块的核心参数

Chunk Size(块大小)

块大小决定每个分块包含的文本量。选择时需要平衡:

块大小优点缺点适用场景
小(100-300字)语义单一、检索精确上下文不足、碎片化精确问答、定义查询
中(500-1000字)平衡性好可能截断语义通用场景
大(1500-3000字)上下文完整噪音多、检索粗略需要完整上下文的场景
from langchain.text_splitter import RecursiveCharacterTextSplitter

# 小块
small_splitter = RecursiveCharacterTextSplitter(
chunk_size=200,
chunk_overlap=50
)

# 中等块(推荐)
medium_splitter = RecursiveCharacterTextSplitter(
chunk_size=800,
chunk_overlap=200
)

# 大块
large_splitter = RecursiveCharacterTextSplitter(
chunk_size=2000,
chunk_overlap=400
)

Chunk Overlap(块重叠)

重叠确保相邻块之间有上下文延续,避免信息断裂。

文档: "公司规定,员工入职满一年后可享受年假。年假天数根据工龄计算,
工龄1-5年享5天,5-10年享10天,10年以上享15天。"

无重叠分块:
块1: "公司规定,员工入职满一年后可享受年假。"
块2: "年假天数根据工龄计算,工龄1-5年享5天..."
问题: 检索到块2时,不知道"年假天数"指什么

有重叠分块:
块1: "公司规定,员工入职满一年后可享受年假。"
块2: "可享受年假。年假天数根据工龄计算,工龄1-5年..."
解决: 块2包含上下文,知道讨论的是年假

重叠比例建议:通常设置为块大小的 10%-20%。

# 重叠示例
splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200 # 20% 重叠
)

分块策略分类

1. 固定大小分块

按固定字符数或 token 数切分,最简单但不考虑语义边界。

from langchain.text_splitter import CharacterTextSplitter

splitter = CharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200,
separator="\n" # 只在换行处切分
)

chunks = splitter.split_text(text)

优点:实现简单、速度快 缺点:可能切断句子或段落,破坏语义完整性 适用:格式统一的文档、快速原型

2. 递归字符分块

按优先级尝试不同的分隔符,尽可能在语义边界处切分。

from langchain.text_splitter import RecursiveCharacterTextSplitter

# 默认分隔符优先级(适用于英文)
# ["\n\n", "\n", " ", ""]

# 中文优化分隔符
splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=100,
separators=[
"\n\n", # 段落
"\n", # 行
"。", # 句号
"!", # 感叹号
"?", # 问号
";", # 分号
",", # 逗号
" ", # 空格
"" # 字符
]
)

chunks = splitter.split_text(chinese_text)

工作原理

输入文本 → 尝试按"\n\n"切分 → 块太大?
↓ 是
尝试按"\n"切分 → 块太大?
↓ 是
尝试按"。"切分 → ...

优点:保留语义边界、通用性强 缺点:仍可能在不太理想的位置切分 适用:大多数文本场景

3. 语义分块

基于语义相似度动态确定分块边界,相关性高的句子聚在一起。

from langchain_experimental.text_splitter import SemanticChunker
from langchain_openai.embeddings import OpenAIEmbeddings

splitter = SemanticChunker(
OpenAIEmbeddings(),
breakpoint_threshold_type="percentile" # 断点阈值类型
)

chunks = splitter.split_text(text)

原理

句子序列: S1 → S2 → S3 → S4 → S5 → S6

计算相邻句子的语义距离:
S1-S2: 0.1 (相似)
S2-S3: 0.15 (相似)
S3-S4: 0.8 (不相似) ← 断点
S4-S5: 0.12 (相似)
S5-S6: 0.1 (相似)

结果分块:
块1: S1, S2, S3
块2: S4, S5, S6

优点:语义完整性强 缺点:计算开销大、需要嵌入模型 适用:对语义完整性要求高的场景

4. 文档结构分块

根据文档的结构元素(标题、章节、列表)进行分块。

from langchain.text_splitter import MarkdownHeaderTextSplitter

markdown_document = """
# 公司制度

## 考勤管理

员工应按时上下班,迟到超过30分钟视为缺勤。

## 年假制度

入职满一年后可享受年假,天数根据工龄计算。

### 工龄与年假对应表

- 1-5年:5天
- 5-10年:10天
"""

headers_to_split_on = [
("#", "header_1"),
("##", "header_2"),
("###", "header_3"),
]

splitter = MarkdownHeaderTextSplitter(
headers_to_split_on=headers_to_split_on
)

chunks = splitter.split_text(markdown_document)

# 结果示例:
# {
# "header_1": "公司制度",
# "header_2": "考勤管理",
# "content": "员工应按时上下班..."
# }

优点:保留文档结构、元数据丰富 缺点:依赖文档格式、实现复杂 适用:结构化文档(Markdown、HTML、PDF)

5. 父文档分块

索引小块以提高检索精度,检索时返回包含该小块的大块(父文档)。

from langchain.retrievers import ParentDocumentRetriever
from langchain.storage import InMemoryStore
from langchain_community.vectorstores import Chroma
from langchain.text_splitter import RecursiveCharacterTextSplitter

# 小块分割器(用于索引)
child_splitter = RecursiveCharacterTextSplitter(
chunk_size=400,
chunk_overlap=50
)

# 大块分割器(用于返回)
parent_splitter = RecursiveCharacterTextSplitter(
chunk_size=2000,
chunk_overlap=400
)

vectorstore = Chroma(embedding_function=embeddings)
docstore = InMemoryStore()

retriever = ParentDocumentRetriever(
vectorstore=vectorstore,
docstore=docstore,
child_splitter=child_splitter,
parent_splitter=parent_splitter,
)

# 索引文档
retriever.add_documents(docs)

# 检索时:先匹配小块,返回对应的大块
results = retriever.invoke("年假制度")

优点:检索精确 + 上下文完整 缺点:需要额外存储空间 适用:需要完整上下文的问答场景

6. 滑动窗口分块

以固定步长移动窗口,生成大量重叠的分块。

def sliding_window_chunks(text, window_size=500, stride=100):
"""
window_size: 窗口大小
stride: 步长(越小重叠越多)
"""
chunks = []
start = 0

while start < len(text):
end = start + window_size
chunk = text[start:end]
chunks.append(chunk)
start += stride

if end >= len(text):
break

return chunks

# 示例
chunks = sliding_window_chunks(text, window_size=500, stride=100)
# 产生大量重叠的分块,提高召回率

优点:高召回率、不遗漏信息 缺点:索引量大、存储开销高 适用:高精度要求、存储不敏感场景

分块策略选择指南

按文档类型选择

文档类型推荐策略说明
技术文档递归分块 + 父文档需要完整代码上下文
FAQ/问答对按问答对分块天然的语义单元
法律文档结构分块按条款切分
新闻文章语义分块保持故事完整性
代码文件按函数/类分块保持代码块完整
聊天记录按时间窗口分块保持对话上下文

按查询类型选择

查询类型推荐块大小说明
定义查询小(200-400字)"什么是XX"
事实查询中(400-800字)"XX的具体规定"
推理查询大(1000-2000字)"为什么XX"
摘要查询大或全文"总结XX的主要内容"

代码示例:多策略结合

from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.document_loaders import PyPDFLoader

def smart_chunk_documents(file_path, doc_type="general"):
"""智能分块:根据文档类型选择策略"""

# 加载文档
loader = PyPDFLoader(file_path)
docs = loader.load()

if doc_type == "technical":
# 技术文档:中等块 + 大重叠
splitter = RecursiveCharacterTextSplitter(
chunk_size=800,
chunk_overlap=200,
separators=["\n\n", "\n", "。", " "]
)
elif doc_type == "faq":
# FAQ:小块 + 元数据
splitter = RecursiveCharacterTextSplitter(
chunk_size=300,
chunk_overlap=0
)
elif doc_type == "legal":
# 法律文档:按条款
splitter = RecursiveCharacterTextSplitter(
chunk_size=1500,
chunk_overlap=300,
separators=["第.*条", "\n\n", "\n", "。"]
)
else:
# 通用:默认设置
splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=100
)

return splitter.split_documents(docs)

分块元数据管理

每个分块应保留丰富的元数据,支持后续过滤和追溯。

from langchain_core.documents import Document

# 分块时添加元数据
def chunk_with_metadata(docs, splitter, source_info):
chunks = []

for doc in docs:
doc_chunks = splitter.split_documents([doc])

for i, chunk in enumerate(doc_chunks):
# 添加元数据
chunk.metadata.update({
"source": source_info["filename"],
"page": doc.metadata.get("page", 0),
"chunk_index": i,
"total_chunks": len(doc_chunks),
"doc_type": source_info["type"],
"created_at": datetime.now().isoformat()
})
chunks.append(chunk)

return chunks

分块质量评估

内部指标

语义完整性:分块是否包含完整的思想单元。

def evaluate_chunk_completeness(chunks):
"""评估分块的语义完整性"""
scores = []

for chunk in chunks:
text = chunk.page_content

# 检查是否以完整句子结束
ends_with_punctuation = text[-1] in "。!?.!?"

# 检查开头是否是句子中间
starts_with_lower = text[0].islower()

# 计算分数
score = 1.0
if not ends_with_punctuation:
score -= 0.3
if starts_with_lower:
score -= 0.2

scores.append(score)

return sum(scores) / len(scores)

外部评估

通过检索效果评估分块质量:

# 使用测试集评估
test_cases = [
{"question": "年假如何计算?", "relevant_chunks": ["chunk_id_1", "chunk_id_3"]},
{"question": "报销流程是什么?", "relevant_chunks": ["chunk_id_5"]},
]

def evaluate_retrieval(test_cases, retriever):
hits = 0
for case in test_cases:
results = retriever.invoke(case["question"])
retrieved_ids = [r.metadata["chunk_id"] for r in results]

if any(rid in case["relevant_chunks"] for rid in retrieved_ids):
hits += 1

return hits / len(test_cases)

常见问题与解决方案

问题 1:分块切断重要信息

原文: "公司年假制度规定:员工入职满一年后可享受年假。
年假天数根据工龄计算:工龄1-5年享5天,5-10年享10天。"

问题分块:
块1: "公司年假制度规定:员工入职满一年后可享受"
块2: "年假。年假天数根据工龄计算:工龄1-5年..."

问题: 块1不完整,块2开头突兀

解决方案:增加重叠或使用语义分块。

问题 2:分块大小差异过大

文档结构:
- 简介:50字
- 第一章:5000字
- 第二章:300字

问题:
- 简介分块太小,嵌入向量不稳定
- 第一章分块太大,噪音多

解决方案:设置最小和最大分块大小。

splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=100,
length_function=len,
# 合并小于最小大小的块
merge_chunks=True,
min_chunk_size=100
)

问题 3:代码块被切分

原文:
```python
def calculate_vacation(years):
if years < 5:
return 5
elif years < 10:
return 10
return 15

问题分块(在代码中间切断): 块1: "def calculate_vacation(years):\n if years" 块2: "< 5:\n return 5..."


**解决方案**:识别代码块并保持完整。

```python
import re

def split_preserving_code(text, chunk_size):
"""保持代码块完整的分块"""
# 提取代码块
code_pattern = r'```[\s\S]*?```'
code_blocks = re.findall(code_pattern, text)

# 替换代码块为占位符
text_with_placeholders = re.sub(code_pattern, '<<CODE_BLOCK>>', text)

# 分块
chunks = recursive_split(text_with_placeholders, chunk_size)

# 恢复代码块
for i, chunk in enumerate(chunks):
while '<<CODE_BLOCK>>' in chunk and code_blocks:
chunk = chunk.replace('<<CODE_BLOCK>>', code_blocks.pop(0), 1)
chunks[i] = chunk

return chunks

最佳实践总结

  1. 从递归分块开始:这是最通用、性价比最高的方法
  2. 块大小 500-1000 字:适合大多数场景的起点
  3. 重叠 10-20%:确保上下文连续性
  4. 保留元数据:来源、页码、标题等信息
  5. 评估和迭代:用实际数据验证分块效果
  6. 特殊文档特殊处理:代码、表格、列表需要专门策略

小结

分块是 RAG 系统的基础工程,直接影响检索质量。关键要点:

  • 分块大小和重叠是核心参数
  • 递归分块是通用选择
  • 结构化文档应利用其结构
  • 父文档分块平衡精度和上下文
  • 必须保留丰富的元数据

下一步

参考资料