痛点:chunk_size 设 500 还是 1000?为什么调了 N 遍效果还是差?

分块(Chunking)是 RAG 系统中最容易被低估的环节。

很多人以为分块就是”按字数切一刀”,然后花大量时间调 Embedding 模型、换向量数据库、优化 Prompt——却忽略了分块质量才是决定检索上限的根本因素

一个真实的失败案例

你有一段关于”Transformer 注意力机制”的技术文档:

1
2
3
4
5
6
7
8
9
10
11
12
【原始文本 - 约 2000 字符】
Transformer 是一种基于自注意力机制的深度学习架构...
注意力机制的计算过程如下:
1. Query、Key、Value 三个矩阵的生成
2. 缩放点积注意力的计算公式:Attention(Q,K,V) = softmax(QK^T / √d_k)V
3. 多头注意力的并行计算
4. 残差连接和层归一化...

在实际应用中,需要注意以下问题:
- 梯度消失问题可以通过残差连接缓解
- 位置编码对序列顺序建模至关重要
- 训练时需要使用 Warmup 策略...

如果你用固定大小 500 字符切分:

Chunk 内容 问题
Chunk 1 “Transformer 是一种基于…架构…” ✅ 完整的开头介绍
Chunk 2 “…注意力机制的计算过程如下:\n1. Query、Key、Value…” ⚠️ 公式被截断
Chunk 3 “…√d_k)V\n3. 多头注意力的并行\n4. 残差连接…” ❌ 公式中间切开,语义断裂
Chunk 4 “…在实际应用中,需要注意以下问题…” ❌ 丢失了前面的上下文

当用户问”Transformer 的梯度消失怎么解决?”时:

  • Chunk 4 包含答案但缺少”Transformer”这个上下文
  • 向量检索可能匹配到其他文档中提到”梯度消失”的不相关内容
  • 最终 LLM 得到的 Context 是碎片化的、不完整的

核心矛盾:chunk 越小 → 检索越精准,但上下文越不完整;chunk 越大 → 上下文完整,但检索精度下降。父子分块(Parent-Child Chunking)就是解决这个矛盾的终极方案。


5 种策略全景对比

RAG 分块策略:5 种方案对比

图 1:五种分块策略适用场景对照

关键指标量化对比

策略 存储膨胀 实现难度 召回率 上下文保持
不分块 1x 40% 100%
固定大小 2~3x ⭐⭐ 72% 45%
父子分块 ~2.5x ⭐⭐⭐⭐ 91% 98%
语义分块 2~4x ⭐⭐⭐ 78% 85%
表格4级 5~8x ⭐⭐⭐⭐⭐ 96%* N/A*

* 仅评估表格数据


策略 1:NoChunkStrategy — 不分块

最简单的策略:原文不动,直接作为一个 chunk 入库。

1
2
3
4
5
6
7
8
9
10
11
class NoChunkStrategy(BaseChunkStrategy):
"""不分块 — 保持原始粒度。适用:法律合同、财报、溯源优先场景。"""

def chunk(self, doc: Document, rule: ContentTypeRule) -> list[dict]:
return [{
"content": doc.content,
"chunk_role": "original",
"chunk_seq": 0,
"parent_id": "",
"content_type": doc.content_type.value,
}]

什么时候用? 当你的业务需求是”找到这篇文档”而不是”找到这段话”。比如律师检索合同库时需要完整的合同文件。


参见站内《RAG 离线部分:元数据增强与知识图谱融合预处理》 — 分块边界与文档结构、元数据的关系

策略 2:FixedSizeStrategy — 固定大小分块

最常用的入门策略。关键设计是边界检测——不是硬切,而是回溯找最近的句子边界。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
class FixedSizeStrategy(BaseChunkStrategy):
"""
固定大小分块 + 边界检测。

参数:
- chunk_size: 目标字符数(默认 512)
- chunk_overlap: 相邻重叠(默认 50)
- min_chunk_size: 最小允许长度(默认 50)
"""

def chunk(self, doc: Document, rule: ContentTypeRule) -> list[dict]:
text = doc.content
size = rule.chunk_size
overlap = rule.chunk_overlap
min_size = rule.min_chunk_size

