← 返回文章列表

RAG 知识库分段为什么切断了代码块:清洗输出尺寸与分段配置的对齐实测

基于 doc-cleaner v1.11.0 + Dify 1.16.x 实测(2026-09-10)。

📖 摘要:一次知识库体检里,1401 个分段中有 341 个是代码碎块(24.3%)——检索命中碎块,AI 拿到的就是半截内容。奇怪的是,这个库上线前专门做过一轮清洗。本文从碎块的形状倒推根因,定位到一条容易漏掉的链路:清洗合出来的长代码块,超过了目标库分段配置的 max_tokens(800),被分段器按长度硬切,切点落在代码围栏上。我们做了对照实验:结构感知切分把碎块率从 30.0% 降到 5.3%;剩下那 5% 追下去,是源文档里的围栏缺陷。文末给出可直接照用的三层门禁判据。

导读

一、业务场景:体检报告里那个 24.3%

给一套上线运行的知识问答应用做体检时,故障手册知识库的统计里有个数字很扎眼:

指标 数值
分段总数 1,401
其中代码碎块 341(24.3%)

「代码碎块」的判定口径很直接:一个分段里,代码围栏(三个反引号)出现奇数次。围栏成对是完整的代码块,出现奇数次意味着这个块被从中间切开了——前半截在上一段,后半截在这一段。

这个库不是没清洗过,恰恰相反:上线前我们专门做过一轮清洗,把「每行一个代码块」的碎片结构合并成大块。清洗脚本当时还留了一句注释:

合并长代码块后 Dify 分段仍可能切块(残片数不降反升属正常),但残片形态从碎渣变为代码切片——最终以 hit-testing 检索质量为准。

问题就出在这句话上:我们知道会切,但把它当成了"可接受的现象",没有当成"待办的问题"。 直到体检把这个数字摆到面前,才回头把它当问题解。

二、碎块的真实代价:不是难看,是答案少一半

先确认一件事:碎块到底有没有实际影响?我们用 5 个真实运维问题做了一次检索命中检查:

检查项 结果
命中分段总数 25
其中碎块 7(28%)
top1 命中就是碎块的题目 2 / 5

也就是说,近三成的命中内容是碎的,四成的提问第一顺位就撞上碎块

碎块的伤害有三个层次:

  1. 内容截断:一段命令行输出被切成两半,AI 拿到的上下文不完整,答案自然少一半——这是最直观的
  2. 语义污染:围栏被切开后,段落里会混入 text、孤立的 ``` 这类标记,它们进入向量与关键词索引,等于往语料里掺噪声
  3. 引用错位:诊断类应用要给出处(「依据手册第 N 段」),段是碎的,出处就指不准

对问答类应用来说,这是「答得自信,但依据是半截」的典型形态。

三、排查:从碎块形状倒推三条线

修之前先把碎块看清楚。341 个碎块按形态分类:

碎块形态 数量 说明
text 开头 188 围栏标记 ```text 被从中间切开——``` 留在上一段,text 落到这一段
以孤立 ``` 结尾 47 代码块结束围栏落在了段落尾部
其他含围栏 106 段内围栏不配对的其他形态
合计 341

text 开头这个细节很关键:它不是「内容散乱」,而是围栏与语言标记被从中间切断——说明切点落在围栏行上,是硬切造成的。

沿着这条线索往下倒推,三条线要一起看。

线一:源文档的围栏密度

排查容易犯的第一个错,是盯着「库里的分段不对」直接改分段。但分段是下游产物——它的输入只有两个:送进库的语料分段器的配置。要定位就得两头都看,而不是在结果上反复调。

先看语料这一头。原始转换产物(PDF/CHM 转 Markdown 的结果)里,故障处理手册 25 个文档,其中 MPLS 那一章 76,266 个字符里就有 1,502 个围栏行——也就是 751 个代码块,平均每块 1-2 行命令。

这个密度说明源头的形态是「一行命令一个代码块」。代码块密到这种程度,意味着任何「按长度切分」的动作都极可能切在围栏上——后面几个实验的数据会验证这一点。

线二:清洗做了什么、没做什么

清洗把相邻的碎片块合并了:同一章 1,502 个围栏 → 156 个,下降了 90%。这一步是对的——把「微碎渣」合并成了「大块」,检索时上下文更完整。

但合并带来一个新状态:清洗后的 817 个代码块里,有 113 个超过 800 字符,最长的一个 22,034 字符

清洗只做了「合并」这一半,缺了「按下游尺寸切分」的另一半。

线三:目标库的分段配置

