很多团队做RAG(检索增强生成)应用,把大量精力花在调prompt、换embedding模型、上重排序器上,但最终效果还是不稳定——召回的内容驴唇不对马嘴,生成的答案东拼西凑。我排查过不少这种项目,最后发现问题几乎都集中在同一个环节:RAG文本处理。
这里说的文本处理,指源文档进入向量库之前的所有准备动作:格式解析、噪音清洗、分块切分、元数据提取。这个环节决定了模型“能看到什么”,而模型只能基于喂进去的内容作答。这一步做得粗糙,后面不管模型多强、向量多准,都是在沙地上盖楼。这篇文章按我在几个项目里沉淀下来的流程,把RAG文本处理拆开讲一遍:清洗规则怎么定、分块参数怎么算、元数据该提取哪些、效果怎么验证。适合正在搭RAG、以及已经搭完但检索质量始终不理想的人参考。
1. 先看RAG链路里文本处理卡在哪一环
1.1 一次需要“动手翻原始文档”的检索失败
先说个真实场景。某次我做一套企业内部制度问答系统,源文档是几十份PDF格式的员工手册,内容本身很规整,标题层级清晰,正文段落完整。系统上线后,我随手测试了一个问题:“离职流程中需要哪些签字环节”,结果召回回来的排名靠前的内容,是一段PDF里被截断的半句话,后面的文字全部丢失,那个片段还因与“签字环节”这几个字高度重合拿到了比较高的向量相似度,真实的制度流程正文反而排到了很后面。
打开原始PDF人工核对,发现那页在转换时出现了换行粘连,一个完整段落被拆成了七八行碎片,其中几行在嵌入时产生了错误语义关联。这个问题的根源不在embedding模型,也不在检索参数,而在文本处理阶段的文本转写质量——转出来的文本本身带有结构噪声,后续检索模型就被带偏了。
这种问题在RAG项目里非常普遍。很多人遇到检索效果差,第一反应是换更大的模型、加rerank,却忽略了一个基本事实:如果进入向量库的文本本身就是残缺的、带噪声的、切分不合理的,后续任何“豪华”环节都只能放大这些缺陷,而不是弥补它。
1.2 文本处理在完整链路中的具体边界
一条标准的RAG链路大致是:源文档采集 → 格式解析 → 文本清洗 → 分块切分 → 向量化嵌入 → 写入向量库 → 检索召回 → 重排序 → 送入大模型生成。严格说,RAG文本处理覆盖的是从“格式解析”到“向量化之前”的这一段,核心职责是控制进入embedding模型的文本质量。
为什么这一段那么关键?因为embedding模型对输入长度有上限(常见的有512、1024、8192个token等),源文档不能整本塞进去,必须切块;而切出来的每一块,都会独立变成一个检索单元。检索的时候,系统是在这些小块里找与问题最相似的块,而不是看整份文档。所以每一块的质量、每一块的边界是否合理、每一块是否携带足够的上下文信息,直接决定召回命中率。
我经常用一个比喻:RAG像图书馆里的资料外借服务,embedding模型是馆里的分类索引,而文本处理则是决定“装订成册的方式”——如果装订时把一页内容撕成两半分到不同册子里,或者某一册里混入了大量空白页、重复页、页码水印,那读者根据索引来找资料,借到的自然就是残缺的、混杂的册子。
明确了边界,下面逐个环节展开。先说清洗,这是最琐碎、最不性感、却最容易拉开项目差距的一步。
2. 文本清洗:先清解析层错误,再处理格式噪声,最后做内容归一
2.1 源文档里常见的三类“脏东西”
我先汇总一下在真实项目里最常见的文本噪声,按来源分三类,每类给两个高频案例。
第一类是解析层错误。最常见的是PDF转文本时产生的换行错乱——英文文档里单词被硬断行,中文文档里段落被拆成行级碎片;其次是编码问题,比如UTF-8文件带BOM头,解析出来第一行多出不可见字符;还有从网页保存下来的内容带了HTML实体( 、&),某些数据库导出文件里夹杂控制字符。
第二类是排版残留。页眉页脚、页码、文档标题重复出现,目录页和正文重复内容,Markdown文档里的图片引用文本、链接地址,表格被转换后的制表符和错位,PDF里多栏排版解析后文字顺序错乱。
第三类是语义污染。这最隐蔽——有些文本内容本身是“噪声”,比如免责声明、模板条款、版权信息,在每页底部反复出现。这类内容如果不处理,会占据大量向量库容量;更麻烦的是,它们在某些检索场景下会以高词频命中问题关键词,把正确答案挤出TopN。曾有一次,我见到一份合同的检索结果前列全是“兹证明”“特此声明”“本协议一式两份”这类套话片段,几乎瘫痪了问答效果。
2.2 清洗顺序与规则优先级
清洗不是把所有规则一次性堆上去,而是有优先级的。我自己的实践顺序是:
- 先修解析层硬错误——乱码、编码、不可见字符。这些不清理,后面的规则很多会匹配错。
- 再处理格式噪声——页眉页脚、重复标题、链接残留、多余空白。
- 最后做内容归一——全角半角统一、多余空行压缩、肢体语言短语整理(如果有需要)。
为什么这个顺序重要?因为格式清理规则往往依赖“正常文本”的形态。如果文本里还残留着一堆乱码和控制字符,页眉页脚识别规则容易被干扰;如果编码没修好,后续所有基于正则的清洗都可能把合法内容误删。
还有一个原则我从踩坑里总结出来:清洗规则宁少勿多。有人喜欢堆几十条规则,结果某条规则误伤了一批正常文本,排查起来非常痛苦。我一般把规则控制在15-20条以内,并且每条规则都在一个保留的“样本池”上跑回归测试——清洗前后各抽样一部分文本做对比,肉眼确认没有误伤。
2.3 一组可落地的清洗示例
下面给一组比较实用的清洗规则示例,按上面说的优先级组织,代码用Python写,只做示意。
import re import unicodedata def clean_text(text: str) -> str: # 1. 去除控制字符和不可见字符,保留换行和制表符 text = re.sub(r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]", "", text) # 2. 去BOM头 if text.startswith("\ufeff"): text = text.lstrip("\ufeff") # 3. 统一换行符 text = text.replace("\r\n", "\n").replace("\r", "\n") # 4. 压缩连续空行 text = re.sub(r"\n\s*\n", "\n\n", text) # 5. 去除常见页眉页脚(按实际项目频率动态调整) # 示例:页码模式 "第 1 页 共 3 页" 或 "- 12 -" text = re.sub(r"-?\s*\d+\s*-\s*|第\s*\d+\s*页\s*共\s*\d+\s*页", "", text) # 6. 去除HTML实体残留 text = re.sub(r" |&|<|>|"", " ", text) # 7. 全角转半角(保留中文标点) text = unicodedata.normalize("NFKC", text) return text这个示例只解决通用问题。生产环境里,清洗规则一定要针对实际语料定制——我见过最有效的方法是:随机抽取30-50份文档,人工看一遍解析后的原始文本,把出现的噪声类型列成清单,再逐条写规则。比如,某个从扫描件OCR出来的PDF语料,常见噪声是“置信度较低的字符乱码”和“表格边界线残留”;而某个从在线文档导出的语料,常见噪声则是“超链接markdown语法”、“图片描述文本”。语料不同,规则完全不同。
清洗还有一个容易忽略的点:对OCR来源的文本,错误往往不是整行,而是个别字。例如合同扫描件里,“公司”被识别成“公式”,“地址”被识别成“地质”。这种情况靠正则基本无解,我的做法是在清洗阶段加一步“高频易错词表替换”,词表来自对OCR错误的抽样统计。但注意,这个词表一定要小(几十条以内),且只替换上下文明确的高频词,否则容易把正常文本也换了。
清洗做到位,接下来的分块才有意义。分块是RAG文本处理里最核心、也最容易用力过猛的一环。
3. 分块策略实战:固定长度、递归分割与语义分块怎么选
3.1 三种主流分块方式的核心差异
当前主流的分块方式可以粗分成三类:固定长度窗口、递归结构分割、语义分块。我逐个说下实际使用感受。
固定长度窗口:按token数硬切,比如每512个token切一块,相邻块之间设一个overlap重叠。这是最简单的方案,适合文本结构混乱、没有明显分隔符的语料。优点是实现简单、速度最快、对内存友好;缺点是切出来的块经常在语义中途断开,一句完整的话可能被拆到两块里,或者一个标题后的正文被切到下一块开头,导致每块内容缺头少尾。
递归结构分割:按文本的固有层次结构去切,比如Markdown的标题层级、PDF的标题段、对话记录里的人物发言。它先按标题切,切出来的块太大再按段落切,段落还大再按句子切。这种方式切出来的块语义完整度高,非常适合结构化的文档(产品手册、制度文件、技术文档)。缺点是对解析质量要求高,如果标题层级没有保留,或者格式混乱,递归分割会退化成按空行切,效果反而比固定长度更差。
语义分块:用embedding模型计算句子的向量相似度,把相似度较高的相邻句子聚成块。这种方式理论上最“聪明”,但它的计算量很大,且效果依赖embedding模型的敏感度。我在实际项目里只在前面两种方案都不理想时才考虑它,一般不作为首选。因为它的复杂度会引入新的不稳定因素,比如模型的滑动窗口大小怎么定、相似度阈值怎么选——这些参数调起来非常繁琐,收益却不一定值回成本。
3.2 块大小和重叠率:一个真实计算案例
参数怎么定,先说结论:块大小取决于内容的“检索粒度”,没有万能值。下面是实际项目里的一个计算例子。
某次处理一套产品运维手册,文档以条目式为主,每个操作步骤平均约150字,用token折算大约200-250个token。我当时的思路是:检索单元不能大于“一个操作步骤+它的上下文说明”,否则一个块里混入多个步骤,用户问“如何重启某服务”,召回回来的块里包含重启步骤的同时还包含另外两个无关步骤,会给生成模型增加不必要的干扰。那么块大小定在多少?一个完整的操作小节通常包括:标题(约5-10个token)、步骤正文(约200-250个token)、注意事项(约50-80个token),合计260-340个token。我取了一个偏保守的值:固定窗口280个token,重叠80个token。
重叠率怎么确定?80/280,大约28.5%。这个值不算低,为什么这么设?因为操作手册的检索核心逻辑是“用户问题往往命中某一句关键指令”,要确保被切分边缘的那句话在下一块里还能完整出现。如果重叠太少,比如20个token,一句60个token的关键步骤很可能在块边界被截断,导致召回时丢失了动作主体。重叠太多也有问题——向量库膨胀、检索时重复块增多、重排阶段出现大量近似内容互相挤压排名。
下面是几种常见场景的参考值,基于我的项目经验:
| 语料类型 | 推荐块大小 | 重叠率 | 检索单元逻辑 |
|---|---|---|---|
| FAQ问答库 | 200-300 token | 15%-20% | 单条问答尽量完整 |
| 技术操作手册 | 250-400 token | 20%-30% | 独立步骤+说明 |
| 法律合同全文 | 500-800 token | 10%-15% | 条款级语义完整性 |
| 新闻/博客文章 | 400-600 token | 10%-20% | 观点段落完整 |
| 对话记录 | 300-500 token | 20% | 一个话题轮次 |
这个表格不是标准答案,而是一个起跳点。每个项目都应该用测试集去验证,后面第5章我会专门讲怎么验证。
3.3 分块边界的丢失问题与应对经验
分块最容易踩的坑,一是标题与正文分离,二是列表项被切开,三是表格被拆碎。
标题与正文分离,典型表现:切块时把“3.2 环境配置要求”这个标题放在上一块末尾,而下面的环境参数列表在下一块。检索时用户问“环境配置要求是什么”,系统可能只召回包含标题的那一块,里面却没有任何正文,或者召回了正文块但缺少标题,不知道这些参数是干嘛的。我的处理方法是:分块阶段做“标题回溯”——遍历解析后的结构,如果当前块的第一行不是标题节点,就把最近的标题节点合并到当前块开头。实现起来很简单,在递归分割时记录标题栈即可。
列表项被切开,在Markdown里最常见。一个有序列表有8项,固定长度切分可能把第1-4项切到上一块,第5-8项切到下一块。这种块在检索里是“半截货”。我的做法是分块前先把列表项用特殊标记包裹,切分时优先保证同一列表不跨块,如果超长则强制在列表项边界处断行。
表格被拆碎,就更麻烦。PDF转出来的表格,如果直接按行切块,每一行失去表头,检索时语义完全变形。我的做法是分块前把表格转换为“键:值”或“字段:内容”的文本形式,每一行自带表头信息。比如:
名称: 服务状态接口 | 路径: /api/health | 方法: GET这样即使只切到一行,也能完整表达这一行的含义。
到这里,清洗和分块基本讲完。但还有一个很多人忽略的提升检索质量的维度——元数据。它不改变文本本身,却能大幅改变召回排序的可用性。
4. 元数据提取:向量检索的隐形过滤器
4.1为什么只有embedding不够?
很多初做RAG的人会有个误区:既然是语义检索,embedding相似度够高就行,元数据有什么必要?我解释一下问题出在哪。
向量检索天然是“全局模糊匹配”。你问“去年的财务报销标准”,embedding模型能理解“去年”和“2024年”在语义上相关,但如果文档库里同时存在2023年、2024年、2025年三个版本的报销标准,向量相似度会把它们全部召回来,具体哪一年是最新的?模型不知道。这时候需要元数据来过滤:插入时记录每条文本块的生效年份,检索时只保留年份字段匹配的块,再进入重排和生成。
还有一个更常见的场景:一个企业内部知识库里既有产品文档,也有内部培训记录、销售话术、客户案例。用户问“我们的产品主打卖点是什么”,如果不过滤,向量检索可能返回一坨销售话术里的夸张表述,而不是官方产品文档里的准确描述。元数据里标记的内容来源类型(官方文档/内部培训/外部宣传),可以在检索时直接指定只搜索某类来源。
有人可能会说,那我把来源类型和年份直接写进文本不就行了?比如每个块开头加一行“来源:产品手册,2024版”。这样做有两个问题:一是污染语义,embedding模型在计算相似度时会被这些非正文内容干扰;二是消耗token预算,每个块都带冗余前缀会稀释正文的语义密度。正确做法是把这些信息作为独立的元数据字段与文本块一起存储、一起检索过滤,互不干扰。
4.2 值得提取的元数据字段清单
基于项目经验,我常用下面这些元数据字段。不要求全,按场景挑着用。
| 元数据字段 | 作用 | 典型过滤场景 |
|---|---|---|
| 文档ID/块ID | 溯源与去重 | 定位原始文档、调试召回 |
| 来源类型 | 区分内容性质 | 只检索官方文档 |
| 生效时间/版本号 | 时间有效性过滤 | 只检索最新政策版本 |
| 标题层级路径 | 保留文档结构 | 明确文本块在文档中的位置 |
| 作者/部门 | 组织维度过滤 | 只看某团队的文档 |
| 标签/类别 | 业务维度过滤 | 只看合同/FAQ/操作手册 |
| 语言 | 多语言过滤 | 只检索中文/英文 |
| 内容hash | 精确去重 | 避免重复入库 |
“标题层级路径”这个字段值得单独说明。它记录的是文本块在文档里的结构位置,比如“3.2.1 环境配置 → 网络要求 → 防火墙设置”。这个字段有几个用途:一是拼接语义,让每块知道自己属于哪个章节,检索时可以按父级标题过滤;二是支持“章节级召回”,当某个章节内容被切到多块时,可以通过层级路径把它们聚合起来,送进大模型时按完整章节排序,而不是按块散着给。
我见过一个很好的用法:在检索之后、生成之前,把召回块的标题层级路径提取出来,用相同路径的块相互补全上下文。比如用户问“防火墙有哪些端口要开放”,向量召回抓到了“防火墙设置”这一节的三块碎片,系统按路径识别它们是同一章节,就把整个章节的完整内容作为上下文传入,生成质量会明显好于只传三块碎片——因为碎片之间往往有内容跳跃。
4.3 元数据过滤在混合检索里的实操组合
元数据不是单独用的,它和混合检索配合起来威力最大。所谓混合检索,通常是向量检索+BM25关键词检索的并行/融合方案。元数据在中间起到的作用是:先过滤,再排序。
举一个实际组合方式。给定一个问题“2025年报销标准里住宿费上限是多少”,检索过程是:
- 先用关键词层(BM25)在全文里做初步召回,因为报销标准和住宿费、上限这类词在字面上高度匹配,BM25在这个场景下比向量更精准。
- 向量层同时做语义召回,防止问法变体导致字面不匹配。
- 两层召回的结果,先按“生效时间=2025年”这个元数据字段过滤一遍,把2023年、2024年的版本全部丢弃。
- 过滤后的结果再做融合排序,进入重排器。
这个流程里,元数据过滤是前置硬约束,它和“语义相似度”一起决定了检索质量的上下限。没有这一步,就算BM25和向量都做了,模型依然会面临“多个版本相似内容互搏”的问题。
元数据提取本身怎么实现?对结构化文档(JSON、Markdown带front matter),直接用解析器取字段;对非结构化文档(PDF、扫描件),需要依赖解析阶段的标题识别、时间正则、以及文档分类模型。这部分的精度要求不那么苛刻,因为元数据的作用是过滤而不是展示,少量缺失可以通过规则兜底,不需要追求百分之百准确。
做到这里,文本处理流程基本完整了。但还有一个问题:你做的这些清洗、分块、元数据方案,到底好不好?如果只靠“感觉效果还行”,这个项目迟早会翻车。最后一章讲验证方法。
5. 文本处理效果怎么验证:离线评估与人工抽检配合
5.1 靠一个测试问题集,别靠“看着还行”
我见过很多RAG项目负责人,验收方式是找两三个问题问一遍,觉得“回答得挺流畅”就算过了。这极其危险——大模型生成本来就带有一定的“圆满感”,面对糟糕的检索结果,它也常能组织出一段有模有样的回答,只是细看内容错漏百出。真正要验证文本处理效果,必须做离线评估,用一组固定的问题去测检索质量。
构造问题集是第一步。我通常按“每类场景10-20个问题”来建,覆盖四个维度:问答匹配(问题直接命中某一篇文档里的明确答案)、多文档融合(答案分散在多篇文档里)、版本冲突(同一个问题有多个版本答案,需要靠元数据筛选)、边界模糊(问法很大,答案分散在很多块里,考验召回排名的天花板)。测试问题来源有两种:一种是直接用真实用户日志里的高频问题,这个最贴近真实场景;另一种是基于文档内容人工设计,补充用户可能问的方式变体。
5.2 三个核心指标:命中率、上下文重建率、噪声率
评估文本处理效果,我重点看三个指标,计算都很轻量,人工标注半天就能做。
命中率:对每个测试问题,看Top5召回里是否存在包含标准答案的块。如果有,记作命中。这个指标反映的是“检索漏斗”的顶端质量。文本处理阶段如果切分不合理、清洗不到位,命中率会显著偏低——典型表现是问题里的关键词分散在两个块中,没有一块能同时包含完整答案。
上下文重建率:对命中的块,人工判断它的完整程度——块内是否包含回答该问题所需的全部上下文,比如标题、前置条件、步骤主体。这个指标用来检验分块参数是否合适:块太小,重建率会偏低;块太大,检索命中率会被稀释,因为相似度分数被无关内容拉低。
噪声率:召回结果中与问题无关的块占召回总数的比例。噪声率高,说明清洗不干净(无关模板文本反复出现)、或分块粒度太粗(一个块包含大量无关主题)、或元数据过滤没生效(不同版本/不同类型的文档混在一起)。
这三个指标一起看,能快速定位问题出在清洗、分块还是元数据。比如:命中率低但重建率高,说明大部分块切得太大,检索时相似度被稀释;命中率正常但噪声率高,通常是清洗规则漏掉了页眉页脚这类重复文本,或元数据缺少来源过滤。
5.3 一套可复用的评估流程
评估流程我建议这么走,成本不高,收益却很直接:
- 准备一个50-100条的问题集,每条标注标准答案所在的原始文档位置。
- 跑一遍当前的完整检索流程,导出每条问题的Top5召回块。
- 按上面三个指标人工标注,记录通过率。
- 调整文本处理参数(比如块大小、重叠率、清洗规则),重新跑一遍,对比指标变化。
这个过程通常能很快暴露问题。我记得某次调参经历:把块大小从800 token降到300 token之后,命中率没有明显变化,但上下文重建率从61%掉到了48%,说明切太细了,答案虽然被召回了,但块内缺少完整的步骤上下文。后来把块大小调回450 token,重叠率从10%提到20%,两个指标都回升了。这种量级的变化,靠“问几个问题感觉一下”是感知不到的。
人工抽检也是必须的环节。即使指标好看,也建议每轮评估随机抽20条召回结果,人工阅读一遍,确认没有“看起来相关、实际是拼接怪”的块。因为有时模型生成的回答能完美圆场,但召回块本身逻辑混乱——这种隐患指标不一定能反映,人工抽检才能兜底。
文本处理这个环节,做好做坏,差别不在工具多高级,而在愿不愿意沉下心去看语料、调规则、跑评估。我最后再分享一个小经验:不要追求一次性做到完美,先上一个“够用”的版本,然后靠问题集持续迭代。每次改完清洗规则或分块参数,跑一遍问题集对比指标,比什么都好使。