做了三年 RAG 落地项目,被文档切分坑过无数次之后,我发现大部分检索精度问题根本不是 Embedding 模型不够好,而是切分这一步就没做好。你这边的向量检索结果乱七八糟、召回的内容答非所问,先别急着换模型、调 prompt,回去把切分策略重新捋一遍,效果往往立竿见影。这篇文章我把自己的切分经验和踩坑记录整理成一份完整攻略,从切分策略选型到参数设置再到问题排查,全部讲透。
1. 为什么说“切不对,检索全白费”
1.1 切分决定了检索的下限
RAG 的流程听起来很简单:文档切块 → 向量化 → 存入向量库 → 检索 → 交给大模型生成。但很多人忽略了一个关键事实:大模型只能看到你检索回来的那几段内容,切分质量直接决定了它“能看到什么”。如果一块内容本身就不完整、语义被拦腰截断,后面 Embedding 再强、大模型再聪明也救不回来。
我习惯把 RAG 检索精度拆成两条线:一条是召回率,看看该回来的有没有回来;另一条是相关度,看看回来的内容到底是不是用户想问的。文档切分对这两条线都有决定性影响,尤其对相关度的影响几乎是立竿见影的。一个典型场景:用户问“这个项目的部署需要什么环境”,如果文档切分时把“环境要求”这一节和“部署步骤”那一节强行合并成一个大块,检索时确实能召回,但内容里混了一堆无关操作步骤,大模型从中提取答案的难度就成倍增加了,最终 Answer 的质量自然惨不忍睹。
1.2 切分质量直接决定三件事
第一是语义边界是否完整。每个 Chunk 都应该是一个相对独立、能自解释的信息单元。比如一段介绍“系统登录”的文字,如果切到一半就断了,后半句话跑到下一个 Chunk 里,检索时这个 Chunk 会带上一个残缺的句子,语义漂移是必然的。
第二是信息密度是否合适。切分过大,Chunk 里塞了太多主题,向量表示会被拉平,检索时什么都沾点边但什么都不精确;切分过小,单个 Chunk 语义不足,别说大模型,人看到那半句话都不知道在说啥,向量检索结果更是五花八门。
第三是上下文连贯性是否保留。文档中的指代词(“如上所述”“该功能”“此参数”)、递进逻辑、因果链条,是你切分时最容易破坏的东西。一旦上下文断裂,检索回来的片段就变成了“无源之水”,大模型只能靠猜。很多生产环境里的 RAG 效果差,根因不在模型选型,而在切分环节把文档的“气”切断了。
2. 切分方案选型:没有最好,只有最合适
2.1 四种主流切分方式对比
我把常用切分方式分成四类,实际项目中基本都是围绕这四类做组合:
| 切分方式 | 核心思路 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 固定长度切分 | 按字符或 token 数硬切 | 实现最简单、速度最快 | 语义边界完全不可控 | 日志、格式极其统一的数据 |
| 递归字符切分 | 按分隔符优先级逐级切 | 实现简单、语义边界有一定保障 | 对结构化文档不敏感 | 通用文本,是默认最快方案 |
| 结构感知切分 | 按 Markdown/HTML/PDF 结构切 | 保留标题层级、表格完整 | 不同格式需定制解析 | 产品手册、技术文档、公众号文章 |
| 语义切分 | 利用 Embedding 相似度找断点 | 语义完整性最好 | 计算成本高、引入额外模型环节 | 对检索精度要求极高的核心知识库 |
这里多说一句,固定长度切分并不是一无是处。有些场景(比如日志分析、固定格式的设备上报数据)每一条记录本身就是完整的语义单元,直接按固定长度或固定分隔符切完全没问题。但如果是技术文档、产品介绍、方案书这类非结构化文本,固定长度切分基本等于自杀,你会把完整段落拦腰截断,Chunk 与 Chunk 之间语义纠缠不清。
2.2 抛开场景谈参数都是耍流氓
切分参数不是拍脑袋定的,而是要结合文档类型、检索粒度、大模型上下文窗口三个因素来分析。
文档类型决定了先天特征:技术手册天然有章节结构,适合结构感知切分;公众号文章逻辑松散,适合语义切分;合同、论文这类严谨文本,段落边界本身就有语义含义,直接尊重段落结构就是最佳方案。检索粒度决定了切分块的大小:如果用户提问大多是“某个概念是什么”“某个参数什么意思”这类精准问题,块必须足够小,保证答案是聚焦的;如果提问大多是“对比 A 和 B 的差异”“总结某章节内容”这类大粒度问题,块太小反而让检索结果零散破碎,需要配合父文档检索策略来恢复上下文。上下文窗口决定了你能容忍多大的 Chunk:上下文窗口小,Chunk 太大直接塞不下,必须做压缩;上下文窗口大(比如 128K 以上的模型),你可以适度放宽 Chunk 大小,让每个 Chunk 承载更完整的信息,降低切分难度。
我在官网文档场景里最常用的方案是:先用结构感知切分器识别标题层级,把文档切到二级或三级标题粒度,再对每个大节内部执行递归字符切分。因为官网文档的标题本身就是人工总结的语义核心,跟着标题走,基本不会跑偏。标题切完以后,大节内部往往还有长段落,这时候才轮到递归切分器上场,用小步长把段落切到适合 Embedding 的长度。
3. 实操落地:从原始文档到高质量 Chunk
3.1 步骤一:文档解析与清洗
这一步很多人会跳过,但它是切分的前置条件。你拿到的原始文档可能是 PDF、Word、Markdown,甚至是扫描件。不同格式解析出来之后的“干净程度”天差地别:PDF 容易出现栏位错乱、文本顺序颠倒、表格被拆碎;Word 带各种隐藏样式和批注;Markdown 本身还好,但可能嵌了图片和代码块。
我的建议是在进切分器之前先做一次清洗,把以下问题解决掉:
- 移除页眉页脚、页码、目录信息(这些内容检索时没有任何价值,反而会污染语义)
- 修正 PDF 解析造成的段落乱序(可以用解析工具的坐标信息做重排)
- 把表格转换成 Markdown 表格,或者在切分前单独提取出来
- 统一换行符和编码格式,尤其是从 Windows 和 macOS 混传的文档
这一步看起来不起眼,但实测下来,解析清洗做不做,直接影响后续切分边界的准确率。我自己就遇到过 PDF 解析出来段落顺序错乱,导致切出来的 Chunk 里句子前后颠倒、完全不可读的诡异情况。
3.2 步骤二:按结构划分一级块
拿到干净文本后,第一步不是直接上切分器,而是先做结构划分。我会先用正则或解析库识别文档里的标题层级(H1、H2、H3),把文档组织成一棵树,然后决定在哪一层作为“一级块”的边界。
比如一份产品部署文档,它的结构可能是:
# 产品部署指南 ## 环境要求 ### 硬件配置 ### 软件依赖 ## 部署步骤 ### 初始化 ### 启动服务我通常会把一级块定在 H2 这一层,也就是“环境要求”和“部署步骤”各作为一个待切分的大块。为什么不直接定位到 H3?因为单个 H3 下面的内容往往太短,语义量不足以支撑一次高质量检索。但也不排除 H3 下面是个超长列表、超长表格的情况,那就需要继续往下拆。所以“一级块”定的原则是:让每个块具备独立回答某类问题的信息量。
实现这一层时,我会先用代码把每个二级标题下的文本范围提取出来,存成“段落块”列表。这一步的关键是标题层级提取要准,否则后面全乱了。
3.3 步骤三:用递归切分器设置核心参数
结构划分之后,一级块可能还是太长(比如一个 H2 下面有 3000 字),这时就需要递归切分器上场了。我以目前最通用的递归字符文本切分器为例,说下参数怎么设。
分隔符优先级是递归切分器的灵魂。它的原理是:先尝试用最优先的分隔符(通常是换行符)切,如果切出来的块还是太大,就换下一级分隔符(句号、感叹号、问号、逗号、空格)继续切,直到每个块满足大小要求。对于中文文档,我会把分隔符优先级设置成:
["\n\n", "\n", "。", "!", "?", ";", ",", " ", ""]注意我把中文句号、感叹号、问号优先于逗号和空格,这样能尽量保证切分边界落在完整句子之后,而不是让一个句子被硬生生劈成两半。很多英文模板默认把空格放在比较高优先级,直接用在中文文本上会导致句子被切得稀碎,这是新手非常容易踩的坑。
**块大小(chunk_size)和重叠(chunk_overlap)**的配合,建议这样做:初始块大小可以从 300 到 800 token 之间选,我拿产品文档类项目做测试,500 token 左右是性价比比较高的范围。偏向科普问答类的文档可以降到 300 token,偏向深度技术文档的可以升到 800 token。重叠区域一般设 50 到 100 token,或者按块大小的 10%-20% 来算。比如 500 token 的块,overlap 我通常设 50。
设置 overlap 的原因很朴素:为了照顾跨块边界的关键信息。比如“参数 k 的取值范围是 0 到 100”这句话,如果“取值范围是 0 到 100”落到了下一个 Chunk,而用户的问题是“k 的取值范围”,检索时可能只命中第一个 Chunk 里“参数 k”这几个字,答案就被切丢了。overlap 就是给边界信息上的“双保险”。
3.4 步骤四:嵌入与验证
切分只是过程,最终效果要看嵌入和检索。我强烈建议你在设定切分方案后,不要直接端到端跑完看最终回答,而是单独验证检索召回的效果。做法是准备一组测试问题,对每个问题手动标出应该召回哪些标准段落,跑一遍检索,算召回率和命中率。
这一步很多人偷懒不做,结果整个链路跑完发现效果差,根本不知道是切分的问题、Embedding 的问题还是生成的问题。花的全是排查时间。我的经验是:先用经典 Embedding 模型做基线,再验证切分方案,最后再逐步调 Embedding 参数。一旦召回结果稳定,再进到端到端测试,这样定位问题会清晰得多。
4. 常见事故现场与排查手册
4.1 检索结果老是不相关,八成是切分问题
典型事故一:用户问“数据库连接超时怎么排查”,结果召回的内容是“数据库优化建议”那一整节,而且这节内容又臭又长。这种情况十有八九是切分块太大,向量把整个大节的内容平均“揉”在了一起,检索时匹配到的是整节的综合语义,而不是针对“连接超时”的具体语义。处理方式就是调小块大小,同时检查大节内部有没有按小标题再切一层。
典型事故二:召回结果只有半句话,信息残缺。这种情况通常是切分边界落在了句子中间,或者 overlap 设置过小导致边界信息丢失。处理方式是把分隔符里的句子终结符优先级往前提,并且适当增大 overlap。
典型事故三:召回结果张冠李戴,把 A 概念的解释和 B 概念的参数说明混在一个块里。通常是文档结构切分没做好,标题层级识别失败,导致一个块跨越了两个主题。这种情况需要回到结构划分步骤,改用结构感知切分器,或者手动调整一级块边界。
4.2 中文文档切分的专属坑
中文文本跟英文文本最大的区别是没有天然空格边界,而且单字信息密度远高于英文字母。这带来几个实际问题:
第一个坑是分词粒度。直接用英文默认的字符切分器切中文,很容易把完整成语、专业术语切碎。比如“卷积神经网络”这个术语被切到两个块里,Embedding 时语义就缺损了。我遇到的解决思路有两个:一是把中文常用词、领域术语作为不可切分单元处理;二是优先用句末标点做边界,而不是用空格。
第二个坑是标点符号。中文标点有全角半角之分,解析出来的文档很容易混用。如果你在设置分隔符时只考虑了半角逗号和句号,遇到全角标点文档就会失效,切分器沿着「无标点」的长文本硬切,效果会非常难看。我的做法是在清洗阶段统一把全角标点转成半角(或者反过来统一语义),再送入切分器。
第三个坑是中文段落往往“意合”而非“形合”,经常一个自然段包含多层意思,或者多个段落围绕同一个主题展开。这时候只按自然段切分并不一定合适。我一般会先对中文文本做句子切分,再根据句子的语义相似度合并成块,这其实就是语义切分的思路。如果不想引入额外的语义切分模型,也可以退而求其次:把中文的段落首句作为检索的核心单元,段落其他部分作为扩展上下文。
4.3 表格和代码块怎么切
表格和代码块是切分时的“老大难”。很多人直接把它当普通文本扔进切分器,结果表格被切成三行五块,列头跑到上一个块,数据跑到下一个块,检索出来根本没法看。
我现在的做法是:遇到表格和代码块,先单独提取出来,不参与普通文本的递归切分。表格我会转成 Markdown 格式,并且保留表头,让每行数据都带上字段名信息;代码块我会按函数、类、配置块等逻辑单元切分,而不是按字符硬切。这样检索到表格某一行时,它天然携带上下文(表头),不会出现“光看到数字不知道啥意思”的情况。
表格切分的模板可以这样:先按行拆,每行附带表头;再按列拆,每列附带行索引。如果表格太大,可以按主题拆成多个小表,每个小表自带标题。实测下来这种策略对“参数查询类”问答非常友好,用户问“某参数默认值是多少”,我能精确锁到那一行的数据。
5. 一些实测经验与展望
直接说几个我反复验证过的结论,你拿去就能用。
块大小跟领域强相关。代码文档类我建议 300 到 500 token,技术博客类 500 到 800 token,政策法规类文献 800 到 1000 token 也是可以接受的。核心思想是:越需要精确定位的信息,块要越小;越需要理解上下文的信息,块要越大。
overlap 不是越大越好。overlap 过大,会导致同一个信息被多次检索到,去重逻辑一旦做不好,生成结果会变得啰嗦。我一般控制在块大小的 15% 以内。
不要所有文档用同一套参数。我如今的做法是给知识库做一次“文档画像”,按文档类型、平均段落长度、标题层级深度三个维度分类,每一类单独配置切分策略。虽然前期多花了一些分析时间,但后期检索效果稳定得多。
优先引入结构信息。切分时把标题、小节名、段落号作为元数据随 Chunk 一起入库,检索命中后可以把这部分元数据拼进大模型的上下文里,回答时上下文完整度会提升非常多。这一招我实测下来对“某功能在哪一章节、怎么操作”这类问题尤其有效。
最后,切分方案要留出“返工”余地。别把切分代码写死在业务流程里,最好把切分结果缓存下来,方便调整参数后快速对比评测。我自己的项目里,切分和向量化都是可重跑的任务,一个参数变化就能在五分钟内重新生成全部 Chunk 并做对比,这样迭代效率才高。
文档切分没有“一次到位”的银弹,它本质上是围绕文档结构、语义边界和检索粒度做的一次信息工程。把这个环节打磨好,你的 RAG 检索精度立竿见影地往上走,后面再去追模型选型和 Prompt 优化才有意义,不然一切都是空中楼阁。