1. 为什么切片是 RAG 系统里最被低估的环节
做 RAG 应用的人,十个里有八个把精力砸在模型选型和向量库调参上,剩下两个在折腾提示词模板。但真正跑过几个生产级项目之后你会发现,决定一个 RAG 系统到底是“能用”还是“好用”的分水岭,往往不在那些光鲜的环节,而在最不起眼的文档切片上。
我见过太多团队,embedding 模型换了三四个,重排模型加了两层,检索效果就是上不去。最后回头一看,切片策略还是最原始的“按固定字数硬切”,一段完整的话被拦腰截断,上下文碎得拼不起来。检索出来的片段语义不完整,再强的模型也救不回来。
切片这件事,本质上是在做一件很朴素的工作:把长文档拆成一段段语义自洽、长度合适的文本块,让后续的向量化、检索、生成都有干净的输入。它不涉及什么高深算法,但每一个决策——切多长、在哪切、要不要重叠、要不要保留结构——都会沿着流水线一路放大,最终体现在回答质量上。
这篇文章适合正在搭建或优化 RAG 系统的开发者,也适合那些已经跑通了 demo 但效果始终差口气的团队。我会把切片这件事从原理到实操拆开讲,包括不同文档类型的处理策略、参数怎么定、坑在哪里、怎么排查问题。看完你至少能判断:你现在的切片方案,到底是在帮你还是在拖你后腿。
2. 切片到底在切什么:核心概念与设计思路拆解
2.1 切片的本质是语义单元的边界识别
很多人把切片理解成“把长文本按字数切开”,这个理解从根上就偏了。切片的真正目标,是找到文档中那些天然的语义边界,让每一个文本块都尽可能是一个完整的意思单元。
打个比方,切片就像切蛋糕。你不能随便拿刀在蛋糕上划一道就完事,得顺着奶油花纹、水果分界线去切,每一块拿起来都是完整的。文本也一样,一个段落、一个小节、一段对话,这些才是天然的切割线。按字数硬切,等于闭着眼睛下刀,切出来的东西看着是块,实际上语义是散的。
为什么语义完整性这么重要?因为后续的 embedding 模型是把整个文本块编码成一个向量。如果这个块里混了半句上一个话题、半句下一个话题,那这个向量就变成了一个“四不像”,既不像上一个话题,也不像下一个话题。检索的时候,用户问 A 话题,这个块因为沾了点 A 的边被召回,但里面一半内容是 B,生成模型拿到这种上下文,要么答偏,要么直接胡编。
2.2 切片粒度决定了检索的精度与召回
切片长度这个参数,是整套方案里最需要反复权衡的。切得太短,每个块信息量不够,检索时可能召回一堆碎片,拼起来还是缺胳膊少腿;切得太长,一个块里塞了好几个主题,向量被稀释,检索精度下降,而且生成时上下文窗口也吃不消。
这里有个经验性的平衡点。对于大多数通用场景,单个切片控制在 200 到 500 个 token 之间是比较稳妥的。这个区间的好处是:足够容纳一个完整的段落或几个相关句子,语义密度够高;又不至于长到混入多个主题。当然这不是铁律,具体要看你的文档类型和查询特点。
如果用户的查询普遍很短、很具体,比如“XX 功能的默认端口是多少”,那切片可以偏短,200 到 300 token,让每个块聚焦一个具体事实。如果查询偏宏观,比如“这套系统的整体架构是怎样的”,那切片可以偏长,500 到 800 token,保留更多上下文。
2.3 重叠窗口:用冗余换连贯
切片之间要不要重叠,这个问题争议一直很大。我的观点很明确:绝大多数场景下,需要重叠。原因很简单,语义边界不是总能切得那么准。你以为切在了段落之间,但可能这个段落的最后一句和下一段的第一句是强关联的。没有重叠,这个关联就断了。
重叠窗口一般设置为切片长度的 10% 到 20%。比如切片 400 token,重叠 40 到 80 token。这个冗余看起来浪费存储,但换来的是检索时的连贯性。用户问一个问题,可能答案横跨两个切片,有重叠的话,至少有一个切片能覆盖到完整的答案。
不过重叠也不是越多越好。重叠太多,相邻切片高度相似,检索时会召回一堆几乎一样的块,浪费上下文窗口,还可能让生成模型重复输出。我一般建议从 15% 起步,根据实际效果微调。
2.4 结构信息:别把标题和表格切没了
很多切片方案只盯着正文,把文档的结构信息全丢了。标题、小标题、表格、列表,这些在切片时如果被粗暴对待,损失的信息量远超你的想象。
举个例子,一个技术文档里有一节叫“数据库连接配置”,下面列了几个参数。如果你按字数硬切,可能标题在一个块里,参数在另一个块里。检索时用户问“数据库连接怎么配”,召回的块里只有参数没有标题,生成模型不知道这些参数是干嘛的。正确的做法是,切片时把标题和它下面的内容绑在一起,或者至少在切片内容前面加上所属的标题路径。
表格更麻烦。表格的行列结构在纯文本里本来就脆弱,硬切很容易把一张表切成两半,语义完全错乱。对表格的处理,要么整表保留为一个切片(如果不太长),要么按行切但每行都带上表头。
3. 不同文档类型的切片策略与实操要点
3.1 纯文本与 Markdown:按标题层级递归切分
纯文本和 Markdown 是最常见的文档格式,也是最好处理的。核心思路是利用文档自身的结构标记来做递归切分。
Markdown 的标题层级天然就是语义边界。一级标题下面是一级标题的内容,二级标题下面是二级标题的内容。递归切分的逻辑是:先按一级标题切,如果某个一级标题下的内容还是太长,再按二级标题切,以此类推,直到每个块的长度落在目标区间内。
具体操作上,我一般用这样的流程:先解析出文档的标题树,然后从根节点开始遍历,把每个叶子节点的内容作为一个候选切片。如果某个叶子节点内容超长,再按段落切;如果段落还超长,再按句子切。这样切出来的块,天然带有层级结构信息。
# 伪代码示意:基于标题层级的递归切分 def split_by_headers(text, max_tokens=500): sections = parse_markdown_headers(text) # 解析出标题树 chunks = [] for section in sections: if count_tokens(section.content) <= max_tokens: chunks.append(section) else: # 超长则按段落继续切 paragraphs = section.content.split('\n\n') chunks.extend(split_paragraphs(paragraphs, max_tokens)) return chunks这里有个细节要注意:切出来的每个块,最好在开头带上它的标题路径。比如一个块属于“第三章 > 3.2 节 > 3.2.1 小节”,那这个块的内容前面就加上“第三章 3.2 节 3.2.1 小节”作为前缀。这样即使块被单独召回,生成模型也能知道它在文档中的位置和上下文。
3.2 PDF 与扫描件:先解决解析质量再谈切片
PDF 是切片的重灾区。很多 PDF 的文本层是乱的,双栏排版被解析成单栏交错,表格变成一堆散落的数字,页眉页脚混在正文里。这种情况下,你切片策略再精妙也没用,因为输入本身就是垃圾。
我的经验是,处理 PDF 的第一步永远是解析质量检查。随便抽几页,看看解析出来的文本是不是可读的、顺序是不是对的。如果解析质量差,优先换解析工具,或者对特定页面做人工校正。常见的解析问题包括:双栏文档左右栏交错、表格内容错位、公式变成乱码、页眉页脚重复出现。
解析过关之后,PDF 的切片策略和纯文本类似,但要多做一步:去掉页眉页脚和页码。这些内容在每个切片里重复出现,会严重干扰检索。可以用规则过滤,比如连续出现在多页同一位置的短文本,大概率是页眉页脚。
对于扫描件,得先做 OCR。OCR 的质量直接决定后续一切。如果 OCR 错误率高,建议对关键文档做人工校对,或者至少对检索效果差的片段做针对性修正。
3.3 表格与结构化数据:整表保留或按行切分带表头
表格的切片策略取决于表格的大小和用途。小表格,比如十几行以内的,直接整表作为一个切片,前面加上表格的标题和说明。这样检索时,用户问表格里的某个数据,召回的块包含完整表格,生成模型能准确找到对应行列。
大表格,比如几百行的数据表,整表保留会超出长度限制。这时候按行切分,但每一行都要带上表头。比如一张“产品参数表”,表头是“型号、尺寸、重量、价格”,那每一行切片就是“型号:X,尺寸:Y,重量:Z,价格:W”。这样每个切片都是自解释的,检索时不会丢失列的含义。
# 表格按行切分并保留表头 def split_table(table, headers): chunks = [] for row in table.rows: row_text = ",".join([f"{h}:{v}" for h, v in zip(headers, row)]) chunks.append(row_text) return chunks这里有个坑:表格里的合并单元格。解析时合并单元格的内容可能只出现在第一行,后续行是空的。切片时要做好填充,把合并单元格的值补到每一行,否则检索时会丢失信息。
3.4 代码与技术文档:按函数或逻辑块切分
代码文档的切片不能按字数来。一个函数、一个类、一个配置块,这些才是天然的切片单元。按字数硬切代码,很可能把一个函数切成两半,检索时召回半个函数,生成模型补全的代码大概率是错的。
我的做法是,对代码文件按语法结构切分。Python 按函数和类切,JavaScript 按函数和对象切,配置文件按顶层键切。每个切片包含完整的函数或配置块,前面加上文件路径和函数名作为上下文。
技术文档里的代码示例,要和它周围的解释文字绑在一起。用户问“这个 API 怎么调用”,召回的块应该包含 API 说明和示例代码,而不是只有代码或只有说明。
4. 切片参数的计算与实操流程
4.1 切片长度的计算方法
切片长度用 token 数来衡量,不是字符数。因为 embedding 模型和生成模型都是按 token 处理的,字符数和 token 数的比例在不同语言、不同内容类型下差异很大。中文里一个汉字大约 1 到 2 个 token,英文一个单词大约 1 到 1.5 个 token,代码里的符号 token 密度更高。
计算切片长度,我一般用对应模型的分词器来数。比如用某个 embedding 模型,就用它的 tokenizer 来统计。实操中,可以先设定一个目标 token 数,比如 400,然后写个脚本统计每个切片的 token 数,看看分布是否合理。如果大部分切片在 300 到 500 之间,少数超长的做二次切分,那就比较健康。
这里有个经验公式可以参考:目标切片长度 = 生成模型上下文窗口 × 0.3 到 0.5 ÷ 预期召回切片数。比如生成模型上下文 8K token,你预期每次召回 5 个切片,那每个切片控制在 500 到 800 token 比较合适。留出空间给系统提示词、用户问题和生成内容。
4.2 重叠窗口的设置与验证
重叠窗口的设置,我一般从切片长度的 15% 开始试。比如切片 400 token,重叠 60 token。然后做检索测试,看召回结果里有没有因为边界问题导致的语义断裂。
验证方法是:构造一批查询,每个查询的答案明确横跨两个相邻切片。看看检索时,这两个切片是否都被召回,或者至少有一个切片包含了完整答案。如果经常出现答案被切断的情况,就加大重叠比例。
但重叠也不是万能的。如果文档本身结构清晰,段落之间边界明确,重叠可以少一点甚至不要。如果文档是连续叙述、段落之间关联紧密,重叠就要多一些。我见过一些团队无脑设 50% 重叠,结果检索时召回一堆重复内容,反而降低了有效信息密度。
4.3 完整切片流程的实操记录
我拿一个实际项目举例。文档是一份产品技术手册,Markdown 格式,大约 200 页,包含概述、架构、配置、API、故障排查几个大章节。
第一步,解析 Markdown 标题树,得到三级标题结构。第二步,按三级标题切分,统计每个切片的 token 数。发现“API”章节下的某些接口说明特别长,单个切片超过 1000 token。第三步,对这些超长切片按段落二次切分,目标 400 token,重叠 60 token。第四步,给每个切片加上标题路径前缀,比如“API 参考 > 用户接口 > 创建用户”。第五步,过滤掉页眉页脚和空内容。第六步,用 embedding 模型的分词器统计最终切片长度分布,确认大部分在 300 到 500 之间。
整个过程跑下来,200 页文档切出大约 800 个切片。检索测试时,用 50 个典型查询验证,召回准确率比之前的固定字数切片方案提升了将近 30%。提升主要来自两点:一是语义完整性好了,二是标题路径提供了额外上下文。
5. 切片效果的问题排查与优化技巧
5.1 检索结果不相关的排查思路
检索结果不相关,很多人第一反应是 embedding 模型不行。但在换模型之前,先检查切片。我总结了一个排查顺序:先看召回的切片内容本身是否语义完整,再看切片长度是否合理,再看是否有重复或高度相似的切片,最后才考虑模型问题。
如果召回的切片内容读起来就是断断续续的,那问题在切片。如果切片内容完整但就是不相关,那可能是切片太长导致向量稀释,或者切片太短导致信息不足。如果召回了一堆几乎一样的切片,那是重叠设置过大或者文档本身有大量重复内容。
5.2 常见切片问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方向 |
|---|---|---|---|
| 召回内容语义断裂 | 切片边界切在了句子中间 | 检查切片是否在段落或句子边界切分 | 改用语义切分,按段落或标题切 |
| 召回内容不相关 | 切片过长,向量被稀释 | 统计切片长度分布 | 缩短切片长度,或做二次切分 |
| 召回大量重复内容 | 重叠窗口过大 | 检查相邻切片相似度 | 降低重叠比例,或去重 |
| 答案横跨多个切片但只召回一个 | 切片间关联丢失 | 检查答案是否被切断 | 增加重叠,或把关联内容绑在一起 |
| 表格数据检索错乱 | 表格被硬切 | 检查表格切片是否保留表头 | 整表保留或按行切分带表头 |
| 代码检索不完整 | 代码按字数切分 | 检查函数是否被切断 | 按语法结构切分代码 |
5.3 独家避坑经验
第一个坑:别在切片阶段做摘要。有些人为了压缩长度,把每个切片用模型摘要一遍再存。这看起来省了 token,实际上丢了大量细节。检索时用户问的是具体细节,摘要里根本没有,召回的就是个大概,生成模型只能编。切片就是切片,别加戏。
第二个坑:切片元数据要存全。每个切片除了文本内容,至少要存:来源文档 ID、标题路径、在文档中的位置、切片序号。这些元数据在检索后处理、结果去重、引用标注时都用得上。我见过一些项目切片只存文本,后面想加引用来源都加不了,只能重跑。
第三个坑:别忽略小文档。有些文档很短,比如一页的说明,切片时可能就一个块。这种块如果直接存,检索时可能因为太短而匹配不到。我的做法是,对超短文档,要么合并相邻文档,要么在切片时补充上下文说明,让它的语义密度够高。
第四个坑:切片不是一次性的。文档更新了,切片要跟着更新。我建议把切片流程做成可重复执行的管道,文档一变就重新切,而不是手动改。手动改迟早会乱。
6. 切片策略的进阶思路与扩展方向
6.1 基于语义相似度的动态切分
固定长度切分是基础版,进阶版是基于语义相似度的动态切分。思路是:先按句子切分,然后计算相邻句子的 embedding 相似度,在相似度骤降的地方切一刀。这样切出来的块,内部语义高度一致,块与块之间边界清晰。
这个方法的效果通常比固定长度好,但计算成本高,因为要对每个句子做 embedding。适合对检索质量要求极高的场景,或者文档量不大的情况。实操中,可以先用固定长度粗切,再在粗切块内部做语义边界检测,平衡效果和成本。
6.2 父子切片与多级检索
父子切片是解决“切片长度两难”的一个巧妙方案。具体做法是:存两种粒度的切片,小的用于检索,大的用于生成。检索时用小切片匹配查询,命中后返回它所属的大切片给生成模型。这样既保证了检索精度,又保证了生成时的上下文完整。
比如,一个 1000 token 的大块,切成 5 个 200 token 的小块。检索时用小块匹配,命中后返回大块。用户问一个细节问题,小块能精准匹配;生成时大块提供了完整上下文,答案更连贯。
6.3 切片质量的评估方法
切片质量怎么评估?我一般用两个指标:检索命中率和生成准确率。检索命中率是构造一批查询,看正确答案所在的切片是否被召回。生成准确率是看最终回答是否正确。如果检索命中率高但生成准确率低,问题可能在生成环节;如果检索命中率低,那大概率是切片或 embedding 的问题。
还可以做切片粒度的消融实验。同一份文档,用不同的切片长度和重叠比例各跑一遍,对比检索效果。虽然费时间,但能帮你找到最适合当前文档和查询特点的参数组合。
切片这件事,说到底是个细活。它不需要多高深的技术,但需要你对文档内容有理解,对检索需求有判断,对参数有耐心去调。我自己的体会是,每次觉得 RAG 效果到瓶颈了,回头看看切片,总能找到优化空间。这个最不起眼的环节,往往就是决定上限的那一步。