文本分块(Chunking)的艺术:RAG 效果的第一道分水岭
本文是 RAG 系列的第 3 篇。前情回顾第 1 篇讲清了 RAG 的原理第 2 篇用百行代码跑通了完整流程。上一篇结尾我坦白过demo 里那个切分函数是最笨的切法。今天兑现承诺专门聊聊文本分块Chunking——这个在大多数教程里被一笔带过的环节恰恰是拉开 RAG 效果差距的第一道分水岭。一、先看一个事故现场把第 2 篇里那份《差旅报销制度》示例文档用我们的固定长度切分300 字一块、重叠 50 字跑一遍第 1 块的结尾长这样## 报销流程与时限 出差结束后 30 天内在 OA 系统提交报销单并粘贴 发票原件经主管与财务审核后款项在看到了吗句子在款项在 这里戛然而止——报销款到底多少天到账被切没了。再看第 2 块的开头与时限 出差结束后 30 天内在 OA 系统提交报销单……报销流程与时限这个标题被砍了头只剩下与时限三个字孤零零地飘在块首。这次运气不错因为设置了 50 字的重叠完整的那句话在第 2 块里活了下来检索还能找到它。但你应该看出问题了——**重叠只是给拦腰斩断打的补丁而补丁靠的是运气。**文档一长、句子一长断口随时可能超出重叠范围而与时限这种砍头碎片则会一直留在索引里充当噪声。二、为什么分块这么重要在一个 RAG 系统里块身兼三职它是向量化的单位——每个块生成一个语义坐标它是检索的单位——系统找的不是哪份文档相关而是哪个块相关它还是喂给大模型的单位——检索到什么模型就看什么。换句话说你的知识库在系统眼里从来不是一份份文档而是一个个块。检索以块为单位进行块的质量直接决定了检索的上限。切得不好后面模型再强、Prompt 写得再漂亮都是在垃圾堆里做菜。三、核心矛盾块太小 vs 块太大分块的所有技巧本质上都在处理同一对矛盾。块太小语义残缺。比如这句话单独成块“该标准自下季度起执行逾期按旧标准处理。”该标准是哪个标准没人知道——它的上文在别的块里。就算被检索到了模型拿着它也答不出个所以然。这叫指代失联。块太大向量被稀释。一个 800 字的块同时讲了住宿、餐补、发票三个主题。给它生成的那一个向量要同时代表三个主题——结果就是哪个主题都代表不好。打个比方给全班拍一张合影每个人的脸都只有指甲盖大。用户问发票抬头写什么时这个块和问题的相似度被住宿、餐补的内容拉低很可能挤不进检索结果的前三名。就算侥幸检索到了一大块内容里只有一小段相关其余全是塞给模型的噪声既干扰回答又浪费 token。所以分块没有最优解只有权衡块要小到语义聚焦又要大到语义完整。四、三个参数的经验起点chunk_size块大小中文场景200500 字是常见起点。条款、FAQ 这类一条一个意思的内容偏小报告、论文这类叙述性长文偏大。另外有个硬约束块的长度不能超过向量模型的处理上限。比如常用的 bge-v1.5 系列最长只能处理 512 个 token中文大约对应五百字上下超出的部分会被直接截断——你以为存进去了其实模型压根没看见。这一点下一篇讲向量模型选型时会展开。overlap重叠chunk_size 的 10%20% 是常见取值。它的本质是给硬切打补丁如果你用了后面讲的边界切分重叠的必要性会大幅下降。top_k检索条数块切得小可以适当调大 top_k用数量补上下文。三个参数是联动的不要孤立地调。五、四种主流切分策略策略一固定长度切分就是第 2 篇用的那种数够 300 字就下刀。一句话点评实现只要 5 行事故现场也不断。它的唯一价值是当 baseline——没有理由在真实项目里止步于此。策略二递归字符切分行业默认起点思路很朴素在自然边界下刀。给分隔符排个优先级——段落\n\n→ 换行\n→ 句号 → 感叹号/问号 → 分号——优先按段落切某段实在超长再降级用下一层分隔符继续切最后把碎片拼装成接近目标大小的块。下面是可以直接替换进第 2 篇rag_demo.py的教学版实现SEPARATORS[\n\n,\n,。,,,]defsplit_text_v2(text:str,chunk_size:int300)-list[str]:defsplit_by(piece:str,seps:list[str])-list[str]:iflen(piece)chunk_size:return[piece]ifnotseps:# 分隔符用完了硬切兜底return[piece[i:ichunk_size]foriinrange(0,len(piece),chunk_size)]parts[p.strip()forpinpiece.split(seps[0])ifp.strip()]result[]forpinparts:result.extend(split_by(p,seps[1:]))returnresult piecessplit_by(text,SEPARATORS)# 把切出来的片段重新拼装成接近 chunk_size 的块chunks,buf[],forpinpieces:iflen(buf)len(p)1chunk_size:buff{buf}\n{p}ifbufelsepelse:ifbuf:chunks.append(buf)bufpifbuf:chunks.append(buf)returnchunks同一份报销制度v2 切出来是这样的--- 第 1 块237 字--- # 差旅报销制度示例 ## 出差申请 员工出差需提前 3 个工作日在 OA 系统提交申请…… ## 交通标准 …… ## 住宿标准 …… ## 餐费补贴 …… --- 第 2 块132 字--- ## 报销流程与时限 出差结束后 30 天内……款项在 10 个工作日内打入工资卡。 ## 发票要求 所有报销票据须为增值税发票……每个块都在小节边界处干净断开标题完好、句子完整再也没有与时限这种碎片。两点说明一教学版为了让原理一目了然省略了 overlap——在自然边界切分后拦腰斩断基本不会发生二不想手写的话装一个轻量库即可它还支持重叠等更多选项# pip install langchain-text-splittersfromlangchain_text_splittersimportRecursiveCharacterTextSplitter splitterRecursiveCharacterTextSplitter(chunk_size300,chunk_overlap45,separators[\n\n,\n,。,,,,,],)chunkssplitter.split_text(text)注意一定要像上面这样传入中文分隔符——默认配置是给英文准备的不认识中文句号。策略三结构感知切分性价比之王很多文档天生就有结构Markdown 有标题层级HTML 有标签代码有函数和类制度手册有条款编号。顺着结构切胜过任何通用算法。在此基础上还有一个性价比极高的小技巧把标题路径写回块里。比如住宿标准这一节入库时存成【差旅报销制度 住宿标准】 一线城市北京、上海、广州、深圳每晚不超过 500 元 其他城市每晚不超过 350 元。好处立竿见影用户问出差住宿时块里明晃晃写着差旅“住宿”语义匹配直接命中模型回答时也天然知道这段内容的出处和语境。对代码文档同理按函数或类切分别把一个函数的签名和函数体切到两个块里。策略四语义切分上限高成本也高前三种都靠形式字数、标点、结构判断边界语义切分则直接看内容把文本先切成句子用向量模型算出每个句子的语义坐标再计算相邻句子之间的相似度——相似度一路很高说明还在聊同一件事突然跌落说明话题变了就在这个语义断崖处下刀。它能处理结构混乱、话题交错的文本上限最高代价是入库前要多算一遍向量慢、贵而且切分质量反过来依赖向量模型的水平。LlamaIndex、LangChain 都有现成实现。我的建议先把策略二和策略三做扎实再考虑它。六、两个进阶心法点到为止心法一小块检索大块生成。检索要准块越小定位越准生成要全上下文越完整答案越好——这俩天然打架。解法是给块建立父子关系用小块甚至单句做检索命中之后把它所在的父块或前后相邻的块一起交给大模型。检索的准度和生成的完整度就都要到了。这招专治明明找到了回答却缺胳膊少腿。心法二给块补上下文再入库。块被切出来的那一刻就失去了上下文——该标准自下季度起执行的问题就在这。一个越来越流行的做法Anthropic 提出的 Contextual Retrieval 就是代表入库前让大模型给每个块写一句说明——“本段出自《差旅报销制度》的报销时限一节规定了报销提交期限和打款时间”——拼在块的前面再一起向量化。每个块从此自带身份证检索命中率显著提升。代价是建库时多一轮大模型调用适合文档量可控、对效果要求高的场景。七、怎么选一张速查表默认起手式递归切分300 字左右起步配上标题元数据手册、制度、API 文档这类结构清晰的内容结构感知切分长报告、论文这类语义连续的长文且预算充足试试语义切分症状是找得到但答不全上小块检索、大块生成文档量不大、追求极致效果给块补上下文再入库。最后送一句大实话分块参数没有标准答案评估才有。改一个参数效果是好是坏不能靠感觉,得靠数据说话——怎么科学地评估一个 RAG 系统,这个系列后面会用一整篇来讲。写在最后回顾今天的要点块是 RAG 里向量化、检索、生成的基本单位块的质量决定检索的上限核心矛盾是小而残缺 vs 大而稀释所有策略都在找平衡点实战路线固定切分只配当 baseline →递归切分是默认起点→ 有结构就用结构 → 语义切分留给高要求场景两个进阶心法小块检索大块生成、给块补上下文。下期预告《中文 Embedding 模型选型指南》——块切得再漂亮向量模型不给力语义坐标标错了位置检索照样抓瞎。BGE、GTE、Qwen3-Embedding、M3E……中文场景到底怎么选下一篇见。如果这篇文章对你有帮助欢迎点赞、在看、转发三连。关注本号不错过下一篇。