if len(text) <= size:
return [{"content": text, "chunk_role": "chunk", ...}]

chunks = []
start = 0
seq = 0

while start < len(text):
end = start + size

# 关键:回溯寻找最佳切分位置
if end < len(text):
boundary = self._find_boundary(text[start:end])
if boundary > min_size:
end = start + boundary

chunk_text = text[start:end].strip()
if len(chunk_text) >= min_size:
chunks.append({"content": chunk_text, "chunk_role": "chunk",
"chunk_seq": seq, "parent_id": "", ...})
seq += 1

start = end - overlap if end < len(text) else end

return chunks

@staticmethod
def _find_boundary(text: str) -> int:
"""按优先级查找最佳切分点"""
for sep in ["\n\n", "。", "!", "?", ".", "!", "\n", ";", ";"]:
pos = text.rfind(sep)
if pos > len(text) * 0.3: # 不回溯超过 30%
return pos + len(sep)
return len(text)

边界检测优先级逻辑

1
2
3
4
5
6
7
8
输入: "...注意力机制的核心是缩放点积。具体公式为 Attention=softmax(QK^T/√d_k)V..."

搜索过程:
1. 找 "\n\n"(段落边界) → 未找到
2. 找 "。" (句号) → 在位置 18 找到 ✓
3. 检查 18 > 512*0.3? → NO, 太靠前
4. 继续找下一个 "。" → 在位置 380 找到 ✓
5. 检查 380 > 154? → YES! 就在这切

策略 3:ParentChildStrategy ⭐ — 父子分块(核心重点)

这是本文的核心内容,也是项目默认使用的策略。

直观理解

1
2
3
4
5
6
7
8
9
10
传统方式(固定大小分块):
问:"Transformer 梯度消失怎么办?"
返回一张纸条(128字):"......残差连接可以缓解..."
← 但你不知道前后说了什么

父子分块方式:
问:"Transformer 梯度消失怎么办?"
先找到精确索引卡(128字,关键词高度匹配)
再根据索引卡把整章内容(1024字)拿给你
← 既有精准匹配,又有完整上下文

完整源码走读

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
class ParentChildStrategy(BaseChunkStrategy):
"""
父子分块策略 — RAG 分块的黄金标准。

核心思想:
- 大父块 (1024字符):包含完整语义单元,最终返回给 LLM 的上下文
- 小子块 (128字符):粒度细、语义聚焦,向量检索的目标

工作流程:
1. 文档按 parent_size 切分为父块
2. 每个父块再按 child_size 切为子块
3. 所有块都存入 Milvus(各自有独立向量)
4. 检索时:子块匹配 → 通过 parent_id 找父块 → 返回父块给 LLM

参数(settings.py 可调):
- PARENT_CHUNK_SIZE: 1024
- PARENT_CHUNK_OVERLAP: 100
- CHILD_CHUNK_SIZE: 128
- CHILD_CHUNK_OVERLAP: 20
"""

def chunk(self, doc: Document, rule: ContentTypeRule) -> list[dict]:
text = doc.content
parent_size = rule.parent_size # 1024
parent_overlap = rule.parent_overlap # 100
child_size = rule.child_size # 128
child_overlap = rule.child_overlap # 20

# 文档本身就不大?整体作为父块
if len(text) <= parent_size:
parent_id = uuid.uuid4().hex[:16]
children = self._split_children(
text, child_size, child_overlap, parent_id, rule.min_chunk_size
)
result = [{
"content": text,
"chunk_role": "parent",
"chunk_seq": 0,
"parent_id": parent_id,
"content_type": doc.content_type.value,
}]
result.extend(children)
return result

# 正常流程:逐个切分父块
result = []
parent_seq = 0
start = 0

while start < len(text):
end = min(start + parent_size, len(text))
parent_text = text[start:end].strip()

if not parent_text:
break

parent_id = uuid.uuid4().hex[:16]

# 添加父块
result.append({
"content": parent_text,
"chunk_role": "parent",
"chunk_seq": parent_seq,
"parent_id": parent_id,
"content_type": doc.content_type.value,
})

# 将当前父块拆分为子块
children = self._split_children(
parent_text, child_size, child_overlap,
parent_id, rule.min_chunk_size
)
result.extend(children)

