切分:效果的一半,就看你切得好不好
约 2809 字大约 9 分钟
2026-09-14
先把结论拍桌上:切分是整条 RAG 管道里对效果影响最大的单一环节。不是模型,不是检索算法,是切分。听起来有点反直觉——最不起眼的「把文章剁成段」决定了上限?这篇就讲清楚为什么,以及怎么剁才不糟蹋文章。
本篇对应 relax-admin 的「切片治理」一整个菜单组:入库策略、切片列表、切片测试、质量分析,四页正好是切分这件事的四个动作——配参数、看结果、试效果、看统计。也是整个管理端里我最喜欢的一组页面,因为它把「切分手感」这种玄学变成了看得见的东西。
先理解切分到底在权衡什么
切分这件事,骨子里是一个两头堵的权衡:
切小了,检索准。 一段只讲一件事,它的向量就是「这件事」的纯粹表达,语义集中,一查一个准。但切太小,召回来的是残句——「需要手动开启」四个字进了大模型的上下文,它也不知道要开启什么。
切大了,内容全。 一段自包含前因后果,大模型拿到就能答题。但一段讲五件事,它的向量是五件事的平均值,哪件事都检索不准——你想搜「退款」,它可能因为「发货」的那部分语义被捞回来。
一句话总结:
一句话记住
检索想要小而锋利,生成想要大而完整,切分就是在这个矛盾里找平衡点。后面所有策略、参数、花活,全是这句话的展开。
切分策略从粗暴到精细,一档档看
第一档:按固定长度硬切
每 500 字来一刀。快是快,但它完全不知道「语义」俩字怎么写——「退款金额为每月 29 | 元起」从中间断开,前半段能被「退款」检索到,送进模型却答不了题。这一档只配当反面教材,或者给完全无结构的日志文本兜底。
第二档:按分隔符递归切
思路是「尽量在自然边界断刀」:先按段落(\n\n)切,段落太长就按行(\n)切,还长就按句子(。!?)切,实在不行才硬切。LangChain 的 RecursiveCharacterTextSplitter 就是这个,业界默认选择。
有个中文特有细节:网上抄来的分隔符列表清一色是英文标点,中文的 。!?; 得自己加,不然中文长段落会一路退化到硬切。
第三档:按 token 切
和第二档的区别是计量单位。为什么单位必须是 token 不是字数?因为:
- Embedding 模型的输入上限按 token 算,超了直接报错
- 大模型的上下文预算也按 token 算,而中文一个字和英文一个词的 token 数没有稳定换算
Java AI 的 TokenTextSplitter 就是这档,默认 400 token 一段,是个不错的起点。
第四档:按 Markdown 结构切(我们的主角)
按文章的天然结构下刀:一个二级标题下的内容切成一段,小节太长就退化为第二档继续切。这是我们博客场景的正解,原因上一篇埋过了——解析时保住了标题层级,切分就该用上。
按结构切的三个直接收益:
- 代码块永远完整。一个代码块就是一个原子单元,绝不从中间切开。半段代码进模型等于让它凭空补全,这是大忌
- 段落天然语义完整。「## 方案二:分布式锁」下面那坨内容,讲的就是分布式锁,边界比任何算法猜的都准
- 标题路径白送。切的时候顺手把标题路径记下来(「下单接口 > 方案二:分布式锁」),这个路径马上有大用
第五档:语义切分
用 Embedding 算相邻句子的相似度,在「话题转折点」下刀。理论最优,实际又贵又难调,效果提升还不稳定。
我的建议
知道有语义切分这个东西就行,第一版别碰——把前四档做扎实,收益比它大得多。
参数手感:size 和 overlap 怎么定
策略定了,轮到两个参数。这是我被问得最多的问题,答案可能让你失望:没有万能数字,但有靠谱的定法。
这些参数在 relax-admin 里就活在「切片治理 → 入库策略」页:每个知识库一组策略配置(按库分参数,而不是全局写死——这是前面说过的「切分参数要随文档类型可配置」的落地形态),策略改了之后走「按库全删全灌」重建,旧切片不会和新切片打架。这一页还承担了策略版本管理的角色:每次调参是一个新版本,随时能回滚。
📷 图片位(待补充)
应放什么图:「切片治理 → 入库策略」页截图,圈出博客知识库的那组配置:切分方式(Markdown 结构化)、chunk_size 512、overlap 0、碎屑合并阈值,以及策略版本切换的控件。
chunk_size(每段多大):通用知识库的甜点区在 384 到 1024 token,博客这种「一个标题下讲透一件事」的语料,512 是个舒服的起点。真正该做的不是抄数字,而是看语料的语义密度:一句顶一段的干货文切小点,车轱辘话多的切大点。
chunk_overlap(相邻段重叠多少):解决的是「关键句恰好骑在刀口上」的问题——有了重叠,边界句在相邻两段都活着,哪段被召回信息都完整。经验值是 10% 到 20%(512 的重叠就是 50 到 100 token)。
但有个反直觉的点要指出:重叠是硬切的创可贴,不是普适美德。按结构切的段落,边界本来就在语义断点上,根本不需要重叠——硬加重叠反而制造大量雷同段落,检索时挤占名额:top5 里三条几乎一样,有效召回等于两条。我的博客方案里,结构化切分的段落重叠设成了 0,只有小节内部退化硬切时才给重叠。
另外送一个不显眼但真会遇到的问题:碎屑合并。切完会剩些二十来字的碎渣(一个孤零零的标题、一行过渡句)。这种碎渣单独入库就是检索噪声——信息量趋近于零,却可能因为字面撞词冲进结果。小于阈值的碎屑,合并进相邻段落,别让它们单独成段。
上下文增强:让切片不再当「孤儿」
这是切分里区分「跑通了」和「做好了」的部分,三招,成本从低到高。
第一招:标题路径前缀(必须做,性价比全管道第一)
切出来的段是「孤儿」——它脱离了原文位置,经常自己说不清自己。经典病例:某段内容是「该功能默认关闭,需手动开启」,它原本属于「## 缓存配置」这节,单独看就是天书。
解法土得掉渣但极其有效:把标题路径拼在每个段落前面再去算向量:
【别让重复请求搞垮你的下单接口 > 方案二:分布式锁】通过 SET NX 命令尝试获取锁……一石三鸟:「分布式锁」这些标题词进了向量,检索命中率肉眼可见地涨;段落进了模型上下文后自带语境;用户问题里出现标题词时字面语义双重对齐。一行代码级别的改动,我愿称之为全管道性价比之最。
第二招:元数据继承(上一篇的老朋友)
父文档的所有元数据——slug、分类、时间——逐段继承。不展开,反正不做的话,后面所有按文章过滤、按分类过滤的功能全瘫。
第三招:父子切片(第一版之后的第一个升级项)
终极解法,直接把「大小矛盾」拆掉:检索用小切片(比如 256,向量锋利),命中后送模型用它的父块(比如 1024,上下文完整)。实现上是子块入向量库、元数据里记 parent_id,检索到子块后回查父块拼装。
现在不用做,但架构上知道它在那儿——等你被「召回准了但答案还是缺上下文」折磨的时候,它就是解药。
怎么知道切得好不好
最后给个实操心法,也是切分这个环节的「验收标准」。
relax-admin 在这里给了两件趁手的工具:
- 「切片列表」页:每篇文档切出来的段落逐条展示——标题路径、token 数、归属文档。前面说的「切片尸检」就在这页做:找到出问题的段落,肉眼看它是不是被刀口切散了
- 「切片测试」页(playground):输入一个真实问题,立刻看到会命中哪些切片、各自的内容和元数据——切分效果不进检索流程就能单测,这是调参时最快的反馈回路
📷 图片位(待补充)
应放什么图:「切片测试」页截图,展示输入「怎么防止重复下单」后命中的切片列表(带标题路径和相似度),最好能体现一条命中切片的完整内容展开——含标题路径前缀的效果。
切片尸检法:找一个「答错了的问题」,手动找到它本应命中的那段原文(行话叫 gold chunk),然后问两个问题:
- 这段信息是否完整地存在于某个切片里?不存在——信息被刀口切散了,切分问题,调 size 或换结构化切
- 存在但没被召回——那是检索的问题,之后几篇的事
排查口诀
切分的问题特征是「该有的信息不在任何切片里完整存在」;检索的问题特征是「存在但没被召回」。这句口诀能帮你省掉无数南辕北辙的调参。
今天的落地清单
切分讲完,数据侧还剩两步:把段落变成向量,把向量存进库。下一篇先讲 Embedding——顺便算一笔账:为什么「切分别乱调参」这件事,最后是用账单教做人的。
下篇见。