1. 为什么你的RAG系统检索总是不准?可能是分块方式错了
大家好,我是老方,在AI和智能硬件这块摸爬滚打了十几年。今天想和大家聊聊一个在搭建RAG系统时,很多人都会踩的坑:文档分块。你可能已经用上了最新的向量模型,花了大价钱搞定了GPU,但最后发现,系统检索出来的内容总是“答非所问”,或者上下文支离破碎。问题出在哪?很多时候,根源就在于最基础的文档分块策略上。
传统的分块方法,比如固定长度分块,简单粗暴。它不管你的文档结构,直接按字符数或者token数一刀切。想象一下,你有一篇技术文档,前面在讲“Java多线程的实现”,中间突然切到了“Spring Boot的配置”,最后又接上了“线程池的参数详解”。当用户问“如何配置线程池的核心参数?”时,系统检索到的可能只是被切碎的“Spring Boot的配置”片段,完全丢失了“线程池”这个核心上下文。这种“断章取义”是检索质量低下的主要原因。
另一种常见的思路是按段落或换行符分块。这比固定长度好一些,但对于结构复杂的技术文档,尤其是中文文档,依然不够。中文文档的段落可能很长,一个段落里包含了多个子主题;而且,很多从PDF转换来的文档,其段落结构在OCR识别后已经混乱了。更重要的是,标题所承载的层级和语义信息被完全丢弃了。“1.1 安装依赖”和“2.1 性能调优”显然是两个完全不同的语义单元,但按段落分块时,它们只是两个普通的文本块。
所以,我们需要一种更“聪明”的分块方式,它能理解文档的内在结构。这就是基于Markdown标题分块的核心思想:让分块的边界与文档的逻辑边界保持一致。一篇好的技术文档,其标题(#, ##, ###)天然就是内容章节的划分。我们以标题为锚点进行分块,就能确保每个块都是一个语义完整的单元,比如“## 2.3 数据库连接池配置”下的所有内容,就应该作为一个整体。这样做,在后续向量化时,这个块所表达的语义是连贯的;在检索时,系统也能更精准地定位到与问题最相关的完整知识模块。
2. 从混乱的PDF到结构清晰的Markdown:预处理是关键
理想很丰满,但现实是,我们的原料往往是五花八门的PDF文件,而不是规整的Markdown。直接对这些PDF进行分块,无异于在垃圾堆里找宝藏。因此,一个高质量的预处理流程至关重要。这一步的目标,是把非结构化的PDF文本,转换成结构清晰、带标题层级的Markdown文档。
2.1 PDF解析:选对工具,事半功倍
PDF解析是个技术活,特别是中文技术文档,里面可能有扫描图片、复杂的表格、代码块和水印。我的经验是,别自己从头造轮子,用成熟的方案更靠谱。这里我推荐两个我实测过的工具:
MinerU(开源,强烈推荐):这是我目前的主力工具。它是一个开源项目,提供了PDF解析的API,效果非常不错。对于常见的双栏排版、图文混排、表格,它的识别和还原能力都比不少商业服务要好。最棒的是,它提供了免费的在线API额度,对于个人开发者或小规模项目完全够用。如果你有服务器资源,也可以把它部署在自己的环境里,数据更安全。
阿里云智能媒体服务(商业API):如果你追求稳定和开箱即用,阿里云的文档解析服务是个不错的选择。它同样支持PDF转文字和OCR,对于大多数文档格式处理得都还行。不过根据我最近的测试(2025年初),它在处理复杂表格时,偶尔还是会出现格式错乱的情况,需要后期做一些校正。
我的选择建议是:优先尝试MinerU,如果遇到非常特殊的文档格式它处理不了,再考虑用商业API作为补充。把解析这一步做好,后面的路就顺了。
2.2 文本后处理:用大模型给文档“梳妆打扮”
从PDF里解析出来的文本,经常是“蓬头垢面”的。你可能会看到莫名其妙的页码“- 5 -”,OCR识别错误的字符“垵装”而不是“安装”,以及完全丢失的列表和代码块格式。这时候,就需要请出我们的大模型助手来做一次深度清洁和格式重建。
我习惯用通义千问的qwen-long模型来做这件事。为什么选它?因为它支持长达1000万tokens的上下文,意味着你甚至可以一次性扔进去一整本书让它整理,而且价格非常便宜。后处理的核心是给它一个明确的指令,让它扮演一个“格式优化专家”。
下面是我一直在用的一个提示词模板,你可以直接抄作业:
你是一个专业的文档格式优化助手。请严格遵循以下规则处理我提供的文本:
1. **核心原则**:只修正格式和明显的OCR错误(如“垵装”改为“安装”),绝不改变原文的技术含义和表述。
2. **结构化重建**:
- 识别原文中的标题,并将其规范化为Markdown格式。例如,将“第一章 概述”改为“# 概述”,将“1.1 背景”改为“## 1.1 背景”。
- 识别无序列表(如以-、*、•开头的行)和有序列表,为其添加或修正Markdown列表符号。
- 识别代码块,用 ``` 语言类型 将其包裹起来。
3. **噪音清理**:
- 删除所有明显的页码、页眉、页脚(如“第5页”、“Copyright 2023”)。
- 删除多余的空行,确保段落间只有一个换行符。
4. **输出要求**:直接输出优化后的完整文档,不要有任何额外的解释。在文档结尾,单独一行输出【DONE】。
把解析后的文本和这个提示词喂给qwen-long,它就能还你一个干干净净、结构分明的Markdown文档。这里有个小技巧:如果文档特别长,模型一次输出不完,你可以让它“继续输出”,并在你的代码里判断是否收到了【DONE】标志,然后将多次的输出结果拼接起来。
3. 核心实战:手把手实现基于Markdown标题的Java分块器
预处理得到了干净的Markdown,现在我们来啃最硬的骨头:实现分块器。我将结合代码,详细解释每一步的设计思路和容易踩的坑。
3.1 设计思路与效果预览
我们的目标是模仿LangChain中 MarkdownHeaderTextSplitter 的设计,但要用Java实现,并针对中文环境做适配。其核心逻辑是:遍历文档的每一行,识别标题行,并根据标题的层级关系,动态维护一个“标题栈”和“元数据映射”。
举个例子,对于下面这个Markdown:
# Java并发编程
本文介绍Java并发编程的核心概念。
## 线程基础
线程是操作系统调度的最小单位。
### 创建线程
可以通过继承Thread类或实现Runnable接口来创建线程。
## 线程池
使用线程池可以避免频繁创建和销毁线程的开销。
理想的分块结果应该是:
- 块1:
- 内容:
本文介绍Java并发编程的核心概念。 - 元数据:
{header_1: Java并发编
- 内容:

&spm=1001.2101.3001.5002&articleId=152619850&d=1&t=3&u=04498b2230274a1f903db4e60ad69fdf)
196

被折叠的 条评论
为什么被折叠?