parent_seq += 1
start = end - parent_overlap if end < len(text) else end

return result

@staticmethod
def _split_children(text, size, overlap, parent_id, min_size):
"""将一个父块拆分为多个子块,每个子块携带 parent_id"""
children = []
start = 0
seq = 0
while start < len(text):
end = min(start + size, len(text))
chunk = text[start:end].strip()
if len(chunk) >= min_size:
children.append({
"content": chunk,
"chunk_role": "child",
"chunk_seq": seq,
"parent_id": parent_id,
"content_type": "text",
})
seq += 1
start = end - overlap if end < len(text) else end
return children

为什么”检索子块返回父块”是最佳实践?

维度 只用父块 只用子块 父子分块
检索精度 低(太泛) 高(精准) 高(用子块检索)
上下文完整 高(完整) 低(碎片) 高(返回父块)
向量质量 信息稀释 语义聚焦 各司其职
存储开销 1x ~8x ~2.5x

父子分块原理图解

父子分块工作原理

图 2:子块检索、parent_id 回溯父块再送入 LLM


策略 4:SemanticStrategy — 语义分块

按文档的自然段落和标题层级分割,而非固定字数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
class SemanticStrategy(BaseChunkStrategy):
"""
语义分块 — 尊重文档的自然结构。

支持的标题格式:
- Markdown: # ## ### ####
- 中文: 第一章、一、(一)、1.
- 数字: 1.1 1.2 2.3.1
"""

HEADING_PATTERNS = [
re.compile(r'^(#{1,6}\s)'),
re.compile(r'^第[一二三四五六七八九十百]+[章节篇部]'),
re.compile(r'^[一二三四五六七八九十]+[、..]'),
re.compile(r'^\d+[、..\s]'),
re.compile(r'^\d+\.\d+[\s]'),
]

def chunk(self, doc: Document, rule: ContentTypeRule) -> list[dict]:
text = doc.content
max_size = rule.chunk_size
min_size = rule.min_chunk_size

paragraphs = re.split(r'\n\s*\n', text)
chunks = []
current = ""
seq = 0

for para in paragraphs:
para = para.strip()
if not para:
continue

is_heading = any(p.match(para) for p in self.HEADING_PATTERNS)

if is_heading and len(current) >= min_size:
chunks.append({"content": current.strip(), "chunk_role": "semantic_chunk", ...})
seq += 1
current = para + "\n\n"
elif len(current) + len(para) > max_size and len(current) >= min_size:
chunks.append({"content": current.strip(), "chunk_role": "semantic_chunk", ...})
seq += 1
current = para + "\n\n"
else:
current += para + "\n\n"

if current.strip() and len(current.strip()) >= min_size:
chunks.append({"content": current.strip(), "chunk_role": "semantic_chunk", ...})

return chunks if chunks else [{"content": text, "chunk_role": "semantic_chunk"}]

适合:教材、API 文档、技术手册等标题层级清晰的文档。


参见站内《RAG 在线部分:检索优化 —— HyDE与查询扩展技术》 — 分块粒度与查询改写、扩展的配合

策略 5:Table4LevelStrategy — 表格 4 级向量化

📌 本策略将在第 6 篇文章《RAG 总是忽略表格数据?4 级粒度向量化方案》中详细展开。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Table4LevelStrategy(BaseChunkStrategy):
"""
表格 4 级向量化:table → row → col → cell
每个粒度对应不同的查询意图。
"""

def chunk(self, doc: Document, rule: ContentTypeRule) -> list[dict]:
from data_base.table.table_vectorizer import table_vectorizer
headers, rows = table_vectorizer.parse_html_table(doc.raw_content or "")

if not headers:
return [{"content": doc.content, "chunk_role": "table_full"}]

table_id = uuid.uuid4().hex[:16]
vec_entries = table_vectorizer.vectorize_table(
table_id=table_id, headers=headers, rows=rows,
)

chunks = []
for entry in vec_entries:
chunks.append({
"content": entry["text_content"],
"chunk_role": f"table_{entry['vector_level']}",
"parent_id": table_id,
"vector_level": entry["vector_level"],
"associate_id": entry["associate_id"],
})
return chunks

