☰
RAG文本处理实战:从文档清洗到切分的完整指南
2026/10/18 4:39:09 网站建设 项目流程

在 RAG 这个方向上摸爬滚打了两三年,我的一个强烈感受是:真正决定项目能不能上线、回答准不准的,往往不是大模型选得有多新,而是喂给 RAG 系统的文本处理做得有多细。很多团队在最开始很兴奋地把文档丢进去、向量库里一存、接口一接,demo 阶段看着挺像回事,一旦换到真实业务数据,回答就开始答非所问、胡编乱造、漏信息。我 debug 过太多这种问题,最后定位到根因,基本都是文本处理环节出的岔子:要么原始文档没洗干净,要么切分切断了语义,要么元数据没做导致召回时无法过滤。

这里说的“RAG文本处理”,指的是从拿到原始文档开始,到可以把文本真正送进 Embedding 模型、向量检索、最终交给生成模型这一段路上所有对文本做的加工。它包含文档加载、格式解析、清洗、结构化识别、切分、元数据抽取、向量化前的文本规范化等一整套动作。这套动作做得好不好,直接决定了检索阶段能不能召回正确内容、生成阶段能不能引用到准确上下文。

这篇文章我不打算讲太理论的东西,而是想把我在真实项目中反复验证过的处理流程、参数选择、踩坑经验完整写出来。适合正在做 RAG 落地、被召回质量和回答准确率困扰的开发者,也适合刚接触 RAG、想一开始就把数据处理思路摆正的新手。看完之后,你可以直接照着这套思路去检查自己的处理链路,很多问题其实改几行处理逻辑就能解决。

1. 先理解RAG文本处理到底在解决什么问题

很多人把 RAG 想得很简单:文档切块 -> 向量化 -> 检索 -> 拼接给模型。其实这个链条里,文本处理不是一个一次性动作,而是分布在多个阶段、反复影响最终效果的关键变量。

1.1 RAG链路里,文本处理出现在哪几个环节

我习惯把 RAG 拆成离线处理和在线推理两个部分来看。

离线处理阶段,文本处理主要做四件事。第一是文档加载,把 PDF、Word、HTML、Markdown、表格、扫描件等各种格式变成可读的纯文本或半结构化文本。第二是清洗,去掉页眉页脚、水印、乱码、多余空行、特殊符号,同时尽量保留对语义有帮助的信息,比如标题层级和表格结构。第三是结构识别,识别出文档里的标题、章节、段落、表格、代码块,为后面的切分提供依据。第四是切分与元数据抽取,把长文档切成适合检索的片段,并为每个片段打上来源、章节、页码、文档ID等标签。

在线推理阶段,文本处理同样无处不在。用户提问进来之后要先做查询改写和问题规范化,比如把口语问题补全、把指代消解、把问句转换成更适合检索的表达。检索回来的文本块也不是直接就能拼给大模型的,要做去重、截断、按照相关性排序,有时还要做上下文压缩,把冗余内容去掉,只保留和问题相关的片段。

这两个环节经常被当成两套独立的东西来对待,实际上它们必须一起设计。离线切分的粒度、元数据的标签体系,直接决定了在线检索时的过滤能力和召回策略。我曾经见过一个项目,离线切分时完全没有保留图片、表格和对应页码,在线检索的时候用户问“第三页的那张表怎么解释”,系统根本无从下手。这就是典型的离线处理没有为在线查询场景留好接口。

1.2 为什么文本处理比模型选型更影响效果

大模型圈子更新迭代很快,大家很容易把效果差异归因于模型能力。可我在大量项目里观察到的情况是:同一个模型,换一套文本处理策略,回答质量的波动幅度相当大;而不同模型之间,只要文本处理链路做得扎实,差距反而没那么明显。

原因不难理解。RAG 的输出质量,本质上是“检索到的信息质量”和“生成模型的信息利用能力”共同决定的。检索到的信息如果本身就是残缺的、含噪声的、语义被切碎的,那后面再强的模型也只能在垃圾输入上做“精致的缝合”。反过来,如果检索回来的内容准确、完整、结构清晰,模型哪怕参数少一些,也能给出很靠谱的答案。

