乐于分享
好东西不私藏

RAG系统中的Markdown文档分割策略:从实现到效果分析

RAG系统中的Markdown文档分割策略:从实现到效果分析

今天看Ruoyi-vue-pro项目,发现其中AI子模块中添加了对Markdown文档的分块功能,特别是针对QA格式的Markdown文档,设计了专门的切片器(MarkdownQaSplitter)。在此前搭建基于Redis的知识库过程中,当时就比较好奇如何对文章进行合适的拆分,从而得到较好的召回效果。

基于Redis 8实现RAG工程实战(一):无GPU搭建可用的向量检索方案全景

基于Redis 8实现RAG工程实战(二):Qwen3-Embedding在CPU上的向量化落地

基于Redis 8的RAG工程实战(三):向量检索与全文检索的混合召回

基于Redis 8实现RAG工程实战(四):诗词数据清洗与繁简转换实现

现在正好通过阅读项目中的实现代码,结合测试结果,分析下不同分块策略的设计思路、实现细节。

一、背景:为什么文档分块是 RAG 的关键环节?

RAG(Retrieval-Augmented Generation,检索增强生成)系统的核心流程可以概括为三个阶段:

知识索引(Indexing) → 知识检索(Retrieval) → 内容生成(Generation)

知识索引阶段,原始文档被拆分为适当大小的片段(Chunk),然后通过 Embedding 模型转换为向量并存储到向量数据库中。当用户提问时,系统在向量数据库中进行相似度搜索,找到最相关的知识片段,再将其注入LLM的 Prompt进行回答。

文档分块是连接”原始文档”和”向量检索”的桥梁,直接决定了:

  1. 检索精度:分块是否语义完整,决定了Embedding向量能否准确表达内容含义
  2. 回答质量:注入Prompt的知识片段质量,直接决定了LLM回答的准确性和完整性
  3. 系统效率:分块数量影响向量存储成本和检索计算开销

二、项目中的分块策略体系

在Ruoyi-vue-pro的AI模块中,文档分块策略通过枚举AiDocumentSplitStrategyEnum定义了五种策略:

publicenum AiDocumentSplitStrategyEnum {    AUTO("auto""自动识别"),    TOKEN("token""Token 切分"),    PARAGRAPH("paragraph""段落切分"),    MARKDOWN_QA("markdown_qa""Markdown QA 切分"),    SEMANTIC("semantic""语义切分");}

2.1 自动检测机制(AUTO)

当策略设为AUTO时,系统会通过detectDocumentStrategy()方法自动分析文档特征,选择最优策略:

private AiDocumentSplitStrategyEnum detectDocumentStrategy(String content, String url){if (StrUtil.isEmpty(content)) {return AiDocumentSplitStrategyEnum.TOKEN;    }// 1. 检测 Markdown QA 格式if (isMarkdownQaFormat(content, url)) {return AiDocumentSplitStrategyEnum.MARKDOWN_QA;    }// 2. 检测普通 Markdown 文档if (isMarkdownDocument(url)) {return AiDocumentSplitStrategyEnum.SEMANTIC;    }// 3. 默认使用语义切分return AiDocumentSplitStrategyEnum.SEMANTIC;}

检测优先级:Markdown QA 格式 > 普通 Markdown > 默认语义切分。

2.2 QA 格式检测规则

系统通过isMarkdownQaFormat()判断文档是否为 QA 格式:

privatebooleanisMarkdownQaFormat(String content, String url){// 文件扩展名判断:必须是 .md 文件if (StrUtil.isNotEmpty(url) && !url.toLowerCase().endsWith(".md")) {returnfalse;    }// 统计二级标题数量long h2Count = content.lines()            .filter(line -> line.trim().startsWith("## "))            .count();// 要求一:至少包含 2 个二级标题if (h2Count < 2) {returnfalse;    }// 要求二:H2 标题占比超过 10%long totalLines = content.lines().count();double h2Ratio = (double) h2Count / totalLines;return h2Ratio > 0.1;}

判定条件:

  • 文件扩展名为 .md
  • 二级标题(##)数量 >= 2
  • 二级标题行数占总行数比例 > 10%

三、核心切片器实现详解

3.1 MarkdownQaSplitter — QA专用切片器

设计思想:QA文档的特点是”问题-答案”成对出现,二级标题(##)通常就是问题本身。保持问答对的完整性,是保证检索质量的关键。

核心算法

Step 1: 使用正则匹配所有##标题,解析出[问题, 答案]对列表Step 2: 对于每个QA对:  - 如果Token数 <= chunkSize → 保持完整,作为一个片段;  - 如果Token数 > chunkSize → 智能切分答案,但每个片段都保留完整问题。

智能切分答案的三级策略

  1. 段落级切分:以 \n\n 为分隔符,将答案拆分为段落,按Token限制合并;
  2. 句子级切分:当单个段落超过Token限制时,以 。!?.!? 为分隔符按句子拆分;
  3. 强制切分:当单个句子超过Token限制时,强制截断。

关键特性:长答案被切分后,每个子片段都会重新拼接完整的问题标题,确保每个片段都是独立可理解的。

3.2 SemanticTextSplitter — 语义切片器

设计思想:对于非QA格式的文档(如技术博客、API文档),需要在保持语义完整性的同时合理控制片段大小。

核心算法

Step 1: 检查文本是否超过chunkSize,不超过则直接返回Step 2: 按分隔符优先级尝试切分:  - \n\n\n(三个换行)  - \n\n(双换行)  - \n(单换行)Step 3: 如果没有换行符,按句子边界切分(句号、问号、感叹号)Step 4: 合并小片段,控制Token数不超过chunkSize

重叠机制(Chunk Overlap)

这是SemanticTextSplitter的重要特性。在合并片段形成Chunk时,当一个新的Chunk完成后,会将末尾的片段”重叠”到下一个Chunk的开头:

private List<String> getOverlappingChunks(List<String> chunks, String separator){// 从后往前取片段,直到达到chunkOverlap的Token数    List<String> overlapping = new ArrayList<>();int tokens = 0;for (int i = chunks.size() - 1; i >= 0; i--) {        String chunk = chunks.get(i);int chunkTokens = tokenEstimator.estimate(chunk);if (tokens + chunkTokens > chunkOverlap) {break;        }        overlapping.add(0, chunk);        tokens += chunkTokens;    }return overlapping;}

有趣之处在于,它通过重叠机制保证了相邻Chunk之间的上下文衔接,避免关键信息被硬分割截断。尽管这种做法会在多个Chunk中产生一定的信息冗余,但是我认为在实际的RAG场景中,这种冗余是值得的。

递归切分:当单个片段超过chunkSize时,会递归调用splitTextRecursive() 进行更细粒度的切分,直到所有片段都在限制范围内。

3.3 其他策略

  • TOKEN:使用Spring AI框架提供的 TokenTextSplitter,按Token数量机械切分
  • PARAGRAPH:本质上是 SemanticTextSplitter 的特例(overlap = 0),按段落边界切分但不产生重叠

四、Token 估算器

两种切片器共享相同的 Token 估算逻辑:

privateintestimate(String text){int chineseChars = 0;int englishWords = 0;for (char c : text.toCharArray()) {if (c >= 0x4E00 && c <= 0x9FA5) { // CJK 统一汉字            chineseChars++;        }    }    String[] words = text.split("\\s+");for (String word : words) {if (word.matches(".*[a-zA-Z].*")) {            englishWords++;        }    }return chineseChars + (int) (englishWords * 1.3);}

估算规则:

  • 中文:1 个汉字 ≈ 1 个 Token
  • 英文:1 个单词 ≈ 1.3 个 Token

这是一个轻量级估算,适用于分块场景的粗略控制,实际 Token 数取决于具体使用的 Embedding 模型的 Tokenizer。


五、分块效果分析

为了直观感受两种切片器的差异,我用一段典型的 FAQ 文档来演示。假设 chunkSize = 500

## 什么是 Spring Boot?Spring Boot 是一个基于 Spring 框架的快速开发框架,它简化了 Spring 应用的初始搭建和开发过程。通过约定优于配置的理念,Spring Boot 让开发者能够快速创建独立运行的、生产级别的 Spring 应用程序。Spring Boot 的核心优势在于它提供了大量的自动配置,大大减少了开发者的配置工作。同时,它还内置了 Tomcat、Jetty 等 Web 服务器,使得开发者无需部署 WAR 包即可运行应用。## Spring Boot 和 Spring Framework 有什么区别?Spring Framework 是一个基础框架,提供了依赖注入、AOP、事务管理等核心功能。Spring Boot 则是在 Spring Framework 基础上构建的快速开发框架。主要区别包括:1. Spring Boot 提供了自动配置,而 Spring Framework 需要手动配置。2. Spring Boot 内置了 Web 服务器,而 Spring Framework 需要手动部署。3. Spring Boot 提供了起步依赖,简化了依赖管理。4. Spring Boot 提供了生产级别的监控和管理功能。

5.1 MarkdownQaSplitter 的切分结果

MarkdownQaSplitter 会将每个问答对切为一个独立片段:

片段 1(~144 tokens):

## 什么是 Spring Boot?Spring Boot 是一个基于 Spring 框架的快速开发框架,它简化了 Spring 应用的初始搭建和开发过程...Spring Boot 的核心优势在于它提供了大量的自动配置...

片段 2(~135 tokens):

## Spring Boot 和 Spring Framework 有什么区别?Spring Framework 是一个基础框架...主要区别包括:1. Spring Boot 提供了自动配置,而 Spring Framework 需要手动配置。...

每个片段的Token数远低于500的限制,问答对完整保留。这样的片段在Embedding时,问题标题(如”Spring Boot 和 Spring Framework 有什么区别?”)会成为强语义信号,当用户提问”Spring Boot跟Spring有啥不一样”时,检索命中率很高。

5.2 SemanticTextSplitter的切分结果

同样的内容,SemanticTextSplitter会按段落边界合并成一个片段:

片段 1(~500 tokens):

## 什么是 Spring Boot?Spring Boot 是一个基于 Spring 框架的快速开发框架...(完整内容)## Spring Boot 和 Spring Framework 有什么区别?Spring Framework 是一个基础框架...主要区别包括:1. ... 2. ...

虽然只有1个片段(因为总Token未超过限制),但可以想象:当文档更长、主题更多时,SemanticTextSplitter会在段落边界处切分,并通过重叠机制将上一个片段末尾的内容带入下一个片段开头,确保跨片段的信息不会丢失。

5.3 两种策略的核心差异

维度
MarkdownQaSplitter
SemanticTextSplitter
切分粒度
每个问答对一个片段
按段落/句子合并
上下文冗余
极低(仅保留问题标题)
适中(相邻片段有overlap)
检索信号
问题标题自带强语义
依赖 Embedding 的语义匹配
适合场景
FAQ、问答集
技术文档、博客文章

简单来说,QA切片器”按问题切片”,语义切片器”按段落切片”。前者对精确问答场景效果更好,后者对通用的长文档更友好。


六、策略选择建议

6.1 不同文档类型的推荐

文档类型
推荐策略
理由
FAQ / 问答集
MARKDOWN_QA
问题标题是天然检索关键词
技术博客 / 教程
SEMANTIC
重叠机制保持上下文连贯
API参考文档
SEMANTIC 或 PARAGRAPH
每个API端点通常是一个独立段落
法律合同 / 政策
PARAGRAPH
条款之间独立性较强

6.2 关于自动检测的一个问题

当前QA格式检测要求”二级标题占比超过10%”,但是这个阈值可能过高。对于一个典型的FAQ文档,虽然每个问题都以二级标题开头,但答案部分通常会占据大量行数,导致H2占比很难达到10%。此外,例如技术文档可能包含大量的代码块、列表等非文本内容,这些内容会进一步降低H2的占比。这一点上或许可以进行更为细致的调整。


七、总结

回到最初的好奇——如何对文章进行合适的拆分以获得较好的召回效果?通过分析Ruoyi-vue-pro中的实现,我的理解是:

文档分块的核心原则是”保持语义完整”。不同的文档有不同的语义结构:FAQ文档的语义单位是问答对,技术文档的语义单位是段落或章节。MarkdownQaSplitterSemanticTextSplitter正是针对这两种结构设计的专用切片器,前者”按问题切”,后者”按段落切”。

在实际RAG流程中,优秀的分块策略能提升向量检索的命中率,减少无关信息的注入,帮助LLM生成更准确的回答。而自动检测机制detectDocumentStrategy()让系统可以根据文档特征自动选择最优策略,降低了使用门槛——前提是检测规则本身需要足够准确,这也是当前实现还有优化空间的地方。