分块(Chunking)是将大文档切分为小片段的过程,是 RAG 系统的关键步骤。分块质量直接影响:
- 检索精度:能否找到最相关的内容
- 上下文完整性:检索到的内容是否包含完整语义
- Token 效率:是否高效利用上下文空间
原则一:语义完整性
每个块应该是一个语义完整的单元,能够独立表达完整的含义。
原则二:大小适中
块不能太大(占用过多上下文空间),也不能太小(缺失必要上下文)。
原则三:信息自包含
块应该包含理解其内容所需的必要背景信息。
graph TB
subgraph "分块方法层级"
A["固定大小分块"]
B["基于分隔符分块"]
C["递归分块"]
D["语义分块"]
E["基于 LLM 分块"]
end
A --> |"简单"| F["复杂度"]
E --> |"复杂"| F
图 5-2:分块方法层级
按固定的字符数或 Token 数切分。
优点:实现简单、结果可预测
缺点:可能破坏语义完整性
适用:快速原型、对质量要求不高
参数:块大小(通常 500-2000 字符)、重叠大小(10-20%)
按自然分隔符(段落、句子、换行)切分。
优点:保持自然边界
缺点:块大小不一致
适用:结构清晰的文档
常用分隔符:\n\n、\n、.、。
尝试按多级分隔符切分,自动适应文档结构。
优点:平衡完整性和一致性
缺点:实现较复杂
适用:大多数文档类型
典型分隔符序列:[“\n\n”, “\n”, “ ”, “”]
基于语义相似度切分,保持语义单元完整。
优点:语义完整性最好
缺点:计算成本高
适用:对质量要求高的场景
实现方式:计算相邻句子的嵌入相似度,在相似度低谷处切分。
使用语言模型理解内容结构进行切分。
优点:理解能力最强
缺点:成本高、延迟大
适用:高价值内容、复杂文档
为了说明不同分块策略可能带来的差异,下面给出一个 示意性的对比图(不同数据集、检索器、嵌入模型与评测口径会导致结果差异很大):
xychart-beta
title "不同分块策略的检索效果对比 (Recall@10)"
x-axis ["固定大小(200)", "固定大小(500)", "语义分块", "递归分块"]
y-axis "Recall@10 (%)" 40 --> 90
bar [65.2, 72.5, 85.8, 81.3]
图 5-3:不同分块策略检索效果示意
注:数值仅用于说明趋势,不应被视为可复现的通用结论。
- 语义分块 通常能获得最佳的检索效果,因为其保持了语义单元的完整性。
- 递归分块 在效果与成本之间取得了较好的平衡。
- 过小的固定分块 容易破坏语义,导致召回率下降。
块大小选择
| 场景 | 推荐大小 | 理由 |
|---|---|---|
| 精确问答 | 200-500 Token | 聚焦单一信息点 |
| 综合理解 | 500-1000 Token | 更多上下文 |
| 长文档摘要 | 1000-2000 Token | 涵盖更多内容 |
重叠设置
重叠可以防止信息被切断在边界处,典型设置为块大小的 10-20%。
元数据保留
为每个块附加元数据:
- 来源文档
- 位置信息(第几章、第几节)
- 标题层级
- 时间戳
代码文档
按函数、类等逻辑结构切分,而非按行数。
表格数据
保持行完整性,必要时将表头附加到每个块。
对话记录
按对话轮次切分,保持问答对完整。
技术文档
尊重标题层级结构,将标题作为块的上下文前缀。
评估分块效果的方法:
- 边界检查:人工检查切分边界是否合理
- 检索测试:测试典型查询能否检索到相关块
- 下游效果:观察最终任务效果
- 从递归分块开始:适用于大多数场景
- 根据内容调整:不同类型文档使用不同策略
- 保留结构信息:将标题等信息附加到块中
- 测试和迭代:持续评估和优化分块效果
以下是三种典型场景的分块策略对比,展示如何根据内容特点选择合适的方法。
场景:用户问“这款手机支持 5G 吗?”需要从产品说明书中检索答案。
错误做法:
# 按 500 字符固定分块
第一块: "...4800万像素主摄像头,支持光学防抖。5G 网络..." ← 5G信息被切断
第二块: "...支持 SA/NSA 双模。电池容量 5000mAh..."
正确做法:
# 按产品规格表的属性分块
块-网络: "网络制式: 5G SA/NSA 双模,支持 n1/n28/n41/n78 频段"
块-相机: "摄像头: 4800万像素主摄,支持光学防抖"
块-电池: "电池: 5000mAh,支持 67W 快充"
踩坑经验:规格表按固定字符数分块会把相关属性切断,导致模型无法完整回答。应按语义属性分块,每个块对应一个完整的产品特性。
场景:找出合同中的“不可抗力”免责条款。
错误做法:
# 按段落分块,但切断了条款
第一块: "...第八条 不可抗力 如因战争、地震、洪水等不可..."
第二块: "...抗力事件导致合同无法履行,双方均不承担违约责任。"
正确做法:
# 按条款层级分块,保持完整
块-第八条: "第八条 不可抗力
如因战争、地震、洪水等不可抗力事件导致合同无法履行,双方均不承担违约责任。
不可抗力事件发生后,受影响方应在 7 日内书面通知对方。"
踩坑经验:法律文本的完整性至关重要。曾因分块切断句子导致法律含义改变(“不承担”变成“承担”)。必须按条款结构分块,宁可块大一点也不能破坏语义完整性。
场景:开发者问“这个函数是怎么实现的?”
错误做法:
# 按 50 行固定分块
第一块: "def calculate_price(items):
total = 0
for item in items:
if item.discount:" ← 函数被切断
第二块: " price = item.price * 0.8
else:
price = item.price
return total" ← 缺少函数开头
正确做法:
# 按函数/类为单位分块
块-calculate_price: "# 位置: pricing.py:45-62
def calculate_price(items):
'''计算购物车总价,支持折扣'''
total = 0
for item in items:
if item.discount:
price = item.price * 0.8
else:
price = item.price
total += price
return total
调用方: `checkout.py:process_order()`
踩坑经验:代码按行数分块会切断函数逻辑,模型无法理解完整实现。应按 AST(抽象语法树)提取函数/类为单位,并附带调用关系元数据。