用一个生活化的类比来说,RAG 就像做菜。文本处理是买菜、洗菜、切菜的过程;向量检索是挑食材下锅;生成模型是掌勺的厨师。菜买回来带着泥、叶子烂了一半、切的时候又切得乱七八糟,厨师水平再高也炒不出一盘好菜。很多团队花大价钱请了“名厨”,却一直忽略前面那些不起眼的杂活,这在我看来是本末倒置了。

1.3 一套通用的处理流程长什么样

虽然不同业务场景差异很大,但 RAG 文本处理的主干流程是相对稳定的。我整理成了一条我反复使用的基线流程:

  1. 文档接入与格式识别:识别输入文件类型,选择对应的解析器。
  2. 内容提取:从 PDF、Word、HTML 等格式中提取正文和结构信息。
  3. 清洗与规范化:去噪、纠正乱码、统一字符、过滤无用内容。
  4. 结构分析:识别标题层级、段落边界、表格区域、代码块。
  5. 切分:基于结构或语义把文档切成检索单元,并设置合理重叠。
  6. 元数据抽取:为每个分块附加文档名、章节路径、页码、时间、类型等信息。
  7. 向量化:对分块文本做必要的规范化后,交给 Embedding 模型编码。
  8. 索引构建:写入向量数据库,并同步建立元数据过滤字段。

这八个步骤,每一步都有对应的工具和坑。下面我按环节逐个展开讲,重点说参数怎么选、为什么这么选、哪些地方容易翻车。

2. 文档加载与清洗:最脏的活决定了上限

我经常跟别人说一句话:RAG 项目里,最不值钱的环节是向量数据库选型,最值钱的环节是文档解析和清洗。因为向量数据库归根到底就是个存储和检索工具,而文档解析质量直接决定你能把哪些知识搬进系统。

2.1 不同来源文档的加载方式

先说说最常见的 PDF。PDF 在 RAG 里是最让人头疼的格式,因为 PDF 本身没有语义结构,只有坐标和渲染指令。一个 PDF 看起来是排版整齐的段落和表格,实际内部可能是无数个孤立文本框拼出来的。

简单场景,可以用 PyPDF2 或 pdfplumber 直接抽文本。pdfplumber 对规则排版的 PDF 效果不错,能比较准确地还原文字和表格。但它对多栏排版很无力,会把左右两栏的文字混在一起,切分之后语义完全错乱。遇到这种文档,我一般会先做页面布局分析,或者干脆用解析质量更好的专用工具,再进行 OCR 作为兜底。

扫描件或者图片型 PDF 没有文本层,必须先 OCR。OCR 这一环我踩过的坑太多,最典型的是中英文混排识别乱码、表格线被识别成文字、数学公式被识别成一堆符号。所以 OCR 之后的文本不能直接入库,必须接清洗。对表格型 PDF,如果 OCR 做不好,我会优先考虑用视觉模型直接抽取表格结构,而不是硬吃纯文本。

Word 文档相对友好,python-docx 可以提取段落和表格,并且能拿到标题样式。这里有一个很容易忽视的点:很多人用 docx 提取时只抽了段落文本,把页眉页脚和正文混在一起,或者把批注、修订痕迹也抽进来。清洗时必须把这些无关内容过滤掉。

HTML 网页转文本时,我习惯先解析 DOM 结构,只提取正文区域。用现成的 trafilatura 或 BeautifulSoup 都能做。关键是你得有站点级的正文识别规则,不能指望一个通用函数适配所有站点。

2.2 清洗规则怎么定:通用规则与场景规则

清洗并不是“越干净越好”,而是“在保留语义的前提下尽量去噪”。我自己会把清洗规则分成两类。

通用规则适用于所有文档,包括:统一换行符,Windows 和 Unix 的换行不一致会导致切分错乱;去掉零宽字符和其他不可见 Unicode 符号;压缩连续空行为段落边界;把全角半角标点在必要时统一;去掉常见的页眉页脚模式,比如“第 X 页 / 共 Y 页”、公司内部编号等。