最后看库的配置。直接读数据库里的分段规则(而不是靠观测值反推):

配置项 实际值
分段模式 custom(自定义)
分隔符 separator \n#(按 Markdown 标题切)
max_tokens 800
chunk_overlap 0

三条线拼起来,答案就出来了:

清洗合出来的长代码块(最长 22,034 字符)→ 远超分段器 800 token 的上限 → 分段器按长度硬切 → 切点落在代码围栏上 → 碎块。

顺带一个对照:Dify 的自定义分段默认值是 max_tokens 500 / overlap 50。这个库配的是 800——说明建库时是有意识调过的,只是没算准清洗输出的尺寸。

四、机制:分段器的两级切分,为什么必然切断代码块

把机制说透,才知道为什么「调大 max_tokens」不是完整答案。

分段器处理一个文档时是两级逻辑:

graph TD A["原始文档"] --> B["第一级:按 separator 切分(此处按标题)"] B --> C{"章节是否超过 max_tokens"} C -- "否" --> D["成为一个完整分段"] C -- "是" --> E["第二级:按长度硬切"] E --> F{"切点落在哪里"} F -- "普通文本" --> G["分段长短不一,内容仍完整"] F -- "代码围栏" --> H["围栏与内容分离,产生碎块"]

关键在于第二级切分是无差别的:它按字符(严格说是 token)长度切,不认代码块边界。

而第一级的 separator 是 \n#——代码块内部没有 Markdown 标题,所以超长代码块在第一级完全没有可切的锚点,只能一路走到第二级,被随机切一刀。

补充一点关于 chunk_overlap 的配置:这个库的 overlap 是 0,切断了没有任何重叠内容做补偿。即便设了 overlap,也只能缓解「上下文断档」,不能让围栏重新配对——碎块问题不能靠 overlap 解决。

五、修复与对照实验:30.0% → 5.3%

方向清楚了:与其让分段器随机切,不如我们在清洗阶段按它的尺寸主动切好,并且保证每个切片结构自包含。

为了量化效果,我们在清洗后的 25 个文档上做了一次对照实验——用脚本复现两种切分策略(上限取 700 字符作为 token 的近似口径,因为代码与英文的 token 密度高于中文):

策略 分段总数 围栏奇数的段(碎块) 碎块率 超限段
现状:按标题切 + 超限硬切 1,272 381 30.0% 0
修复:结构感知切分 1,447 76 5.3% 2

「结构感知切分」做的就是三件事:

  1. 代码块整体优先:扫描时不把围栏行当普通文本,整块保留,避免从中间切开
  2. 超长块主动切:超过上限的代码块,在语义边界(空行处)切开,每一片补上完整的起止围栏,让它自包含
  3. 普通文本也控长:非代码的长段落同样按上限切,避免它们成为下一个被硬切的对象

实验里现状策略的碎块率是 30.0%,与线上库实测的 24.3% 同量级——说明这个实验确实复现了线上现象,后面的改进数据也就有参考价值。

核心的一行是「每一片补上完整围栏」:碎块的根源是围栏不完整,那么切分时必须保证每个切片都自带一对围栏——哪怕内容被拆开,每片单独看仍是合法代码块。

六、验证:调大 max_tokens 到底有没有用?

排查到这里,最自然的想法是「把 max_tokens 调大不就行了」。我们没有直接采纳,而是先做了一次对照:在同样的 25 个文档上,把切分上限从 500 一路调到 2000,看两种策略各自的表现。

切分上限 现状:按标题切 + 超限硬切 结构感知切分
500 482 碎块 / 1,659 段 = 29.1% 71 碎块 / 1,939 段 = 3.7%
700 381 碎块 / 1,272 段 = 30.0% 76 碎块 / 1,447 段 = 5.3%
1,000 276 碎块 / 993 段 = 27.8% 83 碎块 / 1,090 段 = 7.6%
1,500 214 碎块 / 775 段 = 27.6% 83 碎块 / 826 段 = 10.0%
2,000 169 碎块 / 669 段 = 25.3% 87 碎块 / 687 段 = 12.7%

(口径同前:以上限 700 字符近似 token。)

这张表有两个结论。

第一,调大上限几乎没用。 上限从 500 提到 2,000、翻了 4 倍,现状策略的碎块率只从 29.1% 降到 25.3%——切得少了,但该切的地方照样切。因为问题的性质不是「上限太小」,而是「切点不受控」。

第二,为什么调多大都不够? 看清洗后那 817 个代码块的尺寸分布就明白了:

