RAG项目做久了你会发现一个很扎心的规律检索召回效果差大家第一反应是换 Embedding 模型、调向量检索的 TopK、甚至重写提示词但真正拖后腿的往往是没人愿意多看一眼的分块逻辑。我见过好几个项目召回率从 30% 提到 70%没动任何模型纯粹是把 chunk 切法改了。这篇文章不聊虚的直接讲分块的参数怎么定、不同策略适合什么场景、切坏了怎么定位以及一个 200 行内能跑通的最小评测脚本帮你把分块到底行不行这件事从玄学变成可验证的工程决策。1. 说句实话大部分RAG分块问题是在切之前就埋下的1.1 分块决定了检索的最小单元也决定了答案的可用边界很多人把 RAG 理解成先检索、再生成两步分块只是中间一个不起眼的预处理环节。但在实际项目里分块本质上是你在回答检索系统以什么粒度去理解你的文档。每一条向量记录都是一个 chunk用户提问之后系统先检索出最相关的几条 chunk再把它拼进上下文最后让生成模型输出答案。这意味着两件事。第一如果 chunk 太小比如一句话一块检索时单条命中的信息量往往不足生成模型拿到的只是一个片段缺乏前后文的限定和因果回答容易断章取义。第二如果 chunk 太大比如整篇文档一块检索粒度太粗用户问一个细节问题时命中的是一整篇无关内容占多数的长文语义相似度被大量噪音稀释召回精度会明显下降。更麻烦的是大 chunk 放进上下文还会挤占生成模型的输入窗口要么截断要么把真正有用的内容挤出去。所以分块不是一个随便切成多少字的简单操作它同时决定了检索的精度上限和生成的上下文质量。我经常跟同事说分块是在鸡蛋和篮子之间做平衡切得太碎检索到的鸡蛋常常不够做一道菜切得太大篮子里混进了一堆烂菜叶。理解了这个矛盾后面所有参数调整你都能找到依据而不是靠撞运气。1.2 最常见的三个分块坏习惯死守 512、无视文档结构、overlap 用成安慰剂我接手过的模拟项目 X 里最初沿用了一套看起来没毛病的分块配置所有文档按固定 token 数 512 切分重叠 128 token。这套配置最大的问题是它对 PDF、Markdown、HTML、代码文件一视同仁仿佛所有文档天然就应该以 512 个 token 为边界。第一个坏习惯是死守某个固定数值。512 这个数字流传很广但它只是一个普适性较强的参考值不是标准答案。纯英文技术文档、中文合同、FAQ 列表、代码仓库它们的语义密度和句子长度完全不同。你用一个数去切所有类型本质上是让数据去适应参数而不是让参数服务数据。第二个坏习惯是无视文档结构。Markdown 的章节标题、HTML 的标题层级、PDF 的段落停顿这些天然边界背后都隐含了语义的聚合关系。固定窗口切分经常把一个小节的内容拦腰截断甚至出现表格的一半在 chunk A、另一半在 chunk B这种惨案。结构感知分块并不是什么高级魔法它就是在切之前先识别这些边界尽量让语义完整的段落不被破坏。第三个坏习惯是把 overlap 当安慰剂。很多人迷信重叠多一点就能保证上下文连贯结果重叠比例开到 50%检索结果大量重复去重之后有效信息增量很小反而浪费存储和检索资源。Overlap 应该解决的是段落边界信息被截断的问题不是用来无限弥补不合理的 chunk size 的。后面我会详细展开参数之间的联动关系。2. 参数决策不要只盯着chunk_size把这几个旋钮放在一起调2.1 真正影响分块质量的参数是四个不是两个大多数教程只提 chunk_size 和 overlap但实际工程里至少有四个参数互相咬合chunk_size块长度、overlap块重叠长度、embedding 模型的最大输入长度、生成模型的上下文窗口。前两个是切块层面的后两个是约束层面的。先看 chunk_size。你定的每个 chunk 的实际 token 数最终要经过 Embedding 模型编码成向量。如果嵌入模型最大输入是 512 token你却切了 1024 token 的 chunk不同实现方式会有三种结果直接报错、自动截断、长文本平均池化。自动截断相当于后半段内容根本没有参与向量化检索时自然看不见平均池化则会把长文本语义摊薄。再看 overlap。它是为了补偿硬边界带来的信息割裂设计初衷是让边界两侧的语义有交叉重叠避免用户问到一个跨边界问题时两边都只记得一半。但 overlap 本身也是要占用存储和计算资源的而且重叠太大会让相邻 chunk 的向量高度相似影响检索结果的多样性。还有生成模型的上下文窗口。即使检索很准你一次性把 6 个 1024 token 的 chunk 全部塞进提示词也容易超出窗口限制触发截断或摘要压缩。所以分块参数的最终选择必须回头去看你要把多少个检索结果送给生成模型。我习惯先定检索条数 × 单块长度的上限让它留有余量地落在上下文窗口内然后反推 chunk_size。2.2 一张可落地的参数决策表先定约束再定目标最后试错下面这张表是我在实际项目里常用的参考区间注意它是起始参考而不是通用结论。具体项目一定存在自己的最优区间这张表的用途是帮你把试错范围收敛到合理区间内避免从零瞎猜。参数参考区间适用线索容易踩的坑chunk_size256~1024 token文档信息密度、回答粒度固定 512 一用到底超过 Embedding 输入上限overlapchunk_size 的 10%~20%跨段引用多不多、句式连续性无脑 50% 重叠重复数据拖慢检索单次检索条数3~6 个 chunk问题复杂度和上下文需求条数太多提示词超窗条数太少信息不足检索条数 × 单块长度不超过生成模型窗口的 60%留出提示词和回答的空间几乎打满窗口生成阶段被迫截断我的一般操作顺序是这样的先看 Embedding 模型输入长度把 chunk_size 定在它的 50%~70% 以内留出文本段落到向量化之间的安全余量。然后根据回答粒度粗调做开放域问答chunk 可以偏小256~512 之间做文档摘要或综合分析chunk 可以偏大到 800 左右。最后根据你实测到的跨段截断问题微调 overlap不要一开始就重叠加满。提示如果一个 chunk 的实际内容本身就包含多个语义段落比如一份 FAQ 里连续十几条问答被切进一个块里chunk_size 数值再合理也没用。参数调整之前先检查你的文档结构有没有正确识别。2.3 从 token 到中文字符别再数错单位了很多中文项目在定 chunk_size 时习惯按字或字符去数但 Embedding 模型和生成模型的上下文限制大多按 token 计。中文字符和 token 的换算比例大致在 1 个汉字约等于 1~2 个 token具体取决于分词器。所以如果你要切 500 token实际对应的中文文本大概在 250~400 字之间而不是 500 字。我在某个图像处理 Demo 的配套文档项目里吃过这个亏给英文文档定 512 token 跑得好好的同一套配置搬到中文合同上一个 chunk 直接塞了 800 多字相当于 1200 多个 token超出 Embedding 输入上限后半段被静默截断检索质量肉眼可见地崩了。建议在分块代码里主动打印每个 chunk 的 token 数分布先确认你的库对 token 的计数是否和模型分词器对齐。至少要做到你定的数值和你实际喂给模型的数据是同一个度量单位。3. 策略选型固定窗口、结构感知、语义分块哪种配得上你的场景3.1 固定窗口分块最稳妥的默认项但不是所有场景的好选项固定窗口分块就是把文本按固定 token 数切成若干块再带一个 overlap。它实现简单、处理速度快、跨文档类型表现稳定非常适合做系统基线和冷启动。如果你手头文档结构乱七八糟或者你不确定该用什么边界先用固定窗口跑通全链路永远比纠结策略半天一动不动的强。但它的短板也很明显完全不理解内容的语义结构只是机械地按长度切分。遇到代码块、表格、列表、层层嵌套的 Markdown 标题它经常把逻辑上不可分割的内容劈成两半甚至产生无头无尾的残块。对检索而言残块意味着向量表示被污染——一个 chunk 里混入了两个不相关主题最后检索结果两头不靠。3.2 结构感知分块先让文档自己告诉你边界在哪里结构感知分块的核心思想是不要在纯文本的字节流上盲目切分而是先识别文档的组织结构再基于结构边界来分块。比如 Markdown 文档按标题层级切HTML 按文档标签切PDF 按段落和表格区域切代码按函数和类定义切。切完之后如果某个结构块还是太长再往下一级结构递归切直到满足长度约束。这种策略尤其适合三种场景Markdown/HTML 技术文档、带有明确章节和层级的产品手册、以及源码仓库。拿某个电商客服团队的帮助中心来说每篇文章都有明确的一级标题-二级标题-正文段落结构用结构感知分块切出来的 chunk 内部语义高度内聚用户问退货几天内可以处理命中的基本就是退货政策那一段而不是一半条款加一半物流说明。实现结构感知分块的时候注意两点。第一标题本身要作为 chunk 的元数据前缀保留下来甚至嵌进 chunk 文本里让向量包含上下文的定位信息。第二递归切分时overlap 的位置尽量放在段落边界上而不是硬塞在句子中间否则照样会出现句子被拦腰截断的问题。3.3 语义分块像按话题切一样自然但成本也最高语义分块是指利用语言模型或文本相似度动态判断句子之间的语义连贯性在话题发生转折的地方切块。常见做法有两种一是把文本按句子切分后计算相邻句子的向量相似度相似度低于阈值的点就是切分点二是用 LLM 做主题切分让模型判断一段文本里包含几个独立话题再按话题边界分块。这种策略的优势是块内语义高度内聚检索时命中的话题纯度最高。缺点是明显的处理速度慢每篇文档都要额外跑一遍模型或嵌入成本高阈值不好调常见的问题是有时候切得太碎有时候又太平滑。我的主观结论是如果文档本身就有清晰的结构边界优先考虑结构感知分块只有结构缺失、但内容确实在频繁切换话题的场景才值得为语义分块付出额外成本。比如片段化收集的聊天记录、会议纪要这种文本天然没有标题层级语义分块优势更大。3.4 混合分块父文档与子块结合解决检索精确但上下文缺失的老大难固定窗口检索精确、但上下文太碎结构分块上下文完整、但检索粒度可能不够细。混合分块试图同时拿这两头。实现上通常有两种形态一种是父子分块父块是一个较大的语义块子块是父块切出来的更小单元。检索时命中的是子块但送给生成模型时把子块对应的父块一起带回。子块负责精准命中父块负责提供上下文。另一种是多粒度分块把同一份文档同时用小粒度和大粒度各切一份检索时先看小粒度命中再按位置关联大粒度原文。混合分块的价值在实际项目中非常明显。之前做某个跨平台系统的知识库问答用户提问通常很短比如这个功能怎么关闭如果用大块检索命中的 chunk 跟关闭相关的只有一句话其余全是无关描述召回精度很差。后来改成子块检索、父块拼接命中精度上来了上下文也完整了回答质量一下子提升一个档位。不过混合分块也有代价存储量翻倍、索引结构更复杂、检索后的去重和合并逻辑更繁琐。建议项目进入中后期、检索精度已经成了瓶颈时再引入而不是作为第一天就要上全套的重量级方案。4. 切完反而更差了一条可复现的排错链路4.1 先别急着调参确认问题出在检索阶段还是生成阶段分块改动之后检索效果变差很容易让人立刻去怀疑 chunk_size 和 overlap 的数值。但更高效的做法是先定位差到底差在哪一环。我习惯把 RAG 流水线拆成两个黑盒检索盒和生成盒。检索盒输入用户问题输出召回的 chunk 列表生成盒输入用户问题加检索 chunk输出最终答案。你可以做两个快速实验。第一个实验把固定的正确答案直接写死在提示词上下文里绕过检索看生成模型能不能答对。如果连给完整答案时模型都答不好那问题多半在提示词或生成配置分块别背这口锅。第二个实验保留检索但把召回的 chunk 内容和文本原文打出来人工判断如果我是人光靠这几段能不能回答这个问题。如果人都找不到答案说明是检索召回的问题分块和向量索引才是排查重点。这一步至关重要因为我见过不少团队折腾分块半天结果发现是提示词模板里把上下文顺序搞反了或者 TopK 取太少了。分块调整确实重要但不要让它变成所有问题的垃圾桶。4.2 召回不足的定位方法从召回现象反推 chunk 切错了哪里如果你确认是召回环节的问题就可以按下面的链条逐步定位。第一看召回文档类型。如果用户问题里提到的信息明确在某篇文档里但检索结果完全没有命中这篇文档先检查这篇文档有没有进索引、有没有被跳过。常见原因包括解析失败、清洗阶段被过滤、或者文件格式不支持。第二看命中的 chunk 片段。如果真的命中了但命中的内容不是问题的关键段落那很可能是 chunk 切分把关键信息拆散到两个块里了或者关键信息和大量无关信息混在同一块中向量表示被拉偏。这种情况你可以做一个简单测试手动把问题对应的完整段落抽出来当作唯一一条 chunk 去检索如果这样都无法命中说明问题的向量表示本身和答案段落距离远需要换 Embedding 模型或改写问题如果这样能命中说明是分块方式的问题继续排查切分边界。第三看 TopK 召回结果的排序。除了准确率还要关注顺序。如果正确答案出现在第 8 位而系统只取前 5 个说明向量相似度排序不够理想可能是 chunk 边界削弱了关键词权重也可能是多义词干扰。这里可以适度调大 TopK或者对 chunk 文本做预处理把关键词权重比较高的部分前移。4.3 召回没问题但生成乱答的定位方法从上下文拼接找原因召回明明命中了包含答案的 chunk但生成结果依然胡说八道这种情况也经常发生。原因往往隐藏在上下文拼接这个环节里而不是分块本身。最常见的坑有四种。第一种是多个 chunk 之间语义断裂生成模型拿到的是 3 块互不相干的片段无法拼出连贯的事实链条。这种情况可以改用父子分块或者按文档顺序调整拼接顺序让相邻段落有上下文衔接。第二种是chunk 里混入了同主题但不同结论的相互矛盾的信息模型无所适从。需要做的事是在分块阶段尽量保证块内结论一致或者在提示词里明确只依据最相关段落回答。第三种是chunk 内的关键词被截断比如盖了一半的表格、只留了表头没有数据行、一句话讲到一半没了。第四种是上下文超窗被静默截断提示词末尾的内容没进生成模型恰好答案就藏在被截掉的部分里。你可以做一次所见即所得检查把你构造的最终提示词完整打印出来人工读一遍检查有没有残缺语句、有没有截断、有没有上下文顺序颠倒。看起来简单但很多诡异问题就是这样一眼看出来的。4.4 分块排错速查表现象到对策的一页纸表象可能原因快速确认法调整方向检索完全命不中目标文档解析/索引遗漏查该文档是否入库修预处理重建索引命中文档但命中块不相关chunk 语义混杂看命中块文本内容改结构感知或缩小 chunk正确答案排在 TopK 之外单个 chunk 信息不足手动单块检索测试调大 TopK 或缩小粒度命中块太碎上下文缺失chunk 过小人工读命中块增大 chunk_size 或父子分块命中块把跨页信息截断overlap 不够检查切分边界提高 overlap 至 15%~25%生成结果与命中内容相悖上下文拼接混乱打印 prompts 检查调整拼接顺序、去重、加指引生成模型输入超窗检索块太多太长检查实际输入长度减少检索条数或缩短单块长度这张表我自己贴了很久每次调分块遇到问题先对照现象去查原因而不是像没有头绪一样盲目改参数。工程排错最重要的是建立现象-假设-验证的闭环哪怕是 5 分钟就能跑完的小实验也比拍脑袋调参有价值的得多。5. 最小评测脚手架三指标脚本判断分块到底是好是坏5.1 评测集怎么造手工标注 20~30 条足够跑第一轮很多人觉得评测 RAG 是件重工程的事非要攒几百条数据、搭完整评测平台才敢开始。实际上在分块调优阶段一个 20~30 条的手工评测集已经能帮你解决 80% 的方向判断问题。每条评测样本由三部分组成用户问题、标准答案片段、标准答案所在的参考文档。不需要标注到具体哪个 chunk只需要文档级别的勾连然后让评测脚本根据检索结果自动判断。样本要从真实用户问题里挑而不是拍脑袋想象。如果你还没有真实用户问题可以先找业务方要他们最关心的 20 个高频问题如果连这个都没有至少要覆盖简单事实问、跨段落推理问、限定条件问三类保证评测不是单一维度。评测集做好了固定下来以后每次改分块策略都跑同一套方便横向对比。我用过的最小配置就是一个 JSON 文件每项记录 query、answer、source_id代码里读进来即可。5.2 三个核心指标命中率、答案覆盖度、上下文噪音度评测指标不必贪多我长期使用的就三个。命中率简单说整个检索结果中有没有一段包含标准答案的核心内容。按文档粒度判断只要命中的 chunk 落在标准参考文档内即视为命中。它是召回层面的指标用来回答这条信息到底找没找到前端。答案覆盖度更进一步看标准答案片段中有多少比例实际出现在了检索返回的文本里。因为实际使用中答案通常不止存在一处覆盖度能反映分块是否把相关内容拆散、丢了多少关键信息。上下文噪音度统计最终送入生成模型的检索结果里和标准答案完全无关的文本占比。这个指标在无标注时可以用字符重叠率或关键词命中的近似值代替但它揭示的是模型捡了个西瓜还是捞了一堆芝麻的问题。三个指标配合使用命中率解决有没有找到覆盖度解决找到的是不是完整答案噪音度解决生成模型吃了多少垃圾。5.3 一个 200 行以内的 Python 评测脚本框架下面这个脚本框架是我个人项目里常用的形态经过删减保留核心。它的设计思路是从评测集读样本对每个 query 做检索落地到本地文件然后跑三个指标。注意这个脚本里假设你已经有向量库客户端函数和文本切分函数我这里用接口占位符代替方便你直接换成自己的实现。import json import re from typing import List, Dict, Any # 占位接口请替换为你的实现 def get_embedding(text: str) - List[float]: 返回文本的向量。 raise NotImplementedError(用你的 Embedding 接口替换) def search_chunks(query_embedding, top_k: int) - List[Dict[str, Any]]: 返回长度为 top_k 的检索结果列表。 raise NotImplementedError(用你的向量库检索接口替换) def chunk_document(doc: Dict[str, Any]) - List[Dict[str, Any]]: 返回文档切分后的 chunk 列表。 raise NotImplementedError(用你的分块逻辑接口替换) # 步骤一准备评测集 # 每条样本: {query: ..., answer: ..., source_id: doc_xxx} EVAL_SET_PATH eval_set.json TOP_K 5 with open(EVAL_SET_PATH, r, encodingutf-8) as f: eval_set json.load(f) # 步骤二对每条 Query 完成检索 retrieved_docs [] # 每个元素是 dict含 query_id、hit_doc、overlap_text 等 for item in eval_set: query_vec get_embedding(item[query]) hits search_chunks(query_vec, TOP_K) hit_text .join(h[text] for h in hits) hit_ids set(h[doc_id] for h in hits) retrieved_docs.append({ query: item[query], answer: item[answer], source_id: item[source_id], hit_ids: hit_ids, hit_text: hit_text, hit_scores: [h[score] for h in hits] }) # 步骤三三指标计算 def calc_hit_rate(retrieved_docs: List[Dict]) - float: 命中率检索结果里是否出现了标准参考文档。 ok 0 for r in retrieved_docs: if r[source_id] in r[hit_ids]: ok 1 return ok / max(len(retrieved_docs), 1) def calc_coverage(retrieved_docs: List[Dict]) - float: 答案覆盖度标准答案里有百分之多少的字/词出现在检索文本中。 total_cover 0.0 for r in retrieved_docs: answer r[answer] hit_text r[hit_text] if len(answer) 0: continue # 用字符 n-gram 的简单方式估算覆盖度 answer_chars set(answer) hit_chars set(hit_text) covered len(answer_chars hit_chars) / len(answer_chars) total_cover covered return total_cover / max(len(retrieved_docs), 1) def calc_noise(retrieved_docs: List[Dict], top_k: int) - float: 噪音度检索结果中平均有多少文本是无关内容按字符串相似度近似。 noise_total 0.0 for r in retrieved_docs: answer_chars set(r[answer]) hit_text r[hit_text] if len(hit_text) 0: noise_total 1.0 continue # 与答案字符集合重合率低的部分视为噪音 hit_chars set(hit_text) overlap len(answer_chars hit_chars) / max(len(hit_chars), 1) noise_total 1.0 - overlap return noise_total / max(len(retrieved_docs), 1) hit_rate calc_hit_rate(retrieved_docs) coverage calc_coverage(retrieved_docs) noise calc_noise(retrieved_docs, TOP_K) print(fHit Rate: {hit_rate:.3f}) print(fCoverage: {coverage:.3f}) print(fNoise: {noise:.3f}) # 步骤四把命中失败样本打印出来人工检查 print(\n 未命中的 Query 样例 ) missed [r for r in retrieved_docs if r[source_id] not in r[hit_ids]] for r in missed[:5]: print(fQuery: {r[query]}) print(fSource: {r[source_id]}) print(---)这个脚本本质上不解决任何分块算法问题它解决的是如何快速知道当前分块到底行不行。跑完一版改完参数再跑一版三个指标前后对比胜负一目了然可以避免人工抽样评价时的主观感觉。5.4 用评测结论而不是感觉去决定要不要换策略我帮好几个项目组做过分块方案的技术选型几乎每次都发现大家的感觉和评测数据是冲突的。比如有人觉得语义分块听起来更智能、跑完一定更好但评测结果是命中率和固定窗口分块打平噪音度却明显偏高因为语义分块把相关但非答案的内容也算进了一个块反而污染结果。反过来有人觉得切小一点肯定精确实际覆盖度反而下降因为标准答案被拆散到 4 个不同的 chunk模型拼起来太费劲。评测脚本的最大价值就是逼你在下一次调整之前先给出一个基线。没有基线所有的策略对比都是在空谈。最小评测脚手架的意义也正在于此20~30 条样本、三个指标、一个脚本半天之内就能跑出第一版数据而不是让分块调优在我觉得好像应该里反复打转。6. 写在最后的操作体会分块调优是持久战不是一次性洁癖分块这个东西最大的迷惑性在于它表面上只是个预处理步骤改起来也只需要改一个函数、调两个参数看起来毫无成本。但正因为看起来简单很少有人愿意为它建评测、做对比。而实际项目里影响 RAG 回答质量的变量非常多文档解析质量、Embedding 模型选型、检索 TopK、提示词模板、生成参数全都有影响。分块只是其中一个变量却往往是性价比最高的一个变量因为它能同时作用在检索精度和上下文质量上。我个人在实际操作中的体会是分块调优要当成一个持续迭代的过程每个文档类型都值得单独评估每轮改动都比对指标不要试图一劳永逸。另外最小评测集一定要从真实用户问题里来因为分块效果好坏的最终裁判永远是用户的问题有没有被准确回答。最后再分享一个小技巧在你还没有完整评测集的时候可以先把最容易出错的 10 个问题固定下来每次调整分块后跑一遍把检索结果全部打出来一条一条看人工对比。这个土办法看起来笨但它能帮你快速建立对分块行为的直觉。等你积累的数据足够多了再逐步升级到完整的自动评测体系。分块工程说到底拼的不是技巧而是你能多快发现自己切错了并且有依据地把它矫正回来。
阅读完成 · 觉得有帮助?