场景规则则要根据业务来定制。比如我做过一个法律文档项目,需要保留“第X条”“第X章”这种结构标记,同时把条文序号和正文内容保持一致;做过一个设备说明书项目,需要把“警告”“注意”这类提示词和对应的内容绑定切分,不能拆开。

这里我想强调一点:清洗规则不要一上来就做得特别复杂。最好的做法是先加载一批真实样本,人工看 20 到 50 个文档的原始文本,把高频噪声列成清单,然后一条规则一条规则地加。每加一条规则,跑一遍全量数据,用抽样对比的方式判断有没有误伤正文。

2.3 保留结构化信息:表格、标题、代码块不能丢

很多人以为“文本处理”就是要得到一段连续的文字,这个理解在 RAG 场景下是有问题的。真实业务文档里,信息经常藏在表格、代码块、列表和嵌套标题中,这些结构本身就是语义的一部分。

拿表格举例。直接抽取表格文字,比如“名称 规格 数量 单价”,如果切分时被拆成两半,检索“某型号设备的单价”就会漏召回。我的做法是在解析阶段就把表格转成结构化的 Markdown 表格或“字段: 值”列表,让一行记录变成语义完整的一小块文本。这样切分时表格不太容易被拦腰截断。

代码块也一样。在技术文档 RAG 场景里,用户问的往往是“某接口怎么调用”“某配置怎么填”。代码块最好作为一个整体保留,不要把代码行按普通文本切碎。我的做法是识别代码块边界,优先把代码块完整放入一个分块,并补充一个说明性文本块描述它的上下文。

标题层级是另一项宝藏。保留“H1/H2/H3”的层级信息,既能帮助切分,也能在检索后返回时标注“这段内容来自哪个章节”。我在做索引元数据时,会把标题路径作为一种标签存进去,比如“设备维护 > 故障排查 > 网络连接异常”。用户问“网络连接异常怎么办”,即使正文里没有出现完整标题路径,也能通过元数据辅助召回。

顺便提一句调试经验:清洗逻辑做出来之后,一定要输出一份“清洗前 vs 清洗后”的对照样本用于人工检查。不要只看字符数和样例,很多问题只有在批量抽样时才会暴露。我用一个小脚本随机抽 100 条切分结果,定期人工复核,这成了我每个 RAG 项目的固定动作。

3. 切分策略:chunk size、overlap与结构感知切分

切分是 RAG 文本处理里最受关注、也最容易做错的一步。切得太大,检索时把很多无关信息带进来,稀释了问题相关的内容,还会超出模型的上下文限制;切得太小,语义不完整,一个知识点被切成好几截,召回时只能拿回残缺片段。所谓“增删改查”和“检索质量”之间的平衡点,基本都在切分这一步上体现。

3.1 固定长度切分的问题

新手最常见的方法是把文本按固定字符数切块,比如每 500 个字符一刀切。这种思路实现简单,但实际效果往往很糟糕。原因在于它完全不理解语义边界。一个自然段可能 300 字,也可能 1500 字,固定长度切分会把段落从中间劈开,导致分块里出现“上半段没有结论,下半段没有背景”的尴尬情况。

更隐蔽的问题是,固定长度切分对中文尤其不友好。中文没有天然空格,按字符数切很容易把完整的词、术语和数字表达式割裂。比如“深度学习”被切成“深度学”和“习”,向量化之后这两个分块的语义都会受损。

所以我很少拿固定长度切分当主方案,最多用它做兜底,或者在没有结构信息的纯文本上做临时处理。真正好用的是结构感知切分,下面重点说。

3.2 关键参数怎么选:chunk size、overlap、检索TopK

很多人喜欢找“最佳参数”,但我的经验是:不存在普适的最佳参数,只存在适合你数据集和业务场景的合理区间。合理的做法是先确定一个基线,再用一批标注问题来验证、调参。

我这里给出一个我常用的起点,适用于大部分中文业务文档:

  • Chunk Size:按字符数算,400 到 800 字符。如果按 token 算,大约 150 到 400 token。
  • Overlap:50 到 150 字符,约占 chunk 的 10% 到 20%。
  • 检索 TopK:20 到 50 个候选分块进入重排阶段,重排后取 5 到 10 个最终送入生成模型。

为什么 chunk size 选 400 到 800?因为大多数问题的答案就在一段或几段话里。太小的分块容易丢失上下文,太大的分块会让向量表征变得发散,检索出来的内容噪音多。这个区间是我在大量项目里试出来的比较稳的范围,但如果你处理的是合同条款、法律条文这类结构化很强的文本,分块可以稍小;如果是产品介绍、技术说明这种叙事性文本,分块可以稍大。

Overlap 的意义在于防止“边界信息丢失”。假设一个知识点正好落在两个分块的切缝处,没有 overlap 就可能两边都缺。设置 10% 到 20% 的重叠,能显著提升召回率。但 overlap 也不能太大,否则向量数据库里会有大量重复内容,检索结果里同一个信息出现多次,浪费上下文窗口还容易让模型产生冗余回答。

TopK 的选择要考虑生成模型的上下文长度。你不能只想着“召回更多更好”,还要看拼进 Prompt 之后放不放得下。我的习惯是,用重排模型先把 20 到 50 个候选压缩到 5 到 10 个,既保留高相关片段,又不至于把上下文撑爆。

3.3 结构感知切分实操

所谓结构感知切分,就是先识别文档结构,再在结构边界处切分。比如识别出 H2 标题后,把 H2 下面的所有内容作为一个大块,再根据段落进一步切分。

我常用的一种做法是基于 Markdown 标题层级做递归切分。思路是:先把文档转成带标题标记的结构化文本,然后从最高层级开始,如果某个一级标题下的全文长度超过了目标 chunk 大小,就进入该标题下的二级标题继续切;如果二级标题下还是太长,就按段落切;如果单段还超长,再按句子或固定长度兜底。

这种递归切分的好处是,每一个分块都能找到明确的“归属章节”,检索到之后你可以告诉用户“这个结论来自哪个章节下面”,而且切分边界通常落在语义相对完整的位置。

代码上大概长这样:

def recursive_split(nodes, max_chars=600, overlap=80): chunks = [] for node in nodes: text = node["text"] if len(text) <= max_chars: chunks.append({ "text": text, "meta": node["meta"] }) else: children = split_by_structure(node) sub_chunks = recursive_split(children, max_chars, overlap) chunks.extend(sub_chunks) return chunks

这里的关键在于split_by_structure的实现。不同文档类型对应不同的结构边界识别方式:Markdown 看#前缀,HTML 看标签层级,PDF 看字体大小和位置信息。实际落地时,我会先做一个抽象接口,不同解析器返回统一的节点结构,再交给递归切分器处理。

还有一种对检索很友好的做法,我习惯叫“父子分块”:父块是较大的段落或章节,子块是较短的语义单元,子块用于向量化召回,父块用于注入上下文。比如子块用 200 到 300 字符,父块用 800 到 1200 字符。检索时命中子块,返回父块给模型。这样做的好处是,既能通过小分块获得精准的相关性,又不丢失大上下文。

3.4 面向检索的切分技巧:上下文补全与语义切分

切分做得久了,你会发现纯按结构切还是不够,因为有时候一个自然段本身就有多层含义。于是会有一些更高级的玩法,比如“上下文补全”和“语义切分”。

上下文补全的做法是:切分时不只记录 chunk 自身文本,还把它前后的标题路径和一小段上下文一起拼进去,构造出一个“自包含”的分块。比如某个 chunk 里只写了“价格包含税费”,如果不知道开头说的产品名,这个 chunk 就是有歧义的。补全之后变成“产品A的报价方案:价格包含税费”。这类做法能显著提升向量检索时对短文本的理解力,尤其适合问答型场景。

语义切分则借助语言模型或句向量来做。通用做法是预测句嵌入向量,在相邻句子向量差异较大的位置作为切分点。这个思路对叙事性文档效果不错,但计算成本比较高,不适合在超大规模语料上跑。我的建议是:能靠结构边界解决的问题,优先用结构;只有结构不明显、且语义密集的文本,再上语义切分。