分位 块大小
中位 186 字符
p75 440 字符
p90 1,093 字符
p95 1,784 字符
p99 3,952 字符
最长 22,045 字符

对应到各个上限,必定被切的块有多少:

切分上限 超限块数(共 817 块)
500 179(21.9%)
1,000 87(10.6%)
2,000 29(3.5%)
5,000 4(0.5%)

尺寸分布的长尾决定了:只要上限是个有限值,总会有块装不进去。所以正确方向不是「把上限调到一个神奇数值」,而是两头同时做——配置上把上限设定在「绝大多数结构块都装得下」的水平(大致覆盖到 p90-p95),源头上把长尾里的超长块按语义切开并保证每片结构自包含。

表里还有个反直觉现象值得说:结构感知切分的碎块率随上限升高反而上升(3.7% → 12.7%)。原因不在切分逻辑,而在段落变大之后,兜进的「源文档脏围栏」更多——这部分不是切分能解决的,正好是下一节的内容。

七、剩下 5% 的真相:源文档层的围栏缺陷

从 30% 降到 5.3% 之后,我们没有就此收工——剩下的 76 个碎块追下去,发现了第三层问题。

先排除一个可能:不是围栏数量不配对。25 个清洗后文档的围栏总数是 1,634(全部为偶数),配对正常。

真正的原因是「围栏行不干净」:

缺陷形态 实际样例 数量
围栏后跟正文(同一行) ``` (2) 查看日志文件的数目和名称。 154 处,涉及 22/25 个文档
围栏嵌在正文行中间 1021104 KB total (421552 KB free) ``` • 查看主设备备用主控板的 logfile 日志 同上(同一批缺陷的另一种表现)

这类缺陷来自 PDF 转 Markdown 环节:原文里的代码块边框与相邻正文,在转换时被合并进了同一行。

它有两个后果:

  1. 围栏识别失败:切分逻辑按「行首是围栏」来判断代码块,这些围栏藏在行中或带尾巴,识别不到,于是代码块边界算错
  2. 内容被吞:围栏行后面的正文,容易被当成围栏的一部分丢弃或错位

所以完整结论是:

切分层的改进能把碎块率从 30% 降到 5%;剩下那 5% 必须回到源文档层解决——围栏行必须在清洗阶段就清理干净。

这也解释了为什么单靠调分段参数修不好这个问题:脏的是上游,不是配置。

八、启示:三条可复用的判据

把这次排查收成三句话,都是可以直接照用的判据。

一、清洗的输出尺寸,由目标库的分段配置决定,不由源文档的格式决定。

动手清洗前先做一件事:读目标库的分段配置(max_tokens / separator / chunk_overlap),拿到数字再决定把内容切成多大。清洗的输出是分段的输入,尺寸契约必须跨环节传递——两边各做各的,就会出这次的碎块。

二、入库后要有一道结构门禁,而不是只看「洗得干不干净」。

我们已有的清洗质量门禁管的是「格式/内容/结构/下游」四个层面的洁净度,碎块这件事说明还缺两项针对分段结果的判据:

门禁项 判据 达标线
结构完整性 围栏配对数(段内三个反引号个数是否为偶数) 奇数围栏的段 = 0
尺寸对齐 切片长度与目标 max_tokens 的对照 超限结构块 = 0
围栏规范 围栏行是否独占一行(不夹带正文) 夹带正文的围栏行 = 0

三、把「已知问题」记成待办,不要记成「可接受的现象」。

这次的根子其实在 8 月那轮清洗:脚本注释里明确写了「分段可能切块,残片数不降反升属正常」——当时判断「以检索质量为准」,于是这个已知现象被放过去了。复盘下来更该做的是:既然已经预见到会切,就在清洗阶段把它切好,而不是留给下游随机切,再拿「检索质量为准」当验收口径。

顺带说一句,这个问题不只在 Dify 上存在。任何「按标题/段落切分 + 超长硬切」的 RAG 分段器都有同样的行为——只要你的语料里有长代码块、长表格、长日志,就会有这个风险。差别只在于你有没有去查过自己的碎块率。

常见问题

碎块率多少算正常?

看两个判据比看百分比更有意义:围栏配对数(奇数围栏的段应该为 0)和超限块数(超过 max_tokens 的结构块应该为 0)。如果这两项达标,碎块率自然趋近于 0。经验值上,带大量代码块的语料,碎块率控制在 1% 以内是合理目标——这次的 24.3% 属于明显异常。

把 max_tokens 调大是不是就能解决?