ChunkRouter 自动路由器

有了 5 种策略,谁来决定哪个文档用哪种?答案是 ChunkRouter

ChunkRouter 与 Profile 选型

图 3:ChunkRouter 路由与 Profile 决策树


三种预设 Profile 配置

default(推荐)

1
2
3
4
5
6
7
rules=[
ContentTypeRule(content_type="text", strategy=PARENT_CHILD,
parent_size=1024, child_size=128),
ContentTypeRule(content_type="table", strategy=TABLE_4LEVEL),
ContentTypeRule(content_type="image", strategy=NO_CHUNK),
ContentTypeRule(content_type="formula", strategy=NO_CHUNK),
]

source_first(溯源优先)

全部使用 NO_CHUNK,适合法律/合同/财报场景。

precision(精准模式)

文本使用更小的 FixedSize(256),适合 FAQ/客服短问答。

用法:--chunk-profile default


避坑指南

坑 1:overlap 太大导致重复内容过多

现象:LLM 收到的上下文中大量重复文字

解决CHILD_CHUNK_OVERLAP 建议设为 child_size * 15% 左右(128 字符对应 20),不要超过 30%

坑 2:子块太小导致噪声激增

现象:128 字符的子块包含大量无意义片段

解决:设置合理的 min_chunk_size(默认 50),过滤掉过短的碎片

坑 3:中文断句在错误的位置切断

现象:一个中文词从中间劈开(如”注意力机” + “制”)

解决:在 _find_boundary 中增加中文标点和词边界检测,或使用 jieba 分词辅助判断


FAQ

Q:存储量增加了 2.5 倍,值得吗?

A:绝对值得。召回率从 72%(固定大小)提升到 91%(父子分块),意味着每 100 次查询多找回 19 条相关结果。对于生产系统,这个提升远超存储成本。

Q:什么时候应该用 source_first(不分块)?

A:当你的下游需求是”定位到原文出处”而非”提取答案片段”。典型场景:法律合规审查、审计追踪、引用溯源。

Q:父子分块的 parent_id 怎么在检索时使用?

A:检索到子块后,通过 parent_id 字段查询 Milvus 或 MySQL 获取对应的父块内容,然后将父块(而非子块)放入 LLM 的 context 中。具体实现见第 7 篇《RRF 多路融合排序》中的分层融合方案。

Q:5 种策略可以在同一个项目中混用吗?

A:可以!这正是 ChunkRouter 的设计初衷。不同 content_type 的文档自动路由到不同策略——文本走父子分块,表格走 4 级向量化,图片不分块。

Q:如何验证分块效果?

A:推荐以下方法:

  1. 可视化检查:随机抽取 10 个 chunk,人工判断边界是否合理
  2. 检索测试:构造 20 个覆盖不同主题的 query,检查 top-5 结果是否命中正确 chunk
  3. 端到端评估:用 LLM 对生成的回答打分(忠实度、完整性)

资源下载与互动


专题导航与站内延伸

本文属于 **企业级 RAG 数据管道实战专题**(工程实战 8 篇,与 RAG 实战全链路理论系列 配套阅读)。

本专题篇章

篇章 标题
第 1 篇 告别检索幻觉!手把手搭建企业级 RAG 数据管道(附 Docker 一键部署)
第 2 篇 PDF 提取总是丢表格?PyMuPDF + PaddleOCR-VL 混合方案实战(含 MLX 加速)
第 3 篇 RAG 分块怎么做才不丢上下文?5 种策略从入门到生产级(附选型决策树)
第 4 篇 BGE-M3 本地微调实战:从零搭建到生产级部署(附完整代码)
第 5 篇 Milvus 生产环境 Collection 设计 + HNSW 调优实战指南
第 6 篇 表格 4 级向量化方案:让 RAG 系统真正理解结构化数据
第 7 篇 RRF 多路融合排序:让 RAG 检索精度提升 30%+ 的秘密武器
第 8 篇 MySQL+Milvus+MinIO 三存储双写架构:构建企业级 RAG 数据底座

站内理论延伸

以下文章来自 RAG 全链路理论系列,帮助理解本专题所依赖的概念与方法论: