RAG 的分块策略详解:让检索结果更接近答案
RAG 的分块不是把文档切短这么简单。好的 chunking 会同时考虑语义边界、文档结构、检索粒度、上下文组装和评估指标,让召回结果既精准又可解释。
RAG 系统里最容易被低估的一步,是分块。
很多团队第一次做知识库问答时,会把 PDF、网页、Markdown 或 Word 文档直接按固定长度切成一段段文本,向量化后塞进数据库。Demo 阶段看起来能跑,真正上线后问题就来了:用户问的是“退款条件”,检索回来的 chunk 只有“满足以下任一条件”;用户问的是“接口鉴权方式”,召回结果命中了代码片段,却丢了前面的认证说明;用户要求引用来源,系统只能给出一个模糊的文件名,无法定位到章节。
这些问题不一定是模型不够强,也不一定是 embedding 模型选错了。很多时候,知识在进入向量数据库之前就已经被切碎了。
分块的目标不是让文本变短,而是让知识以适合检索的形态进入系统:块内信息足够完整,块间边界尽量自然,命中后能回到原文位置,并且能根据问题组装出可回答的上下文。
为什么 RAG 必须分块
一个 RAG 流程通常长这样:
flowchart LR
A["原始文档"] --> B["解析与清洗"]
B --> C["文档分块"]
C --> D["Embedding"]
D --> E["向量数据库"]
F["用户问题"] --> G["检索"]
E --> G
G --> H["重排与上下文组装"]
H --> I["LLM 生成答案"]
分块位于解析和向量化之间。它决定了后面检索系统能拿到什么样的“最小证据单元”。
如果不分块,直接把整份文档向量化,会遇到三个问题。
第一,长文档会超过 embedding 模型或生成模型的上下文限制。即使模型支持很长上下文,把几十页文档全部塞进去也会带来成本、延迟和注意力稀释问题。
第二,整篇文档的向量表示太粗。用户问“如何重置 API Key”,整篇开发手册的 embedding 未必能精准表达这个局部问题。
第三,引用和追溯困难。答案说“文档中提到可以 24 小时内退款”,系统需要指出具体章节、段落甚至页码,而不是只说“来自用户协议.pdf”。
但块也不能太小。比如原文是:
企业用户可以申请退款。条件包括:服务未开通、开通后 7 天内未产生调用量、账户不存在欠费。
如果切成:
企业用户可以申请退款。
条件包括:服务未开通、开通后 7 天内未产生调用量、账户不存在欠费。
用户问“企业用户退款条件是什么”时,第二块有条件但缺主语,第一块有主语但缺条件。两个块单独看都不完整。好的分块要避免把“对象、条件、结论”拆散。
好的 chunk 长什么样
一个高质量 chunk 通常满足四个条件:
- 语义完整:读者单独看到这个块,也能理解它在说什么。
- 信息密度高:块里围绕同一个问题展开,少混入无关段落。
- 可追溯:保留文件名、标题路径、页码、段落编号、块类型等 metadata。
- 可扩展上下文:命中小块后,可以回到父章节、相邻段落或原始页面补足上下文。
例如一个技术文档 chunk 不应该只有正文:
{
"text": "调用接口时需要在 Header 中传入 Authorization: Bearer <token>。",
"metadata": {
"source": "api-guide.md",
"title_path": "认证与鉴权 > Bearer Token",
"chunk_type": "paragraph",
"page": null,
"section_id": "auth-bearer-token"
}
}
text 用于检索和生成,metadata 用于过滤、重排、引用和回链。很多 RAG 系统效果差,不是没有向量检索,而是 metadata 太少,检索回来后无法判断它属于哪个章节、哪个版本、哪个业务线。
固定长度分块:最简单,也最容易误伤语义
固定长度分块按字符数或 token 数直接切文本。例如每 500 个中文字符切一块,重叠 50 个字符。
def fixed_chunks(text, size=500, overlap=50):
chunks = []
start = 0
while start < len(text):
end = start + size
chunks.append(text[start:end])
start = end - overlap
return chunks
它的优点是实现简单、速度快、对任何文本都能处理。做 RAG 原型时,用固定长度分块可以很快建立 baseline。
问题也很明显:它完全不理解标题、段落、列表、表格、代码块。切分点可能落在一句话中间,也可能把表格切成上下两半。
比如:
如果账户存在欠费,需要先完成补缴,才能申请退款。
固定长度可能切成:
如果账户存在欠费,需要先完成补
缴,才能申请退款。
这两个块都很糟糕。第一个块没有结论,第二个块缺主语,embedding 表达也会变弱。
固定长度分块适合三类场景:
- 文档结构很弱,只有连续纯文本。
- 项目早期需要快速跑通。
- 作为兜底策略,处理其他分块方法无法覆盖的残余文本。
中文知识库可以从 chunk_size=300~800 字、chunk_overlap=10%~20% 开始试。这个范围不是规则,只是起点。真正的参数要靠评估集调整。
句子分块:保护最小语义单元
句子分块先按句号、问号、感叹号、分号等边界切句,再把若干句聚合成一个 chunk。
示例:
API Key 创建后只展示一次。请立即复制并妥善保存。如果遗失,需要删除后重新创建。
句子分块会先得到:
API Key 创建后只展示一次。
请立即复制并妥善保存。
如果遗失,需要删除后重新创建。
再按目标长度合并成:
API Key 创建后只展示一次。请立即复制并妥善保存。如果遗失,需要删除后重新创建。
它比固定长度更稳,因为不会把完整句子切断。法律法规、FAQ、新闻、客服知识库这类文本,很多答案本身就在一句或几句话里,句子分块很实用。
中文分句需要谨慎。英文工具不一定适合中文标点,尤其是带编号、括号、金额、缩写、URL 的文本。一个实用的中文分句正则可以作为起点:
import re
def split_zh_sentences(text):
pattern = r"(?<=[。!?;])"
return [s.strip() for s in re.split(pattern, text) if s.strip()]
生产系统里还要处理更多边界,比如:
1.2.3版本号不能被当成句号切开。https://example.com/a.b里的点不能切。- 表格单元格里的换行不一定代表句子结束。
句子分块的问题是块可能太短。短块召回很准,但上下文不足。常见做法是把多个句子合并到目标 token 数,并在检索命中后追加相邻句子。
递归字符分块:多数项目的稳妥起点
递归字符分块的思路是:先尝试用大的自然边界切,切不动再用更细的边界。
对中文文档,可以按这样的优先级:
Markdown 标题
空行
换行
句号 / 问号 / 感叹号 / 分号
逗号
字符
LangChain 的 RecursiveCharacterTextSplitter 就是这一类工具的典型实现。它会尽量保留较大的语义边界,只有超过目标长度时才继续向下拆。
一个典型配置:
from langchain_text_splitters import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=600,
chunk_overlap=80,
separators=["\n## ", "\n### ", "\n\n", "\n", "。", ";", ",", ""],
)
chunks = splitter.split_text(markdown_text)
递归分块适合大多数混合文本:产品手册、帮助中心、技术博客、内部 Wiki、规章制度。它不如结构解析精确,但成本低、可控性强,常作为 RAG 项目的第一版生产方案。
它的短板在表格、代码和复杂版式。代码块中间被切开后,函数定义和调用可能分离;表格被切开后,后半部分丢失表头。遇到这类内容,要让结构感知策略先处理,再把普通段落交给递归分块。
结构感知分块:尊重文档本来的组织方式
强结构文档应该优先按结构切,而不是按长度切。
Markdown、HTML、Word 标题、PDF 目录、API 文档、表格、代码块都有自己的边界。结构感知分块会把这些边界当成切分依据。
以 Markdown 为例:
# 支付接口
## 鉴权
调用接口需要携带 Bearer Token。
## 退款
退款仅支持原路退回,通常在 3 个工作日内处理。
理想 chunk 不是每 500 字切一次,而是:
{
"text": "调用接口需要携带 Bearer Token。",
"metadata": {
"title_path": "支付接口 > 鉴权"
}
}
{
"text": "退款仅支持原路退回,通常在 3 个工作日内处理。",
"metadata": {
"title_path": "支付接口 > 退款"
}
}
标题路径非常重要。用户问“支付接口怎么鉴权”时,正文里可能只有“携带 Bearer Token”,没有“支付接口”几个字。把标题路径注入 metadata,检索和重排时就能利用这个上下文。
HTML 文档也类似。正文可能散落在 <h2>、<p>、<li>、<table> 里。结构感知分块会先解析 DOM,再按标题层级和块级元素组合内容。
表格要特别处理。下面这个表不能按行随便切:
| 参数 | 类型 | 是否必填 | 说明 |
|---|---|---|---|
| user_id | string | 是 | 用户 ID |
| page_size | int | 否 | 每页数量,默认 20 |
如果只召回第二行:
page_size | int | 否 | 每页数量,默认 20
读者能勉强看懂,但模型可能丢失表头语义。更好的做法是把表头和行一起序列化:
表格:请求参数
参数=user_id;类型=string;是否必填=是;说明=用户 ID
参数=page_size;类型=int;是否必填=否;说明=每页数量,默认 20
如果表格很长,可以按行组切分,但每个 chunk 都重复表头和表格标题。
对话分块:按轮次,而不是按字符
客服记录、会议纪要、访谈稿不适合简单按段落切。它们的基本单位是“谁在什么时候说了什么”。
例如:
用户:我的订单为什么还没发货?
客服:请提供订单号。
用户:订单号是 A123。
客服:查到了,仓库今天 18 点前发出。
如果只召回“仓库今天 18 点前发出”,答案缺少订单背景。对话分块通常按轮次窗口聚合:
用户:我的订单为什么还没发货?
客服:请提供订单号。
用户:订单号是 A123。
客服:查到了,仓库今天 18 点前发出。
重叠也不应该用字符重叠,而应该用轮次重叠。比如每个 chunk 包含 6 轮对话,前后重叠 1~2 轮。
metadata 同样关键:
{
"conversation_id": "ticket-2026-001",
"speaker_path": ["user", "agent", "user", "agent"],
"turn_start": 12,
"turn_end": 15,
"topic": "物流发货"
}
用户问“客服有没有承诺发货时间”时,检索结果不仅要有文本,还要能定位到具体工单和轮次。
语义分块:根据话题变化切边界
语义分块不依赖标题或段落,而是根据句子之间的语义相似度判断边界。
一个长段落可能先讲“模型训练”,再讲“部署成本”,最后讲“安全评估”。如果原文没有清晰标题,结构分块发现不了话题变化。语义分块会把每个句子转成 embedding,检测相邻句子或滑动窗口之间的相似度变化。
简化流程:
分句
↓
计算每句 embedding
↓
计算相邻窗口相似度
↓
相似度突然下降的位置作为候选切分点
↓
结合最小 / 最大 chunk 长度修正
示例:
模型训练阶段需要准备高质量语料。训练数据要经过去重和安全过滤。上线后更大的成本来自推理。推理服务需要关注并发、缓存和限流。
语义分块可能切成:
模型训练阶段需要准备高质量语料。训练数据要经过去重和安全过滤。
上线后更大的成本来自推理。推理服务需要关注并发、缓存和限流。
语义分块适合论文、白皮书、长报告、论证型文章。它能发现没有显式标题的话题转移。
代价也更高:需要对句子或窗口计算 embedding,还要调阈值。短文档、格式清晰的文档没必要一上来就用语义分块。一个更实用的选择是:先按结构粗切,只有当某个章节太长时,再对这个章节内部做语义分块。
主题分块:适合长篇多主题材料
主题分块比语义分块更偏宏观。它通常会对句子或段落 embedding 聚类,识别主题标签,再根据主题切换切分。
例如一份年度报告可能包含:
- 市场表现
- 产品路线
- 财务数据
- 风险因素
- 组织调整
如果原文结构混乱,主题分块可以帮助系统把内容归类。但它有几个前提:
- 文档足够长。
- 主题数量相对稳定。
- 可以接受一定预处理成本。
小文档不适合主题分块。十几个段落强行聚类,结果往往不稳定。主题分块更适合书籍、行业报告、研究综述这类长文档。
小-大分块:小块召回,大块回答
很多 RAG 系统会在“检索精度”和“上下文完整性”之间摇摆。
小块适合检索。用户问“API Key 会展示几次”,句子级 chunk 很容易命中:
API Key 创建后只展示一次。
但生成答案时只给这一句又太少。用户可能还需要知道“遗失后怎么办”。这时应该把它所在段落一起带上:
API Key 创建后只展示一次。请立即复制并妥善保存。如果遗失,需要删除后重新创建。
这就是小-大分块。
它通常有两层:
- 小块:句子、短段落,用于向量检索,追求命中精准。
- 大块:完整段落、章节片段,用于回答,提供上下文。
数据结构可以这样设计:
{
"child_chunk": "API Key 创建后只展示一次。",
"parent_id": "api-guide#auth-key",
"parent_text": "API Key 创建后只展示一次。请立即复制并妥善保存。如果遗失,需要删除后重新创建。"
}
检索时只索引 child_chunk,命中后根据 parent_id 拉取 parent_text。这样既保留了小块检索的精确度,又避免答案上下文过窄。
父子分块:把“检索单元”和“上下文单元”分开
父子分块和小-大分块经常一起出现,但它们不是同一个概念。
父子分块是一种数据建模方式:每个子块都知道自己属于哪个父块。小-大分块是一种检索流程:用小块召回,再取大块回答。
一个父子结构可以长这样:
父块:支付接口 > 退款规则
子块 1:退款仅支持原路退回。
子块 2:已开票订单需要先完成红冲。
子块 3:退款通常在 3 个工作日内处理。
用户问“已开票订单怎么退款”,命中子块 2。系统随后取整个父块作为候选上下文,让模型同时看到退款方式、开票限制和处理时间。
父子分块还能帮助去重。多个子块命中同一个父块时,不需要把父块重复塞进 prompt,可以合并后统一重排。
Agent-Based Chunking:让 LLM 辅助判断复杂边界
有些文档很难靠规则处理:PDF 里混着正文、表格、脚注、公式、代码、图片说明;会议纪要里有多人插话;技术文档里一段代码跨越多页。
这时可以让小模型或 LLM Agent 辅助判断分块边界。
一个可控的 Agent-Based Chunking 任务可以这样定义:
输入:一段从 PDF 页面解析出的文本和布局信息
输出:JSON 数组,每个元素包含 start_offset、end_offset、chunk_type、title_path、reason
规则:
- 不允许在代码块、表格、公式中间切分
- 每个 chunk 尽量只包含一个主题
- 如果 chunk 超过 900 字,优先在段落边界切
- 必须保留标题链路
输出示例:
[
{
"start_offset": 0,
"end_offset": 486,
"chunk_type": "paragraph",
"title_path": "安装指南 > Docker 部署",
"reason": "同一小节内连续说明 Docker 启动步骤"
}
]
这种方式很灵活,但成本和不确定性也更高。生产环境里要加护栏:
- 输出必须是可解析 JSON。
- offset 必须能映射回原文。
- chunk 长度要做硬约束。
- 失败时回退到结构分块或递归分块。
Agent-Based Chunking 更适合高价值、复杂格式、低吞吐的文档处理,不适合所有文档都走一遍。
混合分块:生产环境更常见的选择
单一分块策略很难覆盖所有文档。生产系统更常见的是混合流程。
flowchart TD
A["原始文档"] --> B["解析格式"]
B --> C{"文档类型"}
C -->|Markdown / HTML| D["结构分块"]
C -->|PDF / Word| E["版式与段落解析"]
C -->|对话记录| F["轮次分块"]
C -->|代码| G["语法边界分块"]
D --> H{"块是否过长"}
E --> H
F --> H
G --> H
H -->|否| I["写入 metadata"]
H -->|是| J["递归 / 语义细分"]
J --> I
I --> K["小块索引"]
I --> L["父块存储"]
K --> M["检索"]
L --> N["上下文组装"]
M --> N
一个稳妥的生产流程可以分四步:
- 先按结构粗切:标题、段落、列表、表格、代码块、对话轮次。
- 再处理异常长块:普通文本用递归分块,论证型长段落用语义分块。
- 建立父子关系:小块用于检索,父块用于上下文组装。
- 保留 metadata:标题路径、页码、块类型、字符偏移、文档版本。
三种档位可以这样选:
| 档位 | 策略 | 适用场景 |
|---|---|---|
| Fast | 结构分块 + 递归分块 | 快速上线、文档格式相对简单 |
| Balanced | 结构分块 + 递归分块 + 父子分块 + rerank | 大多数生产知识库 |
| Quality | Balanced + 语义分块 + Agent 边界修正 + 更强重排 | 法律、金融、医疗、复杂企业文档 |
Fast 档位能快速建立可用系统。Balanced 档位通常是最划算的生产选择。Quality 档位适合高价值问答,但要接受更高的解析、embedding、评估和维护成本。
chunk_overlap 不是越大越好
重叠窗口用来缓解边界断裂。比如一个定义跨越两个段落,适当 overlap 可以让两个 chunk 都保留关键上下文。
但 overlap 太大会带来三个问题:
- 向量库条目变多,存储和检索成本上升。
- 重复内容增多,top-k 里可能挤满相似 chunk。
- 生成阶段上下文冗余,模型更难聚焦。
一个常见起点是 10%~20%。但结构化文档里,很多 overlap 可以由 metadata 和父块回填替代。
比如每个 chunk 都带:
{
"title_path": "管理员手册 > 权限管理 > 角色继承"
}
这比简单重复前后 100 个字符更干净。命中后再取相邻块或父块,通常比盲目加大 overlap 更有效。
分块参数怎么调
分块调优不要凭感觉。最好准备一小批真实问题和标准答案,固定其他环节,只改分块策略。
评估时可以看这些指标:
- Recall@k:正确证据是否出现在前 k 个召回结果里。
- MRR:正确证据排得是否靠前。
- nDCG:排序结果整体质量如何。
- faithfulness:生成答案是否忠实于检索上下文。
- 引用准确率:答案引用是否能定位到正确章节或段落。
- 上下文利用率:送进 LLM 的 chunk 有多少真正被答案使用。
一个最小评估表可以这样记录:
| 问题 | 标准证据位置 | 策略 A 是否召回 | 策略 B 是否召回 | 备注 |
|---|---|---|---|---|
| API Key 会展示几次 | 认证 > API Key | 是,rank=2 | 是,rank=1 | B 的标题路径更准 |
| 已开票订单如何退款 | 支付 > 退款规则 | 否 | 是,rank=3 | A 把表格切断 |
调参时一次只改一个变量:
- 固定 embedding 模型和 reranker,只改 chunk_size。
- 固定 chunk_size,只改 overlap。
- 固定检索 top-k,只改是否启用父块回填。
如果多个变量一起改,结果变好也很难知道是谁起了作用。
不同文档类型的推荐策略
| 文档类型 | 推荐分块方式 | 注意点 |
|---|---|---|
| Markdown / HTML 技术文档 | 标题结构分块 + 递归细分 | 保留标题路径和锚点 |
| PDF 白皮书 | 版式解析 + 段落分块 + 语义细分 | 注意页眉页脚、脚注、图表说明 |
| FAQ | 问答对作为原子块 | 不要把问题和答案切开 |
| 表格 / CSV / Excel | 表头重复 + 行组分块 | 每块保留表名、表头、主键字段 |
| 代码仓库 | 按函数、类、文件结构分块 | 保留文件路径、语言、符号名 |
| 客服对话 | 按轮次窗口分块 | 保留 speaker、turn_id、工单号 |
| 法律 / 合同 | 条款结构分块 + 句子聚合 | 保留条款编号,避免拆散条件和例外 |
FAQ 是一个典型例子。原文可能是:
Q:如何修改绑定手机号?
A:进入账号设置,完成短信验证后即可修改。
这个问答对应该作为一个原子块。如果把问题和答案分开,用户问“手机号怎么改”时可能只召回问题,没有答案。
代码仓库也不能按普通文本处理。函数签名、注释、函数体、类名、文件路径都影响检索。一个代码 chunk 至少要带:
{
"file_path": "src/auth/token_service.py",
"symbol": "TokenService.refresh_token",
"language": "python",
"chunk_type": "function"
}
用户问“refresh token 在哪里过期”,这些 metadata 会比纯文本更有用。
常见误区
误区一:chunk_size 越大越好。
大块上下文完整,但检索信号会被稀释。用户问一个细节,整章内容的向量未必能精准命中。
误区二:chunk_size 越小越好。
小块检索精准,但容易丢条件、例外和定义。小块通常要配父块回填或邻接扩展。
误区三:overlap 能解决所有边界问题。
overlap 只能缓解相邻文本断裂,不能修复表格切断、标题丢失、问答拆散这类结构问题。
误区四:只看向量召回,不看最终答案。
RAG 的目标是回答问题。召回指标很重要,但还要看答案是否忠实、引用是否准确、上下文是否被模型真正使用。
误区五:所有文档用同一套分块参数。
合同、代码、客服对话、表格、产品手册的知识结构完全不同。统一参数通常只能得到平均但不出色的结果。
一套可落地的默认方案
如果没有特殊约束,可以从下面这套方案起步:
- 文档解析阶段识别标题、段落、列表、表格、代码块、页码。
- Markdown / HTML 按标题层级粗切,PDF / Word 按段落和版式粗切。
- 普通文本块超过 800 中文字时,用递归分块细分。
- 对论文、报告中的超长论证段落,额外尝试语义分块。
- 表格、代码、FAQ、对话作为特殊类型处理,不走普通字符切分。
- 建立父子映射:子块索引,父块存储。
- 每个 chunk 写入 source、title_path、page、offset、chunk_type、parent_id。
- 用 30~100 个真实问题做离线评估,按 Recall@k、MRR、引用准确率调参。
这套方案不追求花哨,重点是稳。它尊重文档结构,又保留递归分块的通用性;它允许小块精准召回,也允许父块补足上下文。
结语
RAG 的分块策略不是预处理细节,而是检索系统的知识建模方式。
固定长度分块能让系统跑起来,句子分块保护最小语义单元,递归分块适合作为通用起点,结构感知分块让文档边界不被破坏,语义分块能处理隐含话题变化,小-大分块和父子分块则把“检索精度”和“回答上下文”拆开优化。
真正好用的 RAG 系统通常不是靠某一种分块方法赢,而是根据文档类型、问题类型和评估结果组合策略。让检索结果更接近答案,第一步就是让 chunk 本身更像一条可引用、可理解、可回溯的证据。
我是姚磊
大模型应用开发者,关注 RAG、Agent 和产品化工程。沉淀实践中的判断、踩坑记录和可复用方法。联系我:yaolei_agent@163.com