解决不了。有一类块是「调多大都不够」的:这次清洗后有 113 个代码块超过 800 字符,最长的一个 22,034 字符——按 800 算要 27 段,按 2000 算还要 11 段。只要块本身超过上限,就一定会被切。正确做法是两头一起做:源头上把超长块按语义切开并保证结构自包含,配置上把 max_tokens 设为「绝大多数结构块的尺寸」而不是「最大的那个块」。

已经上线运行的知识库,怎么快速判断有没有这个问题?

三步:① 取库的分段长度分布,看最长段是否刚好卡在一个上限值(如 799、1023 这类数字,说明存在硬切);② 统计分段中三个反引号出现奇数次的段数(这就是碎块数);③ 抽 5-10 个真实问题做命中检查,看 top1-top3 里碎块占比。三步都不需要动数据,只读不写——先确诊,再决定要不要重建。


相关文章:知识库数据清洗后,怎么知道洗得干不干净?一套三层质量门禁实测(清洗完的质量判据)|RAG 知识库建库前,数据到底该怎么清洗?一条可复用的清洗管线实测(清洗管线的完整流程)|Dify 知识库三种分段模式实测:通用、父子、Q&A 到底怎么选?(分段模式的选择)

本文基于真实项目交付经验撰写(doc-cleaner v1.11.0 清洗管线 + Dify 1.16.x 建库环境)。文中数据均来自我们自己的实测记录:知识库分段统计(1,401 段 / 341 碎块)、25 文档对照实验、源文档围栏缺陷统计,以及 5 个真实问题的命中检查。对照实验为脚本复现分段器行为,切分上限使用字符数近似 token(代码与英文的 token 密度高于中文),结论以同口径对比为准;理论与推断部分已标注边界,不构成任何平台的官方结论。

这些 AI 应用能力,如何交付到真实业务场景?看看方案与服务 →
本系列 · Dify 实战
  1. Dify Agent 应用实战:Beta 版「真 Agent」的能力边界实测
  2. Dify workflow 与 Hermes Agent skill 的确定性对比
  3. Dify 意图分类节点总翻车?从 33% 失败率到兜底不崩——可靠性与韧性的三层加固
  4. Dify 标注回复实战:让智能客服记住人工答案的纠错闭环
  5. Dify 知识库元数据过滤实战:检索噪声 75% 降到 0 的确定性闸门
  6. RAG 建库,如何自动设置分段模式
  7. RAG 知识库分段为什么切断了代码块:清洗输出尺寸与分段配置的对齐实测
  8. RAG知识库,如何进行持续更新运维
  9. RAG知识库的元数据过滤能力边界
  10. 数据库里的结构化数据,怎么建立 RAG 知识库?两条路线与选型判断
  11. 流程卡在「等人审批」?把审批链接送到企业微信和邮箱
  12. Dify 1.17 升级实测(一):从工作流平台到 Agent 平台,升级前必须知道的 5 件事
  13. Dify 应用上架门户:分享页每次回答都挂着内部流程节点?一个字段关掉
  14. RAG 知识库交付实战(上):4277 页手册喂给 AI——从凌晨故障到三模块方案
  15. RAG 知识库建库前,数据到底该怎么清洗?一条可复用的清洗管线实测
  16. 我们的门户机器人,为什么用 Dify 答、不把 skill 搬上云端 Hermes?
  17. 知识库从需求到交付:清洗、入库、维护全流程,照着走、每一步都能验证
  18. Dify 1.17 升级实测(二):Agent V2 节点与技能包实测——配置在数据库,不在 DSL
  19. RAG 知识库交付实战(中):三大深坑与修复实录——流程图截断/限流风暴/并联污染
  20. DeepSeek 思考模式什么情况下可以关?一次空输出事故的排查实录
  21. Dify 1.17 升级实测(三):循环内人工审批与图片直传实测——两个高频场景的新解法
  22. Dify 定时触发(trigger-schedule)实测:工作流到点自动跑,和三个必须知道的坑
  23. Dify 知识库三种分段模式实测:通用、父子、Q&A 到底怎么选?
  24. Dify 知识库接入 Notion/网页:先搞清三件事,再谈清洗
  25. RAG 知识库交付实战(下):18 条用例与成本测算
  26. 知识库数据清洗后,怎么知道洗得干不干净?一套三层质量门禁实测
  27. Dify 实战:供应商报价单格式五花八门,AI 怎么知道哪列是单价?

联系我

邮箱contact@fishsun.cn

点击邮箱直接写信 · 扫码加微信沟通

微信

微信二维码

扫码加微信 · 备注「门户」更快通过