做饭的人都知道,备菜比下锅更花心思。一整只羊不能直接扔进锅里——锅装不下,火候不均,端上桌也没法分。主厨会把它拆成羊排、腿肉、里脊,再按菜谱切成块、片、丝,各归其位。大语言模型的 RAG(检索增强生成)知识库问答,也卡在同样一道“备菜”环节上:文本分块。 用户把文档交给 AI 前,系统得先把长文切成许多小块存起来;用户提问时,再挑最相关的几块喂给模型。块切大了,AI 的“嘴”(上下文窗口)装不下;切小了,一句完整的话被腰斩,语义碎一地;切的位置不对,再聪明的模型也拼不回原意。 这篇文章带你看懂四种主流“刀法”:固定窗口用尺子量着切,滑动窗口带着重叠切,递归字符顺着文本纹理切,语义分块则按“意思”归堆。你会明白它们各有什么脾气、适合什么样的“食材”,以及如何用实验数据找到最佳参数。
文本分块是 RAG 工程的第一道工序,直接决定检索命中率与回答质量。掌握四种策略的原理与选型方法,是 AI 应用工程师的必备基本功,能帮你从“能跑通”进阶到“效果好”。
目录
可视化演示
对比展示RAG四种分块策略:固定窗口(等长切分)、滑动窗口(重叠区域)、递归字符(分隔符优先级)、语义分块(Embedding相似度跳变检测)。
本节配有交互动效(进入视口后自动加载,约 3KB)。
'+captions.fixed+'
分块的初心——为什么不能把整本书喂给 AI
假如你要在家里办一场 20 人的聚会,冰箱里躺着一整只羊。你会怎么做?老练的主厨不会把整只羊塞进烤箱——烤不熟,也分不均。他会先拆解:羊排拿来烤,腿肉拿来炖,剩下的剁成肉馅做丸子。每道菜用到的那部分,恰好是它该有的分量。这个“拆解备菜”的动作,就是今天的主角——文本分块。
大语言模型虽然博学,但它的“工作记忆”(上下文窗口)有字数上限,不可能一次读完你上传的几百页 PDF。RAG(检索增强生成)的思路是:先把文档切成许多小块存入向量数据库,用户提问时,系统只检索最相关的几块拼给模型看。分块的好坏,直接决定“检索”这一步是百发百中还是大海捞针。
为什么不能随便切?第二个动机是检索粒度:块太粗,一块里混了五六个话题,检索时“相关”和“跑题”的内容粘在一起,稀释了相关性;块太细,一个完整观点被劈成碎片,检索永远只能捞到半句话。第三个动机是成本:大模型按 token 计费,块越大,每次问答塞进去的 token 越多,账单越贵。于是有了一个最基本的公式——文档总长 L 除以块大小 S,得到块数 N。
备菜的核心不是“切得整齐”,而是“切得合适”。理解了分块的初心,接下来我们看看市面上四种主流“刀法”,以及它们各自的脾气和适用场景。
- L——文档总长度通常用字符数或 token 数衡量,相当于整只羊的“肉量”。
- S——块大小(chunk size)每块的目标长度,决定“刀口间距”。
- ⌈·⌉——向上取整最后一块可能凑不满,但也要单独成块,就像最后几片肉也要装盘。
- N——总块数块数直接影响检索开销和存储成本,是后续所有成本估算的起点。
固定窗口分块——用尺子量着切
有些食堂大厨切豆腐,先拿尺子比好,一刀一刀,每块整整齐齐 5 厘米。固定窗口分块就是这个思路:不管文本内容是什么,一律按固定长度下刀。文档从左到右,数够 S 个字符就切一刀,然后接着数,再切一刀,像一台没有感情的切菜机。
技术上它是最朴素的实现:设块大小为 S,第 i 块就是文本区间 [i·S, (i+1)·S)。优点是极简单——一个循环就能写完;速度快,不用理解文本;结果可预测,块数固定,成本好估算。很多团队把它当作 RAG 工程的基线(Baseline),先跑通全链路再说。
最大缺点是切断语义。一句话可能从中间被劈开,前半句在上一块、后半句在下一块;检索时只找到后半句,模型读到的是一个没头没尾的残句。这就像一刀切在了豆腐花纹的正中间——花纹断了,卖相和口感都打了折扣。
适用场景:日志、固定格式的流水记录,或者对检索质量要求不高、只想快速验证的 Demo。它简单可靠,但别指望它“懂”文本。用尺子切豆腐方便省事,但你没法保证花纹完整——碰到花纹,只能叹气。
- block_i——第 i 块编号从 0 开始,第一块是 block_0。
- i·S——块起点从第 i×S 个字符开始取,上一块的终点就是下一块的起点。
- (i+1)·S——块终点到第 (i+1)×S 个字符为止,但终点本身不包含在内。
- [ )——左闭右开区间包含起点,不包含终点,避免边界字符被重复取到。
滑动窗口分块——刀锋带着重叠切
老练的肉贩切五花肉时,不会每次把刀完全挪开再下刀,而是让刀锋与上一刀保持一点重叠——确保肉皮上的纹理不会被接缝切断。滑动窗口分块的灵感正来源于此:固定块大小 S,但每刀只向前推进 stride 个字符,让相邻两块共享一段重叠区 O。
这样做的收益很明显:一个恰好被“骑”在边界上的知识点,会在下一块中以完整形态再出现一次,检索时无论命中哪一块,都能拿到完整信息。代价同样肉眼可见——冗余。同样的内容被存了两遍,向量库体积变大,Embedding 和存储成本上升;检索时还可能同时返回两块“看起来几乎一样”的结果。overlap 设得越大,冗余越严重。
适用场景:对话流、新闻正文等上下文高度连贯、没有自然章节边界的文本。合理的 overlap 一般取块大小的 10%~20%,既能防断裂,又不至于太浪费。刀锋重叠虽然会多切几刀,但保证了关键花纹不会断在两刀之间。
- S——块大小每个窗口要截取的长度,是“刀锋宽度”。
- O——重叠大小(overlap)前后两个窗口重复的字符数。
- stride——步长每切一刀,起点向后移动的距离。它等于 S 减 O,是滑动窗口最核心的旋钮。
递归字符分块——顺着纹理切
有经验的大厨分解一只鸡,绝不会抡起砍刀硬剁骨头,而是先找到关节,轻轻一拧就拆开了。递归字符分块就是文本世界的“关节大师”:它不按固定长度硬切,而是顺着文本的天然结构逐层拆解——先按段落分,段落太大再按句子分,句子还太大就按子句、按词分。
具体做法是准备一组按优先级排列的分隔符:段落符(两个换行)、换行符、句号、空格。切分函数 f(t) 这样工作:如果 t 的长度还没超过阈值 S,就直接收下;否则用当前最高优先级的分隔符把 t 劈开,然后对每一片子串递归调用 f。LangChain 的 RecursiveCharacterTextSplitter 就是这一思路的著名实现。
优势是尊重结构:段落、句子通常不会被人为切断,每个块内部的语义相对完整。缺陷是它依赖分隔符的“成色”:如果文档本身没有清晰分隔符——比如一行行滚动的服务器日志——它就退化成固定窗口,甚至因递归开销而更慢。
适用场景:Markdown 文档、技术博客、合同条款这类“天生有结构”的文本。顺着纹理下刀,肉不碎、语义也不散。
- f(t)——递归分块函数输入一段文本,输出一个块集合。
- |t| ≤ S——终止条件文本足够短,不再下刀,原样作为一块。
- t.split(s_0)——用最高优先级分隔符切分s_0 是当前可用的最高级分隔符,比如段落符。
- ⋃ f(t_i)——递归并集对每个子串再调用 f,最后把所有子块合并起来,形成最终结果。
语义分块——按菜品备料
顶级私房菜的主厨备菜,从来不是按重量称好就完事,而是按“这道菜要做什么”来归堆:做鱼的放一盆,做肉的放一盆,炖汤的药材放一袋。这种“按意思归类”的分块方式,就是基于 Embedding 的语义分块。
它的大致流程是:先把文本按句切碎,用 Embedding 模型把每个句子编码成一个向量(一串代表语义的数字);然后计算相邻两句向量的余弦相似度——相似度高,说明话题在延续;相似度低,说明话题发生转折,就在这个“低谷”处下刀。于是每一块内部的句子都围绕同一主题,而块与块之间井水不犯河水。
有了相似度序列后,设定一个阈值 θ,凡是相邻相似度低于 θ 的位置就是候选切分点。这种方法产出的块,内部语义高度内聚,检索质量通常是四种策略中最高的。代价也最贵:需要额外调用 Embedding 模型为每个句子推理一次,长文档处理速度慢、成本高,阈值还需要反复调。
适用场景:FAQ、论文、产品文档等“话题边界清晰”的高质量文本,以及检索质量直接决定业务体验的场景。慢工出细活,说的就是它。
- v_a、v_b——两个句子的 Embedding 向量把句子压缩成一串数字,数字分布代表语义。
- v_a·v_b——点积衡量两个向量在方向上的一致性,对应“话题走向是否相同”。
- ‖v_a‖‖v_b‖——模长的乘积把相似度归一化到 [-1,1],消除句子长度带来的偏差。
- cos 值——余弦相似度越接近 1 越相似,越接近 0 或负值越不相关。
参数调优——火候与刀工的平衡
刀工再好,火候不对菜也废。分块策略选得再好,参数设得离谱,效果照样翻车。参数调优是分块工程从“能用”到“好用”的那段路。
三个核心旋钮:chunk_size(块大小)决定单刀宽度,越大单块信息越全但噪声越多,越小纯度越高但上下文越容易碎;chunk_overlap(重叠大小)决定块与块的衔接,越大上下文越连贯但冗余越重;分隔符选择则决定递归和语义方法的边界质量。三者互相牵制,改一个往往要重新评估全局。
这个公式最直观的提醒是:overlap 从 10% 涨到 30%,块数会明显增加,存储与 Embedding 成本跟着上升。因此参数调优不能拍脑袋,要用真实的检索评测集反复实验,对比 Recall@k 等指标——好厨师靠试菜,好的分块参数靠实验数据说话。
经验上,多数知识库文档的块大小在 300~800 token 之间,overlap 在 10%~20%。但记住:一切以你的数据实测为准。好厨师从不迷信菜谱,而是反复试菜。
- L——文档总长还是那锅菜的“总量”。
- S——块大小、O——重叠大小两个最核心的旋钮。
- S - O——实际步长每块重复 O 个字符后,真正向前走 S-O 个字符。
- ⌈(L-S)/(S-O)⌉ + 1——总块数第一块从 0 开始,之后每走一个步长就多一块,最后不满一块也要兜底。
选型指南——什么菜配什么刀
中式厨房里,切肉的、切鱼的、切水果的刀通常是分开的——没有哪把刀能包打天下,分块策略也一样。关键在于看“食材”和“菜谱”。
三个决策维度。文档类型:结构化的 Markdown、合同、论文,优先递归字符分块,因为它们的天然边界明显;对话、新闻这类流式文本,用滑动窗口保持上下文;话题庞杂的 FAQ 或百科,语义分块的收益最大。业务需求:实时性要求高(比如在线文档实时问答),要避开慢速的语义分块;准确率要求高,则预算允许时优先语义分块。链路协同:下游 Embedding 模型的窗口长度、检索器的类型、大模型的上下文大小,都会反过来约束块大小。
实践上最稳健的路径是“先简单后复杂”:先用固定窗口跑通基线、把链路装好,再针对检索质量瓶颈,换成递归字符或语义分块,用 A/B 数据证明收益。不要一上来就上最贵的方案——你不知道钱花在了哪里。
备菜没有标准答案,但懂了每种刀法的脾气,你就能在灶台前从容不迫。
- Q——质量分该策略在评测集上的检索或问答质量。
- V——速度分处理单位文本的耗时倒数,越快得分越高。
- C——成本分Embedding、存储、API 调用带来的单位成本。
- w_1, w_2, w_3——权重按业务目标设置,比如“质量优先”时 w_1 取大。
AI 应用场景
智能客服知识库——语义分块提升问答精准度
阶段:数据准备与索引构建
解决的问题:传统固定窗口会把“退货政策”和“退款流程”混在同一个块里,用户问退款时效时,检索结果夹杂大量无关介绍,回答差之千里。语义分块按话题边界切分,让每个块聚焦单一主题。
具体案例:某电商平台把上千篇客服文档用语义分块重新切分,每块只包含一个知识点;用户问“退货多久到账”时,检索到的块直接命中退款时效的那一段,回答准确率提升约 20%,人工转接率明显下降。
代码库文档问答——递归字符分块保住结构
阶段:离线索引构建
解决的问题:Markdown 技术文档包含标题、代码块、列表等多层结构,固定窗口常切断函数说明与其示例代码,导致问答时“有代码没解释”。递归字符分块按标题与段落边界切分,结构完整性大幅提升。
具体案例:一个开发者工具团队用递归字符分块处理 GitHub Wiki 和 API 文档,优先按标题和段落边界切分,函数说明与示例代码始终在同一块内,开发者提问时检索命中率显著提升,文档问答机器人被更多团队采用。
客服工单流式问答——滑动窗口保持对话连续
阶段:流式数据接入与处理
解决的问题:客服聊天记录是连续文本,话题边界模糊,固定窗口会割裂同一用户诉求的上下文。滑动窗口的重叠机制确保一个完整诉求至少完整出现在一个块中。
具体案例:某 SaaS 公司对历史工单对话做滑动窗口分块,overlap 设为块大小的 20%;后续做工单聚类分析时,同一诉求的片段能同时被召回,聚类准确率提升,客服运营团队能更快发现高频问题。
Python 演示
# 文本分块策略演示import re def fixed_window(text, size=50): """按固定长度切分""" return [text[i:i + size] for i in range(0, len(text), size)] def sliding_window(text, size=50, overlap=10): """滑动窗口, 相邻块有重叠""" stride = size - overlap return [text[i:i + size] for i in range(0, len(text), stride)] def recursive_split(text, separators=("\n\n", "\n", "。", " "), max_size=50): """递归字符分块: 按优先级从段落切到词""" chunks = [] _recursive(text, separators, max_size, chunks) return chunks def _recursive(text, seps, max_size, chunks): """递归辅助函数""" if len(text) <= max_size: chunks.append(text) return for sep in seps: parts = text.split(sep) if len(parts) > 1: for p in parts: _recursive(p, seps, max_size, chunks) return chunks.append(text) def semantic_split(text, max_size=40): """简化版语义分块: 按句子聚合到接近块大小""" sentences = re.split(r"(?<=[。!?])", text) chunks, cur = [], "" for s in sentences: if len(cur) + len(s) <= max_size: cur += s else: if cur: chunks.append(cur) cur = s if cur: chunks.append(cur) return chunks if __name__ == "__main__": sample = ("今天天气很好。我们一起去公园。公园里有很多花。" "下午回家。我开始写代码。代码跑通了。") print("固定窗口:", fixed_window(sample, size=15)) print("滑动窗口:", sliding_window(sample, size=15, overlap=5)) print("递归字符:", recursive_split(sample, max_size=15)) print("语义分块:", semantic_split(sample, max_size=15))
def fixed_window(text, size=50):定义固定窗口分块函数,默认块大小 50 字符return [text[i:i + size] for i in range(0, len(text), size)]按固定步长 size 切片,一块接一块,毫无重叠stride = size - overlap滑动窗口的步长:块大小减去重叠量,重叠越大步长越小for sep in seps:递归字符分块的入口:从最高优先级的分隔符开始尝试parts = text.split(sep)用当前分隔符把文本劈成若干子串if len(cur) + len(s) <= max_size:句子还能塞进当前块就继续塞,塞不下就另起一块chunks.append(cur)一个块攒满后,把它收进结果列表
常见误区
误区“文本分块嘛,随便切切就行——反正 Embedding 模型能理解意思,检索靠向量相似度就够了吧?”
正解分块决定检索粒度和语义完整性。切碎了,Embedding 再强也难找回完整观点;切粗了,一块里全是噪声,Top-k 检索结果中真正相关的比例直线下降。就像把一盘红烧肉剁成肉泥,再好的舌头也尝不出原来的味道。
为何常见:刚接触 RAG 时,注意力都放在模型和向量库上,低估了“前处理”对链条的放大作用。
误区“块越大,喂给大模型的信息越全,回答自然越准。512 不够就 1024,1024 不够就 2048。”
正解块过大会引入大量无关话题,检索时相似度被噪声稀释,真正相关的信息反而“沉”在块里;同时 token 成本上升,响应变慢。块大小应匹配任务复杂度,通过实验调优而不是盲目加大。
为何常见:直觉上“信息多 = 好”,忽略了检索机制是“相关性排序”而非“全都要”。
误区“滑动窗口的重叠不就是防止信息断裂吗?那把 overlap 设成块大小的一半,肯定更保险。”
正解重叠会带来存储和成本成倍增长——overlap 越大,stride 越小,块数越多;检索时还可能返回多个几乎重复的块,浪费上下文窗口。一般 overlap 取块大小的 10%~20% 就足够。
为何常见:看到了滑动窗口解决“切断语义”问题的好处,却没算冗余的账。成本是块数的函数,而块数是重叠的增函数。
误区“语义分块检索质量最高,那所有文档都用它就好了,贵有贵的道理。”
正解语义分块需要额外调用 Embedding 模型为每个句子推理,处理速度慢、成本高。对实时性要求高的流水线(如在线问答、流式接入)和超长文本,可能不划算。它适合离线处理的高质量文档库。
为何常见:对比实验里语义分块常表现最好,让人误以为“越贵越高级越有效”,忽略了工程上的时效和成本约束。
误区“递归字符分块顺着段落和句子切,语义一定完整,不会切断观点。”
正解它只是“尽量”尊重文本结构,本质上仍然在靠字符模式(分隔符)切分,而非理解语义。如果文档没有清晰分隔符(如服务器日志、聊天记录),或者分隔符质量不高,效果可能和固定窗口差不多,甚至因为递归开销更慢。
为何常见:把“结构优先”误当成“语义优先”,忽略它依然是一个基于规则的方法。
面试题
Q1. 在 RAG 系统中,对文档做分块的最主要原因是?
- A.减少存储空间的占用
- B.适配上下文并提升检索精度← 正确答案
- C.加快模型训练的收敛
- D.提高生成文字的多样性
答案:B。RAG 的核心是先把文档切成小块存起来,检索时按相关度召回再喂给模型。分块的核心目的是让每个块既能在模型上下文窗口内,又能被精准检索到。减少存储(A)不是主因,加快训练(C)与 RAG 无关,多样性(D)也答非所问。
Q2. 固定窗口分块最大的缺点是?
- A.实现起来过于复杂
- B.计算成本消耗太高
- C.可能切断语义的边界← 正确答案
- D.完全不需要 embedding
答案:C。固定窗口像用尺子切豆腐——整齐但会切断花纹。它不管句子和段落边界,可能把一个完整观点劈成两半,检索时只找到半句话。它实现简单、成本低(A、B 是反的),不需要 embedding 是特点而非缺点(D)。
Q3. 滑动窗口分块中,stride(步长)的计算方式是?
- A.stride 等于块大小加重叠值
- B.stride 等于块大小减重叠值← 正确答案
- C.stride 等于重叠值减块大小
- D.stride 等于块大小乘重叠值
答案:B。滑动窗口让相邻块有重叠,重叠值 O 是重复保留的部分,所以真正向前移动的步长 = 块大小 − 重叠。如果 stride = S + O(A),块之间会出现缝隙;其他两个式子更是无意义的变形。
Q4. 递归字符分块在选择分隔符时的基本策略是?
- A.段落优先于句子和词← 正确答案
- B.句子优先于段落和词
- C.随机挑选任一标点符号
- D.只用换行符做单一切分
答案:A。递归字符分块像顺着筋膜切肉,优先用最自然的分隔符。段落(空行)是最强的边界,其次换行、句号、空格,按优先级从高到低逐层切分。它不会随机选(C),也不会只用一个分隔符(D),更不是句子优先(B)。
Q5. 语义分块判断切分边界的核心依据是?
- A.文本字符总数占比
- B.标点符号出现的频率
- C.相邻句向量的相似度← 正确答案
- D.关键词出现的次数
答案:C。语义分块把句子编码成向量,用相邻句向量之间的余弦相似度判断话题是否转折:相似度低的地方就是切分点。字符数(A)、标点频率(B)、关键词次数(D)都是表面的文本特征,不是语义依据。
Q6. 语义分块相比其他策略的明显劣势有哪些?
- A.需要额外的 embedding 推理← 正确答案
- B.计算和存储的成本较高← 正确答案
- C.检索质量一定处于最优
- D.完全无需任何参数调优
答案:AB。语义分块贵在“理解”,成本自然不低:需要额外调用 Embedding 模型(A),计算与存储开销大(B)。但它并不“一定最优”(C 太绝对),也需要调阈值等参数(D 错)。像顶级大厨备菜,费时费力但味道好——前提是预算够。
Q7. 以下哪种文本最适合用滑动窗口分块?
- A.需要保持段落完整语义
- B.对话流式的连续上下文← 正确答案
- C.超短文本的简单切分
- D.图片描述的自动生成
答案:B。滑动窗口的核心价值是保留块间上下文连续。对话流式文本话题连贯、没有明显的章节边界,最适合用它。保持段落完整(A)更适合递归字符或语义分块;超短文本(C)怎么切都行;图片描述(D)不是文本分块的范畴。
Q8. RAG 分块策略选型时,需要考虑的因素包括?
- A.文档的类型与结构← 正确答案
- B.业务的时效与预算← 正确答案
- C.办公室座位的朝向
- D.下游生成的性能要求← 正确答案
答案:ABD。选型要看“食材”和“菜谱”:文档类型与结构(A)决定天然边界在哪;业务时效与预算(B)决定能否接受语义分块的高成本;下游生成性能要求(D)决定块大小与检索质量目标。办公室座位(C)显然是干扰项,选型不取决于工位风水。
Q9. 设置 chunk_overlap 参数的主要目的是?
- A.让每个块变得小一些
- B.避免跨块信息被切断← 正确答案
- C.减少总体的计算量
- D.替代语义模型做分块
答案:B。overlap 是相邻块之间的重叠区域,让一个跨边界的观点能完整出现在至少一个块里,避免信息断裂。它不是让块变小(A)、更不是减少计算(C)或替代语义模型(D)。就像刀锋带着重叠切肉,是为了不遗漏纹理。
Q10. 关于文本分块参数调优说法正确的有?
- A.大块信息全但噪声较多← 正确答案
- B.小块信息纯但易断语义← 正确答案
- C.块大小需实验对比评估← 正确答案
- D.任何文档都用固定大小
答案:ABC。参数调优是权衡的艺术——块大有噪声(A)、块小易断义(B),所以要用实验对比(C)找到最佳点。“任何文档都用固定大小”(D)正是我们反对的“一刀切”。火候对了,菜才好吃;参数对了,检索才准。