做RAG项目也有一阵子了,我觉得文本分片(Chunking)是整个链路里最容易被低估、也最值得花时间琢磨的环节。不少朋友刚开始接触RAG时,注意力全放在Embedding模型选型和向量库调参上,结果检索效果不理想,翻来覆去改的是prompt,最后才发现问题往往出在“喂进去的文本是怎么切开的”这件事上。我甚至遇到过一种情况:只在分片策略上做了调整,没动任何模型和参数,检索命中率就提升了近一倍。今天就把我对文本分片的理解,从原理到实操一次讲透。
这篇内容主要面向两类人:一是准备搭建RAG应用、但还没想清楚文本该如何处理的开发者;二是已经在跑RAG系统、检索质量却总差一口气的工程师。我会把分片的“为什么”和“怎么做”都讲清楚,包括不同分片策略的原理和适用场景、块大小怎么定、工程落地时会踩哪些坑,以及如何设计实验来衡量分片质量。无论你用的是长文本问答、知识库问答还是文档增强生成,这篇都能给你一些可以直接照做的思路。
1. 为什么分片策略比Embedding模型更难选
1.1 先搞清楚一个核心逻辑:分片决定了语义检索的基本单元
很多人以为RAG检索是先有一个“用户问题”,然后去文档库里找相似的段落,最后拼给大模型生成答案。这个理解方向没错,但忽略了一个关键细节:你的向量库里存的不是“整篇文档”,而是“切碎后的一个个文本块”。检索系统拿用户问题去匹配的,也不是文档本身,而是这些块。
也就是说,分片策略其实决定了系统“看世界”的基本单元。如果你的块切得太大、太粗,一个块里塞进了好几个不相关的主题,那么当你问其中一个小问题时,这个块的向量表示会被其他无关内容的语义“稀释”。打个比方,一个块讲了三件事,Ask模型算出它的向量时,会被三件事的平均语义带偏,检索时自然找不到精确答案。
反过来,切得太细、太碎,原本紧密关联的上下文被硬生生拆开,模型在看单个块时就像只看到一句话的前半句,缺乏足够语境,自然也难给出好回答。所以分片本质上是在解决一个矛盾:既要保证每个块内部尽量是一个完整、连贯的信息单元,又要保证块与块之间的边界恰好落在语义的“天然断点”上。
1.2 向量化的特性决定了“块内上下文”的重要性
Embedding模型不是逐字逐句独立编码的。以主流Transformer结构为例,一个Token在生成向量时,会通过自注意力机制参考上下文窗口内其他Token的信息。这意味着一个块的长度、块内内容的连贯性,会直接影响这个块最终被编码成什么样。
更直白一点说:向量化生成的Embedding,是“站在块内所有文字的语境下去理解其中每一句话”的结果。同样的“苹果”两个字,在一个说水果的段落里和在一个说手机品牌的段落里,向量表达完全不一样。分片的目的,就是让块内保持尽量单一的语境背景,这样才能发挥Embedding模型的真正能力。
还有一点容易被忽略:Embedding模型通常有最大输入长度限制。超过这个长度的文本,要么被截断,要么被池化压缩,语义信息都会打折。但我也不是建议大家为了“塞进去”就一直把块压小,因为你问LLM的问题往往也需要上下文支撑,块太小时上下文信息量不够。所以分片不是单纯的“字面切分”,而是要替Embedding模型创造“刚好合适”的阅读单元。
1.3 分片质量直接决定RAG系统的三项关键指标
我们可以把分片质量与最终系统的三项具体表现对应起来看:
- 检索命中率:如果答案分散在两个相邻块里,检索过程可能只捞到其中一个块,另一个相关块因为边界问题没被匹配上,命中率直接下降。
- 生成忠实度:即便找到了相关块,如果这个块本身信息残缺或包含过多噪音,大模型在生成时就会“脑补”缺失内容,出现幻觉或事实性错误。
- 召回效率:块越大、越碎,向量库中的条数越多,检索时的计算开销和延迟自然增加。一个合理的分片策略能让系统用更少的向量条数覆盖更多的有效语义。
我记得有一次做知识库问答,客户反馈说“问不到精确数字”。排查时发现文档里有一张表格被统一硬切成了若干固定长度块,同一行数据被切到了两个块里,检索时匹配到的是“标题栏+前几列”,数字信息丢了。后来把表格区域作为一个独立语义块处理,问题立刻缓解。这就是分片质量对实际效果影响最直观的体现。
2. 分片策略的核心框架:语义边界、块大小与overlap的博弈
2.1 语义边界:什么样的地方适合作为切开点
文本里天然存在一些“语义断点”,找对这些断点,分片就成功了一大半。常见的断点包括:
- 段落结束:段落通常是作者表达一个完整意思的基本单元。
- 章节标题前后:新章节意味着话题切换,非常适合切分。
- 列表项之间:每个列表项往往是一条独立信息。
- 换行密集处:很多时候,连续的换行意味着内容的主题或语境发生了变化。
不过要提醒一句,很多PDF转出来的文本、扫描件识别出的内容,段落并不是靠“空行”体现的,往往是连续的一长串文字,只有换行符而无空行。这种情况下,对段落边界的判断就不能只依赖“空行”这一个信号,而要靠标点符号、句子长度、主题连贯度等因素综合判断。
2.2 块大小(Chunk Size):不是拍脑袋出来的
块大小通常用“Token数”来标识,因为向量模型和LLM都以Token为基本单位。我见过不少朋友直接用固定字符数,比如“每500个字切一块”,这其实是把“字”和“Token”混为一谈了。同样500个字,英文可能对应600到700个Token,中文可能是700到800个Token,这直接影响了向量模型是否能完整编码。
块大小没有一个万能值,它取决于四个因素:
- Embedding模型的输入上限:比如某模型支持512或1024 Token,块大小不能超过这个上限。
- 内容本身的重复度与复杂度:如果文档专业术语密集、逻辑环环相扣,建议稍大一些;如果是常见生活化内容,稍小也能接受。
- 下游生成模型的上下文窗口:最终拼接给生成模型的内容越多,越要考虑总成本,块太大会导致一次请求的Token数量失控。
- 检索测试的实际反馈:这一点最重要。块大小应该由实验数据来定,而不是拍脑袋。你先设一个初始值,跑一批问题集,看命中率和答案质量的变化。
2.3 Overlap(重叠)的作用与代价
重叠的意思是,相邻两个块之间会保留一部分共用的文本。比如块1覆盖文本1到400,块2覆盖文本351到750,那351到400这一段就被两个块共同承保了。表面上看,重叠浪费了一些存储空间,但它能解决一个非常实际的问题:避免一个完整的信息单元(比如一句承上启下的关键句)恰好落在块的边界上,导致检索时它的后半段“跑”到上一个块里去了。
重叠的另一个好处是:即使查询点落在两个块的边界附近,系统也能在至少一个块内看到完整的语境。缺点也很明显:
- 存储成本增加,向量条数不变但每个块的Token量变大。
- 检索时可能同时命中多个高度相似的块,导致去重和排序变复杂。
- 生成阶段如果多个块都包含同一段文本,模型看到的上下文会有重复,浪费Token。
我的习惯是,重叠部分一般控制在块大小的10%到20%之间。如果块大小是400 Token,重叠可以设为40到80 Token,再大往往收益降低。实际项目中可以调几次对比效果,不要有洁癖。
3. 六类常见分片策略的横向对比与选型思路
3.1 固定长度分片:最简单的方案,也是最容易翻车的方案
固定长度分片就是按固定Token数或字符数硬切,完全不考虑文本结构。这种方案实现成本最低,适合在快速原型阶段跑通流程时用。但如果文档结构复杂,固定长度经常会把一句话、一个公式、一个表格拦腰砍断,导致检索质量很不稳定。我做原型验证时用过这种方案,结果发现只要切到代码片段、公式或中英文混合内容,问题就特别多,检索到的块经常是“半句话”。所以这个方案只适合当作基线,不能当主力。
3.2 递归字符分片:目前最实用的通用方案
递归字符分片是目前工程上最常用的通用方案。它的核心思想是:按一组分隔符依次尝试切分,如果当前切分结果仍然超过目标块大小,就换用更小的分隔符继续切。
比如一套典型的分隔符序列是“\n\n\n”、“\n\n”、“\n”、“。”、“\n”。意思是,先按多个空行切大段落;如果大段落还是太长,再按单空行、按句子标点、按换行符逐级下探。这样做的好处是:文本尽量保持自然段落结构,每个块内部的语义完整性明显好于固定长度切分。
这一策略几乎是目前做通用RAG的首选。选分隔符的顺序也有讲究,通常把“段落级”的分隔符放在前面,把“句子级”的分隔符放在后面,这样切出来的块首先会尊重原文的结构层次。
3.3 Sentence Level 分片:适合需要精确引用的场景
按句子切分的方案比固定长度聪明不少,它能把每个句子作为独立单元,特别适合法律条款、合同条款这类句子意思高度独立的文档。这类文档里一句话就是一个完整条款,检索时只需要把相关条款捞出来即可,过多上下文反而会干扰。
不过纯按句子切分也有明显缺陷:文档里很多信息是一几句话加几个解释句组合在一起的,单独抽出一句可能丢失必要的限定条件。所以完整方案一般不是“只按句号切”,而是按“句子聚类”或者“滑动窗口句组”来切,比如一次取两到三个连续句子构成块,必要时让相邻句组有重叠,兼顾语义完整性和定位精度。
3.4 语义分片(Semantic Chunking):算法更先进,但要谨慎使用
语义分片不依靠原文的显式结构,而是先计算相邻句子或段落之间的语义距离,再寻找“语义突变点”作为块的边界。实现上通常先把文本拆成句子列表,用Embedding模型给每个句子编码,然后计算相邻句子向量的余弦距离。距离比较大的地方,意味着前后两句语义差异明显,这就是可能的边界。
这种方案的优点是块边界贴近“语义聚落”,理论上效果最好。但实际工程中它有三个隐患:
- 语义距离阈值不好定,不同的文档类型、不同领域,阈值往往不一样。
- 计算成本高:需要额外调用Embedding模型处理每一个候选句子,比递归字符分片贵。
- 不可复现性:有些实现依赖于局部距离的浮动,稍微调整输入就可能改变边界。
我的建议是:如果文档内容高度结构化、且原始结构标记很清晰,语义分片不如直接用结构分片;如果文档是小说、混排内容这类无明显结构的文本,语义分片可以考虑,但也一定要先做成本测算。
3.5 面向结构的感知分片:充分利用文档自带的标记
很多文档本身自带结构信息:Markdown的标题层级、HTML的标签、LaTeX的章节命令、代码的缩进层级、PPT的页面边界等。面向结构的分片就是把这类标记当作天然切分点。以Markdown为例,可以先按一级标题分,再按二级标题分,如此类推,直到每个块的Token量控制在目标范围内。这种方案的核心优势是:语义边界几乎不需要猜测,标题本身就是作者给的“答案”,检索时还能用标题作为块的上下文提示。
具体实现时,可以把标题信息存成块的元数据。比如一个块的正文来自某个二级标题下的段落,我们就把“二级标题文本”和“一级标题文本”一起作为元数据存入向量库。检索时,即便命中的块本身很短,系统也可以带着标题节点去参与匹配,这种“上下文提示”能明显改善检索效果。
3.6 基于模型的语义拆分:边际收益和成本问题
还有一种思路是用短文本摘要模型或语言模型来判断文本边界,甚至让模型输出理想的切分方案。效果确实不错,但成本也最高,因为需要对整篇文档的每个候选切分点都推理一次。除非你的场景特别依赖高质量检索、数据量又可控,否则我不建议在通用场景里直接用模型分片。一块文档只有几十KB,算下来拆分成本可能比后续检索成本还高,性价比很低。
下面把这六类策略放在一起,直观对比:
| 分片策略 | 切分依据 | 优点 | 缺点 | 典型场景 |
|---|---|---|---|---|
| 固定长度 | Token/字符数 | 实现简单、速度快 | 破坏语义、切割代码和表格易出错 | 快速原型、基线对比 |
| 递归字符 | 多层分隔符 | 通用性强、兼顾结构 | 对无结构内容不敏感 | 各类通用文本,首选方案 |
| 句子级 | 句号/语气词 | 检索精度高 | 信息碎片化 | 法律条款、FAQ |
| 语义分片 | 相邻Embedding距离 | 理论上最优 | 成本高、阈值难调 | 无明确结构的混合文本 |
| 结构感知 | 标题/标签/代码缩进 | 语义边界准、可带元数据 | 依赖文档结构完整 | Markdown、HTML、代码文档 |
| 模型拆分 | LLM/摘要模型 | 效果强 | 成本高、延迟高 | 小数据量精品知识库 |
4. Chunk Size与Overlap的调优实验方法
4.1 设计一套可控的对比实验
分片调参最忌讳凭感觉。正确做法是:先准备一个有标准答案的问题集,然后在同一套向量库、同一个Embedding模型、同一个检索参数下,只改变分片策略和块大小,观察指标变化。
问题集要贴近真实使用场景,建议不少于50条。每道问题对应一个或多个期望命中的文档块,这个“标准答案”可以用人工标注来建立。如果你没有人工标注预算,至少也要记录“期望包含答案的文档片段”,然后判断检索结果是否覆盖。
用到的指标主要有三个:
- 命中率(Hit Rate):检索返回的前K个结果中,有多少条命中了包含标准答案的块。这个指标最直观,也最容易理解。
- MRR(Mean Reciprocal Rank):考虑命中结果在排序中的位置。如果正确答案排第一,记1分;排第二,记0.5分。它能衡量检索结果排序质量。
- ** faithfulness(忠实度/答案一致性)**:最终生成答案与标准答案的一致性,此项由人工或模型打分,用来兜底前两个指标无法体现的“内容可用性”。
大致流程可以这样组织:
- 确定多个候选方案,例如递归字符分片,块大小取300/500/800 Token,重叠取50/80/100 Token,组成若干组。
- 依次为每组方案构建向量库索引,注意保持文档集、向量模型、检索的TopK完全一致。
- 跑同一批问题,分别记录上述三项指标。
- 排序对比,选综合表现最好的方案。
4.2 一个可参考的Python验证代码思路
下面是一段伪代码,展示了如何快速测算“命中率”和“MRR”,你可以按需改造成本地环境可跑的脚本:
import numpy as np # chunk_list: 每个分片文档入库时记录的唯一ID或文本 # expected_chunk_ids: 每个query期望命中的chunk ID列表 # retrieved_chunk_ids: 每个query实际检索返回的TopK chunk ID列表 def evaluate(retrieved: list[list[str]], expected: list[list[str]], k=5): hit_count = 0 mrr_sum = 0.0 for ret_list, exp_list in zip(retrieved, expected): exp_set = set(exp_list) # 只看前k个结果 ret_k = ret_list[:k] hit_flag = False for rank, cid in enumerate(ret_k, start=1): if cid in exp_set: hit_flag = True mrr_sum += 1.0 / rank break if hit_flag: hit_count += 1 n = len(retrieved) hit_rate = hit_count / n mrr = mrr_sum / n return {"hit_rate": hit_rate, "mrr": mrr}注意,这段代码里的retrieved要确保来源是同一个向量库在不同分片参数下的检索结果,而不是不同库之间的混跑结果。
4.3 常见实验结论和我的经验参考
从我在多个文档集上的实验经验看,有几个规律可以作为起点:
- 块大小在500到800 Token之间,命中率普遍高于300以下的小块,原因是块内信息量够用。
- 块超过1000 Token后,命中率往往下滑,因为语义开始被杂质干扰,向量表达不够聚焦。
- 重叠设置对命中率的影响通常小于块大小,但会影响排序质量,MRR对重叠更敏感。
- 如果你的文档本身就是问答对,用句子级分片比用递归字符分片效果好很多。
这些规律不是金科玉律,但作为第一轮实验的候选参数非常好用。跑完第一轮后,你再围绕表现最好的一组参数做细粒度调整,比如从500 Token起步,分别试450、500、550、600,找到峰值的拐点。
4.4 别只看检索指标,务必看生成质量
我提醒过很多同行:Hit Rate上去,不代表生成答案质量就同步上去。有个典型场景是块大小恰到好处,检索命中的块标题、摘要都匹配,但块里包含正确答案的句子太靠后,实际生成模型读到时已经信息衰减,导致回答不够准确。
所以调参一定要“检索指标+生成答案人工抽检”双轨并行。每次调完参数后,固定抽10到20道测试题,把检索到的块和生成答案打印出来人工看一遍。这个过程很花时间,但非常值得。曾经有一位同事调整分片后看到Hit Rate提升了0.15,但人工检查发现生成答案出现了更明显的重复引用现象,差点上线后才发现,我建议他一定要先跑人工抽检,后来果然救了一版。
5. 工程落地中容易踩的坑与我的应对思路
5.1 元数据必须和分片绑定,否则知识库就是“黑盒”
分片之后,每个块如果只保存“原文文本”和“向量”,那么当生成模型引用某个块的信息时,你甚至无法追溯它来自哪份文档、哪个章节。工程上一定要给每个块附带元数据,常见的元数据包括:
- 文档ID和文件名
- 章节标题路径(如一级标题/二级标题)
- 块在原始文档中的起始位置和结束位置
- 文档来源链接或页码
这些元数据可以被检索系统用来构造更丰富的返回结果,也可以在生成阶段做引用溯源。我习惯把“标题路径”作为向量检索的前缀拼入文本,比如“税法总则 -> 纳税人权利 -> 第12条”,这样即使块内只提到“该条”,模型也能从上下文中理解确切指向。
5.2 不同文档类型混用时,不要强行统一分片参数
一个常见错误是,一个知识库里既包含合同条款、产品手册、技术支持FAQ,还包含长篇文章笔记,却用同一组分片参数跑全库。这会导致某些文档检索效果差得离谱。
其实合理的做法是:按文档类型设定不同分片策略和参数,并在元数据里标注“分片策略类型”。检索时让向量库支持按元数据过滤,再结合业务场景选择性地检索某一类文档。比如FAQ类内容直接用句子级分片,产品手册用结构感知分片,长篇文章笔记用递归字符分片,效果会好很多。你可以先按文档类型分别做实验,再决定是否统一参数。
5.3 Overlap并不总是越多越好
前面我给出了10%到20%的建议范围,但这里要补充一个反例:如果你的文档是层层递进的(比如数学证明、教程代码),重叠设多了,检索到的多个块之间会频繁包含同一段解释文字,导致生成模型在拼接时出现前后矛盾。因为模型看到两段话都在讲“上一步的结论”,会误以为需要强调或者重复,最终答案出现冗余信息。
所以重叠多少,既要看块大小,也要看文档的叙事节奏。我在处理连续性很强的技术文档时,会故意把重叠压得很低甚至设为零,换取检索结果之间的“信息差”,让每个块尽量呈现新材料。
5.4 处理中英文混合内容时的Token换算问题
中文和英文的单位Token差异极大。英文大约4个字符对应1个Token,中文大约1个字对应0.6到1.5个Token,具体取决于模型和分词方式。如果你按字符设置块大小,很容易超出模型上限。
我建议所有分片实现都用Token级参数,并且在代码里接入模型自带的Token计数函数。另外,混合内容最好单独处理。比如一篇介绍API的文档,英文代码注释多,中文说明多,就不能用同一个字符数去切,通常我会把代码块和文字段落分开,分块后分别管理。
5.5 查询端的分片策略也要同步思考
很多人只关注“入库端”的分片,却忘了检索时查询语句同样会被Embedding模型编码。如果查询很长很乱,模型对查询的理解也会被噪音左右。目前的实践对查询端的处理相对成熟:入库时怎么切,查询时尽量保持相似的粒度和风格。比如入库时每个块是段落级的,那么查询最好不要是超长的整篇文章,否则检索效果会受影响。
如果业务上确实有长查询需求,可以尝试“查询重写”:先用一个轻量模型把长查询压缩成关键词列表或数条短查询,再用短查询分别去检索,最后合并去重。这个方法在长段落匹配场景里很有效。
5.6 边界情况的处理:表格、公式、代码块
这三个数据类型可以说是分片界的“老大难”。
- 表格:表格行之间通常有紧密的对应关系,单独切几行会导致数据断裂。我的方法是把整个表格作为一个块,过长的表格再按“行组”切,并保留表头信息重写到每个行组块里。
- 公式:公式内部的语义不依赖上下文,但公式说明文字依赖。建议把“公式块”和“说明文字块”分开存,并在说明文字块的元数据里引用公式块ID。
- 代码块:不要按行数直接切代码,否则一个函数容易断在中间。按照函数、类、导入区块等代码结构来切,或至少按空行分,效果远好于固定长度切分。如果代码很长,可以保留“所在函数名/类名”作为元数据前缀。
5.7 长期维护视角:分片逻辑和版本管理
知识库不是一次性建好的,文档会更新,分片逻辑也会调整。我踩过的坑是:更新了一部分文档后,新旧两种分片方案混在一个库里,检索结果非常混乱。
所以现在我的做法是:每次调整分片逻辑,都重建新版索引,并且保留版本号。线上检索时,可以灰度切流,让一部分请求走新索引、一部分走旧索引,收集真实用户反馈后再全面切换。伪代码上,只需要在元数据里增加一个“分片版本号”字段,检索时按该字段过滤就行。
6. 最后再分享两个我在分片调优中学会的实操技巧
第一个技巧:不要忽视“标题 + 首句 + 尾句”的组合作用。很多情况下,一个块即使内容丰富,检索匹配时也只依赖其中一两句话的语义;如果这一两句话被埋没在大量中间内容里,命中率就会下降。你在做一个分片方案时,可以试着在向量化之前给每个块创建一个“摘要前缀”,把块的标题、关键要点压缩成一小段文本放到块的前面,再一起向量化。这相当于给每个检索单元加了一个“快照索引”,能显著降低信息被淹没的概率。
第二个技巧:先把“任务分解”这件事做好,再用分片解决“检索单元”。RAG应用中,能不能检索到正确答案,通常不只看分片,也看问题拆得好不好。我的做法是,把用户问题拆成几个子问题,每个子问题对应检索一类文档,最后把结果合并。这样即使某些块检索不到位,整体答案也不会断链。分片和查询设计是一对拍档,单独调分片而不优化查询,并不会让你达到最优效果。
回头看我做过的几个RAG项目,凡是提前在分片策略上花过心思的,后来的调试成本都低很多。分片看起来是链路里最“笨”的一环,却承载着整个系统的记忆精度。希望这次分享能让你在自己的项目里少走几步弯路。