需要注意的是,任何切分算法都要配上“分块质量抽样检查”。我每次调完参数都会随机抽几十个分块,人工判断是否语义完整、边界是否自然、元数据是否正确。这个过程很枯燥,但比线上出了问题再回头排查效率高得多。

4. 向量化与索引:文本处理结果如何服务于召回

切分后的文本要变成向量才能进检索引擎。这一环节经常被简化为“调一下 Embedding 接口”,但实际做的时候,文本规范化、Embedding 模型选择、元数据构建、索引配置,每一个小决定都在影响后续检索效果。

4.1 Embedding模型选择与文本预处理

不同 Embedding 模型对文本格式的敏感度不一样。很多模型在训练时见过比较规整的文本,如果你塞进去的是脏乱差的片段,向量质量会明显下降。所以向量化之前的文本规范化很重要:统一标点、删除不可见字符、确保每个 chunk 有明确的开头和结尾。

模型选择上,我建议按语言和领域试,而不是只听评测榜单。中文场景下,常见的开源 Embedding 模型、商业 API 模型都可以作为候选。评估的时候不要只看公开 benchmark,要拿自己的语料构建一个小的“问题-相关文档”评测集,看检索 TopK 命中率。我自己一般会准备 50 到 100 条真实问题,人工标注标准答案落在哪些文档片段里,然后用 Recall@K 来对比不同模型。

另外要注意,Embedding 模型的输入长度是有限制的。切分时设了 800 字符,如果模型最大输入是 512 token,那么超出部分可能被截断,等于你切好的 chunk 被模型默默“重新切了一遍”。所以设置 chunk size 前,先查一下你打算用的 Embedding 模型支持多长输入。

4.2 元数据设计和过滤

我见过不少团队做 RAG 时,向量数据库里只存了文本和向量,元数据几乎没有。这在 demo 阶段看不出来,一上真实系统就麻烦。用户往往有强过滤需求,比如“只看 2024 年的合同”“只看某产品的说明书”“只看某一章节的内容”。没有元数据,这些过滤只能靠向量相似度硬扛,效果非常不稳定。

元数据的构建应该在切分阶段同步完成,而不是之后补。我通常为每个分块附加这些字段:

  • 文档ID和文档名
  • 章节路径和标题层级
  • 页码或文件偏移位置
  • 文档类型(说明书、合同、FAQ、代码示例)
  • 业务标签(产品线、地域、时间范围)
  • 分块的父子关系(父块ID、子块ID)

有了这些字段,在线检索时就可以先做条件过滤,再做向量检索,检索效率和准确度都会提升。比如用户问“去年华南区的政策”,如果文档里有地域和时间标签,向量检索前就可以直接过滤掉无关区域和年份的文档块。

元数据设计有个原则:只给高频过滤需求建索引字段,不要贪多。每多一个索引字段,写入成本、存储成本和维护复杂度都在增加。我会先和业务方确认几个高频查询条件,优先满足这些。

4.3 混合检索与文本重排

纯向量检索的问题在于,它对“精确词匹配”和“稀有实体”不敏感。两个文本语义相近,但关键实体名完全不同,向量相似度可能不够高。比如用户搜“付款条款”,文档里写的是“结算方式”,向量模型如果没训练到位,召回效果就会打折。

所以很多生产级 RAG 都用混合检索:向量检索 + 关键词检索(BM25),然后再用重排模型融合排序。BM25 对精确词命中很有用,特别适合专有名词、编号、型号这类场景。向量检索负责语义相似,BM25 负责字面匹配,两者互为补充。

重排模型是另一个容易被忽略的环节。前面召回的 20 到 50 个候选块,如果直接按向量相似度排序送入大模型,效果通常一般。因为向量相似度是一个粗粒度的相关性度量,不能精确区分“话题相关”和“能回答问题”。重排模型做得更精细,它会对待检索问题和候选文本做深度交叉编码,输出更准确的相关系数。我实际测下来,加一层重排能显著提升最终答案的准确性,代价是多一次模型推理,但这些延迟是可以接受的。

