一篇讲透 Chunk 切分:RAG 知识库为什么不是“随便切一刀”?
Chunk 可以理解为:把一篇大文档切成一块一块可检索、可召回、可喂给大模型的小知识片段。比如一篇 30 页的产品手册,大模型不可能每次都读全文,向量库也不适合直接存整篇文章。原始文档↓章节↓小节↓段落↓Chunk↓Embedding↓向量库用户提问时,系统不是找整篇文档,而是先找最相关的 Chunk。所以 Chunk 切得好不好,直接决定三个东西:第一,能不能召回到正确内容。第二,召回内容上下文
做 RAG 知识库,很多人第一步就做错了。
他们以为:
把 Word、PDF、网页、Markdown 转成文本,然后每 500 字切一段,丢进向量库,就完成了知识入库。
但真正上线后会发现:
用户问得很具体,系统召回不到;
用户问一个流程,答案只拿到中间一步;
用户问制度条款,模型只看到半句话;
用户问长文档总结,检索出来的内容前后断裂;
用户问“这个操作怎么做”,模型把第 3 步当成第 1 步回答。
问题往往不是模型不行,也不是向量库不行,而是 Chunk 切分策略太粗糙。
Chunk 不是简单分段,而是 RAG 系统的“知识颗粒度设计”。
一、什么是 Chunk?为什么它这么重要?
Chunk 可以理解为:
把一篇大文档切成一块一块可检索、可召回、可喂给大模型的小知识片段。
比如一篇 30 页的产品手册,大模型不可能每次都读全文,向量库也不适合直接存整篇文章。所以要先切分成多个 Chunk:
原始文档
↓
章节
↓
小节
↓
段落
↓
Chunk
↓
Embedding
↓
向量库
用户提问时,系统不是找整篇文档,而是先找最相关的 Chunk。
所以 Chunk 切得好不好,直接决定三个东西:
第一,能不能召回到正确内容。
第二,召回内容上下文是否完整。
第三,大模型回答时会不会断章取义。
LangChain 文档也提到过类似矛盾:检索时希望文档块足够小,这样 Embedding 更精准;但又希望文档块足够大,这样上下文不丢失。Parent Document Retriever 就是为了解决“小块精准召回、大块恢复上下文”的问题。
二、为什么不能直接按固定字数切?
很多初学者会这样切:
每 500 字切一块
每块重叠 50 字
这确实简单,但问题也很明显。
比如原文是:
一、开户流程
第一步:用户提交身份证、银行卡、手机号。
第二步:系统进行实名校验。
第三步:风控系统判断用户是否命中黑名单。
第四步:校验通过后生成账户。
第五步:发送开户成功通知。
如果机械切分,可能变成:
Chunk 1:
第一步:用户提交身份证、银行卡、手机号。
第二步:系统进行实名校验。
Chunk 2:
第三步:风控系统判断用户是否命中黑名单。
第四步:校验通过后生成账户。
Chunk 3:
第五步:发送开户成功通知。
用户问:“开户完整流程是什么?”
系统可能只召回 Chunk 2,因为里面有“风控”“账户”等关键词。
大模型拿到的上下文只有第 3 步和第 4 步,于是回答就不完整。
这就是典型的 切分破坏流程结构。
固定长度切分适合兜底,但不适合直接作为企业级知识库的主策略。
三、Chunk 切分的核心原则:不是按长度切,而是按知识单元切
好的 Chunk 应该满足四个要求:
1、语义完整
一个 Chunk 最好表达一个完整意思。
比如:
差:
“用户提交材料后,系统会进行实名校验。如果校验失败……”
好:
“实名认证规则:用户提交身份证、银行卡、手机号后,系统会校验三要素是否一致。校验失败时,返回失败原因,并提示用户重新提交。”
前者被切断了,后者是完整知识点。
2、边界清晰
Chunk 最好有明确来源:
文档名称:开户操作手册
章节:二、开户注册流程
小节:2.1 实名认证
段落:实名认证规则
这样召回后,大模型知道这段内容来自哪里。
3、颗粒度适中
太小:召回精准,但上下文不足。
太大:上下文完整,但 Embedding 语义变稀释。
所以企业级知识库通常不会只保留一种 Chunk,而是会有:
Small Chunk:用于精准召回
Parent Chunk:用于上下文恢复
Section Chunk:用于章节级理解
Document Metadata:用于过滤和溯源
4、结构信息不能丢
尤其是流程类、制度类、合同类、产品手册类文档,不能只存纯文本,还要存元数据:
{
"docId": "doc_001",
"title": "开户操作手册",
"sectionTitle": "开户注册流程",
"chunkType": "process_step",
"stepNo": 3,
"totalSteps": 5,
"parentId": "parent_001"
}
这样系统才知道:
这是第 3 步,不是孤立的一句话。
四、按语义切分:让一个 Chunk 表达一个完整意思
语义切分的核心是:
不要让一句话、一段逻辑、一个规则被切断。
比如下面这段制度:
用户连续输错密码 5 次后,系统会临时锁定账户 30 分钟。
如果用户在锁定期间再次尝试登录,系统不会重新计算锁定时间。
用户可以通过短信验证码、人脸识别或客服人工审核方式解锁账户。
比较好的切法是:
Chunk:
账户锁定规则:用户连续输错密码 5 次后,系统会临时锁定账户 30 分钟。锁定期间再次尝试登录,不会重新计算锁定时间。用户可通过短信验证码、人脸识别或客服人工审核方式解锁账户。
不要切成:
Chunk 1:
用户连续输错密码 5 次后,系统会临时锁定账户 30 分钟。
Chunk 2:
如果用户在锁定期间再次尝试登录,系统不会重新计算锁定时间。
Chunk 3:
用户可以通过短信验证码、人脸识别或客服人工审核方式解锁账户。
因为用户问“账户被锁怎么办”,系统可能只召回 Chunk 3,却不知道为什么被锁、锁多久。
语义切分更适合这些内容:
制度条款
业务规则
FAQ 问答
产品说明
异常处理说明
接口字段说明
五、按标题切分:标题是天然的知识边界
很多文档本身就有标题结构:
一、账户管理
1.1 开户
1.2 销户
1.3 冻结账户
二、交易管理
2.1 转账
2.2 充值
2.3 提现
这类文档不能只按字数切,而应该优先按标题切。
比如:
标题路径:
账户管理 > 开户 > 实名认证
每个 Chunk 都带上标题路径:
{
"docTitle": "账户管理手册",
"heading1": "账户管理",
"heading2": "开户",
"heading3": "实名认证",
"content": "用户开户时需要完成三要素实名认证……"
}
这样用户问:
开户时实名认证失败怎么办?
系统不仅能召回正文,还能带上标题上下文:
账户管理 > 开户 > 实名认证
LangChain 的 MarkdownHeaderTextSplitter 就是典型的标题切分思路,它会根据 Markdown 标题拆分文档,并把标题作为元数据保留下来;拆完后还可以继续用其他 splitter 控制 Chunk 大小。
六、按章节切分:长文档必须保留章节结构
长文档最大的问题是:
局部内容有用,但离开章节背景就容易误解。
比如一份风控规则文档:
三、黑名单规则
3.1 命中身份证黑名单
……
3.2 命中手机号黑名单
……
3.3 命中设备指纹黑名单
……
用户问:
什么情况下会被风控拦截?
如果只召回 3.2,答案就不完整。
所以长文档一般要设计多层结构:
Document
├── Section:三、黑名单规则
│ ├── Chunk:3.1 身份证黑名单
│ ├── Chunk:3.2 手机号黑名单
│ └── Chunk:3.3 设备指纹黑名单
小 Chunk 用于精准召回。
Section 用于恢复章节上下文。
这就是 Section Retrieval 的核心思想:
用户问具体问题时,先召回小 Chunk;
如果发现多个小 Chunk 都属于同一个章节,就把整个章节或章节摘要一起带给大模型。
七、按流程切分:流程类文档必须记录 stepNo 和 totalSteps
流程类文档是最容易切坏的。
比如:
一、退款流程
第一步:用户提交退款申请。
第二步:系统校验订单状态。
第三步:判断是否超过退款期限。
第四步:调用支付渠道退款。
第五步:更新订单状态。
第六步:发送退款结果通知。
如果普通切分,系统只知道每个 Chunk 是一句话。
但它不知道这是流程,也不知道这是第几步。
企业级做法应该是:
{
"chunkType": "process_step",
"processName": "退款流程",
"stepNo": 4,
"totalSteps": 6,
"stepTitle": "调用支付渠道退款",
"content": "第四步:调用支付渠道退款。系统根据原支付渠道发起退款请求,并记录渠道返回码。",
"prevStep": 3,
"nextStep": 5,
"parentProcessId": "refund_process_001"
}
这样用户问:
退款流程第四步失败怎么办?
系统能精准召回第 4 步。
用户问:
退款完整流程是什么?
系统可以根据:
processName = 退款流程
stepNo = 1 到 6
totalSteps = 6
把完整流程拼回来。
这就是流程类文档切分的关键:
流程不是普通文本,流程必须结构化。
八、Small Chunk:用于精准召回
Small Chunk 的作用是:
让系统更容易命中用户问题。
比如用户问:
退款失败后订单状态怎么处理?
如果 Chunk 太大,里面同时包含退款申请、风控校验、渠道退款、订单状态、通知等内容,Embedding 会变得很“泛”,不够精准。
Small Chunk 可以只保留一个知识点:
订单状态处理规则:支付渠道退款失败时,订单状态保持为“退款处理中”,系统记录失败原因,并进入重试队列。
这种小块非常适合向量检索。
但 Small Chunk 也有缺点:
上下文不完整。
所以不能只用 Small Chunk,还要配合 Parent Chunk。
九、Parent Chunk:用于完整上下文恢复
Parent Chunk 可以理解为 Small Chunk 的上级内容。
例如:
Parent Chunk:
退款流程完整章节
Small Chunk 1:
用户提交退款申请
Small Chunk 2:
系统校验订单状态
Small Chunk 3:
调用支付渠道退款
Small Chunk 4:
退款失败后的订单状态处理
检索时先用 Small Chunk 命中问题:
用户问题:退款失败后订单状态怎么处理?
命中 Small Chunk 4
然后系统根据 Small Chunk 里的 parentId 找到 Parent Chunk:
parentId = refund_section_001
最后把 Parent Chunk 或 Parent Chunk 中相关部分交给大模型。
LangChain 的 ParentDocumentRetriever 就是这个思路:先检索小块,再根据小块找到它所属的父文档或大块内容并返回,从而平衡精准召回和上下文完整性。
一句话总结:
Small Chunk 负责“找得准”
Parent Chunk 负责“讲得全”
十、Window Retrieval:召回命中点附近的上下文
Window Retrieval 适合解决一种常见问题:
召回到的句子是对的,但前后文不够。
比如命中句子是:
系统会进入人工审核。
单看这句话,大模型不知道:
为什么进入人工审核?
什么情况下进入人工审核?
人工审核之后做什么?
所以需要把它前后几句话一起取出来:
上一句:如果用户提交的身份证信息与银行卡信息不一致,系统会判定为高风险。
命中句:系统会进入人工审核。
下一句:人工审核通过后,用户可继续完成开户流程。
这就是 Window Retrieval。
LlamaIndex 的 Sentence Window 思路就是:检索时可以命中一个句子,但通过 metadata replacement 把该句子替换成它周围的上下文窗口,从而让模型看到更完整的语境。
企业里可以这样设计:
{
"chunkId": "chunk_1001",
"content": "系统会进入人工审核。",
"windowContent": "如果用户提交的身份证信息与银行卡信息不一致,系统会判定为高风险。系统会进入人工审核。人工审核通过后,用户可继续完成开户流程。",
"windowSize": 1
}
检索用 content。
生成答案用 windowContent。
这就能做到:
召回精准
上下文完整
回答不容易断章取义
十一、Parent Retrieval:小块召回,大块回答
Parent Retrieval 和 Window Retrieval 有点像,但范围更大。
Window Retrieval 通常取命中点前后几句。
Parent Retrieval 通常取命中 Chunk 所属的父级段落、章节或文档片段。
比如:
Small Chunk:
支付渠道退款失败时,订单状态保持为“退款处理中”。
Parent Chunk:
退款流程章节,包括退款申请、订单校验、渠道退款、失败重试、状态更新、通知用户。
用户问:
退款失败后系统怎么处理?
系统流程:
1、用用户问题检索 Small Chunk
2、命中“退款失败状态处理”
3、根据 parentId 找到退款流程章节
4、把相关 Parent Chunk 给大模型
5、大模型生成完整答案
这样回答会更完整:
退款失败后,系统不会立即关闭退款单,而是将订单状态保持为“退款处理中”,记录渠道失败原因,进入重试队列。如果多次重试失败,则转人工处理,并向用户展示处理中状态。
如果只给 Small Chunk,模型可能只能回答一句话。
十二、Section Retrieval:按章节恢复完整语境
Section Retrieval 适合处理长文档。
比如用户问:
黑名单规则有哪些?
如果只召回一个 Chunk,可能只看到“手机号黑名单”。
但实际上完整答案应该包含:
身份证黑名单
手机号黑名单
银行卡黑名单
设备指纹黑名单
IP 黑名单
账户行为黑名单
Section Retrieval 的做法是:
1、先召回多个 Small Chunk
2、判断它们属于哪个 section
3、如果同一个 section 命中数量较多
4、返回整个 section 或 section 摘要
比如命中结果:
chunk_01:身份证黑名单
chunk_02:手机号黑名单
chunk_03:设备指纹黑名单
它们都属于:
sectionId = risk_blacklist_rules
sectionTitle = 黑名单规则
那系统就可以把整个“黑名单规则”章节拿出来。
这比单纯 topK 检索更稳定。
十三、长文档如何处理?不能一次切到底
长文档处理要分层。
不要这样:
一篇 100 页 PDF
↓
每 500 字切一块
↓
全部丢向量库
更好的方式是:
文档级
↓
章节级
↓
段落级
↓
语义 Chunk
↓
句子窗口
可以设计成:
{
"docId": "doc_001",
"docTitle": "风控规则说明书",
"sectionId": "sec_003",
"sectionTitle": "黑名单规则",
"parentChunkId": "parent_003_001",
"chunkId": "chunk_003_001_004",
"chunkType": "rule",
"content": "手机号命中黑名单时,系统会拒绝开户申请。",
"windowContent": "用户提交开户申请后,系统会校验手机号。如果手机号命中黑名单,系统会拒绝开户申请,并提示用户联系客服。",
"metadata": {
"businessType": "开户",
"ruleType": "黑名单",
"sourcePage": 12
}
}
这样每个 Chunk 都不是孤立文本,而是带着完整身份。
十四、推荐的企业级 Chunk 入库流程
一个比较完整的流程可以这样做:
1、文档上传
↓
2、格式解析:Word / PDF / HTML / Markdown
↓
3、版面识别:标题、表格、段落、图片、页码
↓
4、结构化解析:章节、标题、列表、流程、表格
↓
5、清洗:去页眉页脚、去重复、修复断行
↓
6、标题切分:按一级、二级、三级标题建立层级
↓
7、语义切分:把章节内容切成完整知识点
↓
8、流程识别:提取 stepNo、totalSteps、processName
↓
9、生成 Small Chunk
↓
10、生成 Parent Chunk
↓
11、生成 Section Chunk 或章节摘要
↓
12、Embedding 向量化
↓
13、写入向量库 + 元数据索引
↓
14、检索时执行 Small Chunk + Parent / Window / Section 恢复
这套流程的核心不是“切得越碎越好”,而是:
检索时要精准
回答时要完整
溯源时要清楚
流程时要有顺序
十五、Chunk 元数据应该怎么设计?
一个生产级 Chunk 不应该只有 content。
建议至少包含这些字段:
{
"chunkId": "唯一 Chunk ID",
"docId": "文档 ID",
"docTitle": "文档标题",
"sectionId": "章节 ID",
"sectionTitle": "章节标题",
"headingPath": "一级标题 > 二级标题 > 三级标题",
"parentChunkId": "父 Chunk ID",
"chunkType": "rule / faq / process_step / table / paragraph",
"content": "用于向量化的小块内容",
"windowContent": "命中点前后窗口内容",
"parentContent": "父级上下文",
"pageNo": "来源页码",
"sourceType": "pdf / word / markdown / html",
"businessTag": "业务标签",
"stepNo": "流程第几步",
"totalSteps": "流程总步数",
"createdAt": "创建时间",
"version": "文档版本"
}
为什么要这么多字段?
因为检索不只是向量相似度。
真实业务里经常需要:
按文档过滤
按业务线过滤
按版本过滤
按章节过滤
按流程顺序恢复
按页码溯源
按 chunkType 做不同策略
没有元数据,后面很难做精细化检索。
十六、不同类型文档,切分策略不一样
1、FAQ 文档
FAQ 最好按问答对切:
Q:账户被冻结怎么办?
A:用户可以通过 App 提交解冻申请……
不要把多个问答切在一个 Chunk 里。
推荐:
{
"chunkType": "faq",
"question": "账户被冻结怎么办?",
"answer": "用户可以通过 App 提交解冻申请……"
}
2、制度规则文档
按规则点切:
规则名称
适用范围
触发条件
处理方式
例外情况
3、流程文档
按流程步骤切,并记录:
processName
stepNo
totalSteps
prevStep
nextStep
4、接口文档
按接口或字段切:
接口名称
请求参数
返回参数
错误码
调用示例
5、表格文档
表格不能简单转成一堆文本。
要保留:
表头
行列关系
单位
备注
表格标题
比如:
产品 | 费率 | 生效时间
A产品 | 0.3% | 2025-01-01
B产品 | 0.5% | 2025-02-01
不能切成:
A产品 0.3%
B产品 0.5%
否则大模型不知道 0.3% 是什么。
十七、Chunk 大小怎么选?
没有一个绝对标准,但可以按场景理解。
1、短 FAQ
200~500 字
适合精准问答。
2、业务规则
300~800 字
保证规则完整,不要切断条件和处理方式。
3、流程步骤
每一步一个 Chunk
同时保留整个流程的 Parent Chunk。
4、长章节
Small Chunk:300~600 字
Parent Chunk:1000~2000 字
Section Chunk:整个章节或章节摘要
LlamaIndex 的层级解析也体现了类似思路:可以把文档解析成多层节点,比如较大的 2048 级别、中等的 512 级别、更小的 128 级别,子节点会关联到父节点。
重点不是数字本身,而是层级思想:
小块用于检索
大块用于上下文
章节用于完整理解
十八、Overlap 重叠要不要加?
要加,但不能乱加。
Overlap 的作用是防止边界处信息丢失。
比如:
Chunk 1 结尾:用户连续输错密码 5 次后,系统会锁定账户。
Chunk 2 开头:锁定时间为 30 分钟。
如果没有重叠,用户问“锁定多久”,可能只召回 Chunk 1,答案不完整。
加一点重叠后:
Chunk 1:
用户连续输错密码 5 次后,系统会锁定账户,锁定时间为 30 分钟。
Chunk 2:
锁定时间为 30 分钟。用户可以通过短信验证码解锁。
但 Overlap 太大也不好:
存储变多
召回重复
上下文浪费
答案容易啰嗦
一般建议:
普通段落:10%~20% 重叠
流程步骤:不建议靠 overlap,应该靠 stepNo 关联
章节文档:靠 parentId 和 sectionId 恢复上下文
十九、检索时应该怎么组合?
一个成熟的检索链路可以这样设计:
用户问题
↓
Query Rewrite:问题改写
↓
向量召回 Small Chunk
↓
关键词召回补充
↓
元数据过滤
↓
Rerank 精排
↓
判断 Chunk 类型
↓
如果是流程:按 stepNo 补全前后步骤
↓
如果是长文档:走 Parent Retrieval
↓
如果是句子命中:走 Window Retrieval
↓
如果多个 Chunk 属于同一章节:走 Section Retrieval
↓
组装上下文
↓
大模型生成答案
↓
返回答案 + 引用来源
这里的关键是:
召回阶段不要给太大内容
生成阶段不要只给小碎片
二十、一个完整案例:退款流程文档怎么切?
原文:
退款流程
第一步:用户提交退款申请,填写退款原因。
第二步:系统校验订单状态,只有已支付订单允许退款。
第三步:系统判断是否超过退款期限。
第四步:调用支付渠道发起退款。
第五步:如果退款成功,更新订单状态为已退款。
第六步:如果退款失败,订单状态保持退款处理中,并进入重试队列。
第七步:系统通过短信和站内信通知用户退款结果。
1、Small Chunk
{
"chunkType": "process_step",
"processName": "退款流程",
"stepNo": 6,
"totalSteps": 7,
"content": "第六步:如果退款失败,订单状态保持退款处理中,并进入重试队列。"
}
2、Parent Chunk
{
"chunkType": "process_parent",
"processName": "退款流程",
"content": "退款流程包括:用户提交退款申请、系统校验订单状态、判断退款期限、调用支付渠道、成功后更新为已退款、失败后保持退款处理中并进入重试队列、通知用户退款结果。"
}
3、Window Content
{
"stepNo": 6,
"windowContent": "第五步:如果退款成功,更新订单状态为已退款。第六步:如果退款失败,订单状态保持退款处理中,并进入重试队列。第七步:系统通过短信和站内信通知用户退款结果。"
}
4、用户提问
退款失败后订单是什么状态?
5、检索结果
命中 Small Chunk:
第六步:如果退款失败,订单状态保持退款处理中,并进入重试队列。
6、上下文恢复
补充 Window:
第五步:退款成功更新为已退款。
第六步:退款失败保持退款处理中并进入重试队列。
第七步:通知用户退款结果。
7、最终回答
退款失败后,订单不会直接变成已退款,也不会关闭,而是保持“退款处理中”状态,并进入系统重试队列。系统后续会继续尝试退款,并通过短信或站内信通知用户结果。
这个答案就比单句回答更完整。
二十一、常见错误:为什么很多 RAG 系统回答不好?
1、只按字数切
结果:流程断了,规则断了,标题丢了。
2、只存 content,不存 metadata
结果:无法按文档、章节、业务线、版本过滤。
3、Chunk 太大
结果:召回不精准,Embedding 表达变模糊。
4、Chunk 太小
结果:召回片段太碎,大模型回答缺上下文。
5、流程文档不记录步骤
结果:大模型不知道前后顺序,容易乱答。
6、长文档没有 Parent / Section
结果:只能回答局部,不能回答完整章节问题。
7、表格直接转纯文本
结果:行列关系丢失,费率、时间、条件对应不上。
二十二、企业级推荐方案:多粒度 Chunk + 元数据 + 上下文恢复
最终推荐架构是:
Small Chunk
负责精准召回
Window Chunk
负责补充前后文
Parent Chunk
负责恢复段落级上下文
Section Chunk
负责恢复章节级上下文
Metadata
负责过滤、排序、溯源、流程恢复
可以理解为:
Small Chunk 是“搜索命中点”
Parent Chunk 是“上下文说明书”
Section Chunk 是“章节地图”
Metadata 是“导航系统”
只要这四层做好,RAG 的准确率、完整性、可解释性都会明显提升。
二十三、总结
Chunk 切分不是简单的文本切割,而是知识库建设里最核心的工程能力之一。
普通系统会问:
一段切多少字?
成熟系统会问:
这个文档是什么类型?
它的标题结构是什么?
它有没有流程?
它有没有表格?
它的最小知识单元是什么?
它的父级上下文是什么?
用户问具体问题时怎么精准召回?
用户问完整流程时怎么恢复全貌?
真正好用的 RAG 系统,一定不是只靠大模型硬答,而是在入库阶段就把知识结构设计好。
最后用一句话总结:
Small Chunk 解决“找得准”,Parent Chunk 解决“讲得全”,Window Retrieval 解决“不断章取义”,Section Retrieval 解决“长文档完整理解”,流程类文档必须记录 stepNo 和 totalSteps。
Chunk 切分做得越细,RAG 系统上线后越稳。切分阶段偷懒,后面就只能靠 Prompt、Rerank、模型能力不断补坑。
更多推荐
所有评论(0)