混合检索和重排的文本处理逻辑不难,关键是要保留切分时的文本元数据,因为重排后你需要重新定位到原始文本块和它在原文档中的位置。很多系统在这一步踩坑,都是因为离线切分时没保存“文本块ID到原始文档位置的映射”。

5. 常见问题与排查技巧实录

RAG 系统的调试是很繁琐的,但很多问题是有规律可循的。这一节我把自己和身边同行踩过的高频问题整理成速查表,再补几个我自己常用的排查技巧。

5.1 高频故障速查表

现象常见原因排查思路
回答明显漏信息切分块过小,语义被切断增大 chunk size,检查 overlap 是否生效
回答上下文混乱切分时把不同章节内容混入同一块检查结构识别是否准确,启用层级切分
检索召回明显不相关文档清洗过度或不足,噪声干扰向量抽样检查清洗前后的文本,修正规则
频繁提到“找不到相关信息”元数据过滤条件太严格查看过滤条件是否和用户问题不匹配
同一个问题每次回答不同检索到的候选块不稳定,重排波动固定重排温度,检查向量库是否多版本混用
长文档回答肤浅只检索到局部片段,没覆盖完整上下文用父子分块,检索子块后返回父块
表格内容答非所问表格被切分切断或转文本时结构丢失表格转 Markdown 或“字段: 值”,做整体分块
扫描件 PDF 乱码OCR 模型对版式复杂文档无能为力换更强 OCR 或视觉模型做版面分析

这张表我每次做项目都会打出来贴在工位上,很多反馈问题不用猜,对着表先排查一圈,基本能定位到 80% 的故障。

5.2 三个容易忽视的细节

第一个细节是“同义词和指代问题”。用户问“这个方法的缺点是什么”,文档里对应的其实是“本方案存在如下局限”。向量模型有时候能捕捉到语义相似,但有时不能。我的处理方式是做查询改写:在检索前对用户问题用大模型做一次轻量改写,扩展同义词、补充指代对象,再送进检索。这属于在线文本处理,很多团队没做,效果差异很大。

第二个细节是“切分文本时要注意标点符号的上下文依赖”。中文里一些标点,比如省略号、书名号、引号,会跨越多个句子,自动切分很容易把引用的开头和结尾切断。这种情况下,文本处理最好先识别成对符号,优先让它们落在同一个 chunk 内。

第三个细节是“向量化后的文本缓存”。同一批文本,如果多次向量化,结果可能因为模型版本不同而不一致。我习惯把向量化后的文本和向量一起缓存,并记录 Embedding 模型版本。升级模型时,只重新向量化受影响的分块,而不是全量重跑。这个习惯帮我省了大量算力,也避免了线上新旧向量混用的隐性问题。

6. 一些我在项目里反复用到的处理习惯

写到这里,最后再分享几个我在实际项目里沉淀下来的处理习惯。这些东西不算什么高深技术,但确实是我踩过多次坑之后才固化的流程。

第一,每个 RAG 项目开工前,我会先建一个 20 条左右的“黄金问题集”。这些问题来自真实用户,覆盖高频和疑难场景。后面所有文本处理改动,都要用这组问题去回归测试。没有这组问题,调参基本靠感觉,上线心里没底。

第二,所有清洗规则和切分参数都要做成可配置的,不要写死在代码里。最好的形式是有一份配置文件,规则按顺序执行、可以单独开关。因为真实数据总是在变,今天适用的规则,可能三个月后因为文档模板调整就不再适用。可配置化能让你快速验证新规则,而不需要改代码重新发布。

第三,重视文本处理链路的日志。离线处理时,我会为每个文档记录加载耗时、清洗去掉的字符量、切分产生的分块数、平均分块长度。这些数据平时不看,一旦线上出现批量异常,日志就是定位问题的第一手线索。

RAG 文本处理不是一个“做完就完事”的环节,它会随着数据形态变化、业务问题变化而持续调整。保持对数据的敏感,定期抽查分块质量,比盲目追求新模型更让我安心。希望这篇文章里这些来自实操的经验,能让你在搭建 RAG 的路上少走一些弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询