RAG 数据工程实战:从脏文档清洗到高质量 Chunk 切分的完整方法论

0 阅读21分钟

RAG 数据工程实战:从脏文档清洗到高质量 Chunk 切分的完整方法论

在这里插入图片描述

很多人把 RAG 做不好,第一反应是:换 embedding、换向量库、加 rerank、换更大的模型。
但真正决定系统下限的,往往不是模型层,而是更前面的那一步:你的数据到底是怎么被加载、清洗、结构恢复、切分、补充 metadata 并最终进入索引的。


一、为什么我要把 RAG 看成“信息流水线”,而不是一个 Prompt 技巧

如果你把 RAG 理解成:

用户问题 -> 检索几段文本 -> 拼到 Prompt 里 -> 让模型回答

那你很容易陷入一个误区:
只关注检索和生成,却忽略了检索之前的数据准备阶段。

但在真实工程里,RAG 更像一条完整的信息流水线:

1. 离线索引阶段(Offline Indexing)

负责把原始文档变成“可检索的知识单元”,典型步骤包括:

  • 文档加载
  • 内容抽取
  • 脏数据清洗
  • 结构恢复
  • Chunk 切分
  • Metadata 增强
  • Embedding 生成
  • 向量索引构建

2. 在线检索阶段(Online Retrieval)

负责在运行时完成:

  • Query 预处理
  • Recall
  • Filter
  • Hybrid Search
  • Rerank
  • Context Packing
  • LLM Generation

真正成熟的 RAG 系统,通常会把这两部分严格拆开。
因为离线阶段解决的是“知识如何被表示”,而在线阶段解决的是“知识如何被找到和使用”

换句话说:

RAG 的核心,不只是让模型“看到知识”,而是要先把知识整理成模型“看得懂、找得到、用得上”的样子。


二、很多 RAG 项目失败,不是因为模型太弱,而是因为数据一开始就坏了

在实际项目中,最常见的问题根本不是“召回算法不够强”,而是:

  • PDF 文本抽取错乱
  • 标题层级丢失
  • 表格被打散
  • 页眉页脚混入正文
  • 重复段落大量存在
  • 乱码、OCR 噪声很多
  • 一个 chunk 里混进多个主题
  • 元数据缺失,导致无法过滤和定位来源

这些问题一旦进入 embedding 阶段,后果会被放大:

1. 脏文本会污染向量表示

embedding 模型不是魔法。
如果一段文本里混入了:

  • 版权声明
  • 导航菜单
  • 重复页脚
  • 无意义空白符
  • OCR 错字
  • 乱码字符

那么模型生成的向量,本质上就是在为“噪声 + 有效语义”的混合体建模。
这会直接降低检索相关性。

2. 结构丢失会让语义边界消失

例如一篇技术文档本来是:

  • 一级标题:模型部署
  • 二级标题:Docker 部署
  • 二级标题:K8s 部署

结果抽取后变成一整坨连续文本。
这时你再按固定长度切分,极有可能把两个不同主题切进同一个 chunk 里。
最终的检索结果就会出现一种很典型的现象:

召回“看起来相关”,但回答总是漂,定位不到真正的答案段。

3. 表格、列表、代码块一旦断裂,信息价值会骤降

比如参数表、配置清单、FAQ、对比表,这些内容本来天然适合检索。
但如果解析时被打散成多个不连续段落,它们的结构优势就消失了。
向量召回也会明显变差。

所以很多时候,问题不是:

检索没找到答案

而是:

答案在进入索引前,就已经被错误地表示了


三、RAG 数据工程的第一步:别急着切 Chunk,先把文档“读对”

很多人一上来就写:

splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=100)
chunks = splitter.split_text(text)

这在 Demo 里可以,在生产里通常不够。

因为在切分之前,你首先要回答:

你拿到的“text”,真的是你想要的那个文本吗?

1. 文档加载不是读取文件那么简单

不同来源的文档,处理方式完全不同:

PDF

最麻烦,常见问题包括:

  • 双栏布局被串行化
  • 页眉页脚重复注入
  • 表格结构丢失
  • 图片中的关键信息抽不到
  • 标题层级识别失败
  • 扫描版 PDF 需要 OCR

HTML / 网页

常见问题:

  • 导航栏、侧边栏、广告、评论区混入正文
  • DOM 顺序和阅读顺序不一致
  • 标题与正文关系丢失

Markdown / 文档中心

通常结构最好,但也有问题:

  • 代码块过长
  • 引用块与正文混排
  • 标题层级过深导致切分不均衡

Excel / 表格类数据

重点不是“按字数切”,而是:

  • 保留行列关系
  • 保留表头
  • 明确单元格语义
  • 必要时转成自然语言描述 + 结构化字段

聊天记录 / FAQ / 工单

重点在于:

  • 对话轮次边界
  • 说话人角色
  • 时间顺序
  • 单轮与多轮上下文的关系

四、数据清洗不是可选项,而是高质量 RAG 的地基

下面给出我更推荐的清洗思路。

4.1 基础清洗:先去掉明显的“垃圾信息”

应该优先清理的内容

  • 页眉页脚
  • 页码
  • 水印文本
  • 重复版权声明
  • 网站导航菜单
  • 面包屑路径
  • 多余空白符
  • 连续换行
  • 乱码与无意义 Unicode 噪声
  • OCR 误识别的高频错误

一个很实用的原则

如果某段内容:

  • 对用户问答没有帮助
  • 出现在大量文档中高度重复
  • 会干扰段落语义

那它就不应该进入 embedding。

4.2 结构清洗:保留“文档是怎么组织起来的”

很多时候,比清理噪声更重要的是恢复结构。

你要尽量保住这些信息:

  • 标题层级
  • 小节归属
  • 表格边界
  • 列表结构
  • 代码块边界
  • 图片标题 / 图注
  • 来源页码
  • 来源章节

因为对 RAG 来说,文档不只是“字”,而是“带层级和边界的信息组织体”。

为什么结构这么重要?

因为 chunk 的质量,不只取决于内容本身,还取决于:

这一段内容是否在一个清晰的语义边界内成立

例如:

  • “安装步骤” 应该和 “故障排查” 分开
  • “定义” 应该和 “示例” 建立邻接关系
  • “表头” 应该和 “数据行” 保持绑定
  • “代码片段” 不应被切进普通正文中间

如果结构没保住,后面的任何高级检索优化,都会事倍功半。

4.3 语义清洗:不是所有规范化都一定有利

有些清洗操作要非常谨慎:

1. Stop Words 去除

很多经典 IR 会移除停用词,但在现代 embedding 检索里,不能简单粗暴照搬。

因为有些词虽然常见,但在具体语义中仍有作用,例如:

  • 否定词
  • 比较词
  • 条件词
  • 时间词

所以工程上更推荐:

  • 先做实验
  • 再决定是否做停用词处理
  • 不要默认“一删了之”

2. 大小写归一

在部分场景有帮助,尤其是英文文本。
但涉及专有名词、缩写、代码符号时,要慎重。

3. 拼写纠错与缩写展开

这个很有价值,尤其是:

  • OCR 文档
  • 工单系统
  • 医疗 / 法律 / 企业内部术语库

但要注意:

  • 纠错必须可控
  • 缩写展开最好带领域词典
  • 不要让自动修复引入语义漂移

五、Chunk 不是“切小一点”这么简单,而是在定义知识颗粒度

很多人理解 chunking 的方式是:

大文档太长,所以切小一点

这句话没错,但不完整。
更准确的说法应该是:

Chunking 的本质,是把原始文档转换成适合检索和生成的知识颗粒。

这个颗粒要同时满足几个条件:

  • 足够小,便于 embedding 和高效召回
  • 足够完整,能独立表达一个相对明确的语义单元
  • 足够稳定,脱离原文后仍然容易理解
  • 足够可追踪,能回溯来源位置和原始上下文

所以一个好 chunk 的判断标准,不是“长度合不合适”,而是:

它能不能作为一个最小可用知识单元被准确检索和安全使用。


六、常见 Chunk 切分策略全解

6.1 固定长度切分(Fixed-size Chunking)

最常见,也最容易上手。

方式

按固定字符数或 token 数切分,例如:

  • 500 tokens
  • 800 tokens
  • 1000 characters

并设置一定 overlap。

优点

  • 简单
  • 稳定
  • 实现成本低
  • 方便批处理
  • 易于控制索引体积

缺点

  • 容易切断语义边界
  • 可能把两个主题切在一起
  • 对结构化文档不友好

适合场景

  • 纯文本资料
  • 结构不明显的长文
  • 快速原型验证
  • 数据质量相对干净时的 baseline

6.2 递归切分(Recursive Chunking)

这是我最推荐作为通用起点的一种方式。

核心思想

优先按更强的语义边界切:

  • 段落
  • 换行
  • 空格
  • 最后才硬切

也就是说,它会尽量:

  • 先保住段落
  • 再保住句子
  • 再保住词组
  • 最后才强制满足长度限制

为什么它适合作为默认方案?

因为它在“控制 chunk 大小”和“尽量保留语义连续性”之间取得了比较好的平衡。

对于大多数通用文本:

  • 不需要复杂规则
  • 也比纯固定长度更稳

典型适用场景

  • 知识库问答
  • 产品文档
  • 技术手册
  • 博客文章
  • API 文档

6.3 按结构切分(Structure-aware Chunking)

这是生产系统里非常关键的一类策略。

基本思想

不是按长度切,而是按文档结构切:

  • 按标题
  • 按章节
  • 按子标题
  • 按表格
  • 按列表
  • 按代码块
  • 按 FAQ 项
  • 按网页 DOM 区块

优点

  • 更符合人的阅读逻辑
  • chunk 的可解释性强
  • 便于做 metadata 补充
  • 很适合知识库、文档中心、论文、规范类内容

缺点

  • 依赖解析质量
  • 需要更复杂的预处理
  • 文档源越混乱,实现越难

一个经验判断

如果你的文档本身就有明显层级,那么优先利用层级结构切分,通常比无脑固定长度效果更好。

6.4 语义切分(Semantic Chunking)

这类方法追求的是:

让 chunk 边界尽量落在“语义自然断点”上

比如基于:

  • 句向量相似度变化
  • 段落主题漂移
  • section coherence
  • 标题 + 内容一致性

优点

  • chunk 更自然
  • 更有机会保持完整语义单元
  • 对复杂长文档更友好

缺点

  • 实现复杂
  • 计算成本更高
  • 调试难度更大
  • 在大规模离线索引时成本敏感

适合场景

  • 高价值知识库
  • 法律 / 医疗 / 研究型文档
  • 多主题长文档
  • 对召回精度要求极高的系统

6.5 混合切分(Hybrid Chunking)

这是很多生产系统最后会走到的形态:

  1. 先按结构切
  2. 对结构块再做递归切分
  3. 必要时增加 overlap
  4. 特殊对象单独处理(表格、代码、FAQ、图片说明)

这通常比“单一策略通吃”更靠谱。


七、Chunk 太大和太小,都会出问题

这是 RAG 实战里最常见、也最容易被低估的点。

7.1 Chunk 太大

会导致:

  • 一个 chunk 包含多个主题
  • embedding 语义过粗
  • 召回命中“相关大概念”,但抓不住具体答案
  • 上下文拼接后浪费 token
  • rerank 负担加重

表现出来就是:

看起来召回挺相关,但回答总是泛泛而谈

7.2 Chunk 太小

会导致:

  • 上下文不完整
  • 定义和解释被拆散
  • 问题与答案落在不同 chunk
  • 召回片段碎片化
  • LLM 拿到的信息不够闭环

表现出来就是:

明明库里有答案,但系统只召回半句


八、Overlap 到底怎么设,为什么不是越大越好

Overlap 的目的是:

防止关键信息刚好落在 chunk 边界,导致语义断裂

例如:

  • 上一个 chunk 的最后一句提出概念
  • 下一个 chunk 的第一句给出解释

如果没有 overlap,这种跨边界关系就容易丢。

Overlap 的好处

  • 保留局部连续性
  • 提高跨句依赖的召回概率
  • 减少“刚好切断答案”的情况

但 overlap 过大也有明显代价

  • 索引体积变大
  • embedding 成本上升
  • 召回结果重复度变高
  • rerank 的候选更冗余
  • 上下文窗口被重复内容浪费

一个实战建议

如果你没有现成经验值,可以从下面这个 baseline 起步:

  • chunk_size = 512 tokens
  • chunk_overlap = 128 tokens

也就是大约 25% overlap。

这个起点很稳,后续再根据内容类型调节:

  • 结构化文档:可以更小 overlap
  • 叙述性文档:可以更大一点
  • FAQ / 短条目:甚至可以几乎不 overlap

更实用的判断方法

不要问:

overlap 设多少最标准?

而要问:

我的答案是否经常跨 chunk 边界出现?

如果经常出现,就该增大 overlap 或改切分策略。
如果召回里重复片段太多,就该减小 overlap。


九、不同文档类型,应该用不同切法

这是非常重要的一点。

9.1 技术文档 / 产品手册

推荐:

  • 按标题结构切
  • 对过长 section 再递归切分
  • 保留标题路径 metadata

推荐 metadata

  • 文档标题
  • 一级 / 二级 / 三级标题
  • 来源链接
  • 页码 / section id
  • 更新时间
  • 产品版本号

9.2 API 文档

推荐:

  • 以接口为主单位切分
  • 请求参数、响应参数、示例独立成块
  • 保留 endpoint、method、module 等元数据

常见错误

把多个接口说明切进同一个 chunk。
这样一来,检索虽然“像是同一个系统的接口说明”,但不够精确。

9.3 FAQ / 工单 / 问答库

推荐:

  • 一问一答作为天然 chunk
  • 多轮工单可按 case 切
  • 复杂 case 再按轮次切

关键点

要保留:

  • 问题
  • 答案
  • 类别标签
  • 产品模块
  • 工单状态
  • 时间戳

9.4 论文 / 报告 / 规范文档

推荐:

  • 先做版面解析
  • 按章节切
  • 图表、公式、表格单独处理
  • 摘要、方法、实验、结论尽量不要混切

特别注意

论文类内容最怕:

  • 双栏混串
  • 图注掉落
  • 表格被断裂
  • 参考文献混入正文 chunk

9.5 表格类文档

表格不能简单“转成一大段纯文本再切”。

更合理的方法通常是:

  • 保留表头
  • 行级展开
  • 需要时把每一行改写为结构化自然语言
  • 同时保存原始结构字段

例如把:

型号显存功耗
A10040GB250W

转成:

产品型号 A100,显存 40GB,功耗 250W

然后再附加 metadata:

  • 表名
  • 表头
  • 行号
  • 来源页码

十、Metadata 不是附属品,而是检索系统的第二条腿

很多人做 RAG 时,只存:

  • chunk_text
  • embedding

这能跑,但不够工程化。

一个真正可控的索引层,至少还应该考虑这些字段:

  • doc_id
  • chunk_id
  • source
  • title
  • section_path
  • page_num
  • created_at
  • updated_at
  • language
  • tags
  • keywords
  • entity
  • version
  • permission_scope

为什么 metadata 很重要?

10.1 用于过滤

比如:

  • 只查某个产品版本
  • 只查最近三个月文档
  • 只查中文资料
  • 只查“已生效”的制度文件

没有 metadata,这些都做不了。

10.2 用于增强召回

有些查询并不适合只靠向量语义。

例如:

  • “v2.3 的部署文档”
  • “产品 X 在 2024 年的价格策略”
  • “接口 /auth/login 的限流规则”

这类问题非常适合结合:

  • 关键词搜索
  • 字段过滤
  • 向量检索
  • rerank

也就是典型的 hybrid search。

10.3 用于结果可解释与引用

高质量 RAG 不只是要答对,还要能告诉用户:

  • 答案来自哪
  • 来自哪一页
  • 来自哪一节
  • 是否是最新版本

这对企业知识库、医疗、法律、教育等场景尤其重要。


十一、从“脏文档”到“高质量索引”的完整离线流水线

下面给出一条更接近生产的离线流程。

Step 1:文档接入

输入源可能来自:

  • 本地文件
  • 对象存储
  • 企业网盘
  • Confluence / Notion / Wiki
  • 数据库导出
  • 工单系统
  • 邮件附件
  • 网页抓取

目标是统一进入一个 ingestion pipeline。

Step 2:文档解析

把原始文件转成可处理的中间表示:

  • 纯文本
  • Markdown
  • JSON
  • 结构化元素列表(标题、段落、表格、图片说明)

这一层最重要的目标不是“抽出尽可能多的字”,而是:

尽可能忠实地保留语义结构

Step 3:清洗与标准化

包括但不限于:

  • 去重
  • 去模板化噪声
  • 去页眉页脚
  • 合并断行
  • 清理空白符
  • 纠正常见 OCR 错误
  • 统一编码
  • 必要的缩写展开与术语归一

Step 4:结构恢复与分段

此时你应该尽量拿到:

  • 标题树
  • section 边界
  • 表格单元
  • 列表项
  • 代码块
  • 图注 / 表注
  • 页码定位

Step 5:Chunk 切分

根据文档类型选择:

  • 固定长度
  • 递归切分
  • 结构切分
  • 语义切分
  • 混合策略

Step 6:Metadata 增强

补齐:

  • 来源信息
  • 标题路径
  • 页码
  • 标签
  • 时间
  • 权限
  • 关键词
  • 摘要
  • 可回答问题

这里尤其有价值的一项是:

为 chunk 生成“它能回答哪些问题”的补充字段

因为用户查询的表达方式,和原文的表达方式,常常不是一回事。
提前生成 query-like metadata,往往能提升召回。

Step 7:Embedding 生成

这里要注意几件事:

1. 不要让输入超过模型限制

大 chunk 直接送进 embedding 模型,可能会被截断,导致信息丢失。

2. 保持 chunk 粒度一致

不要一部分是 50 token,一部分是 2000 token,还指望它们在同一个向量空间里表现得很稳定。

3. embedding 不是一次性工作

当你:

  • 更新切分策略
  • 调整清洗规则
  • 增加 metadata
  • 更换 embedding 模型

通常都需要重建索引。

Step 8:索引构建

至少要支持这些能力:

  • 向量检索
  • 关键词检索
  • 元数据过滤
  • 结果去重
  • rerank 接入
  • chunk 到原文的映射回溯

一个成熟的索引结构,通常不是“只存向量”,而是向量 + 文本 + 元数据 + 来源定位共同组成的。


十二、在线检索阶段,为什么也会反向暴露离线数据问题

很多人做线上调优时,会发现这些症状:

  • 召回内容总是偏泛
  • top-k 很多重复段
  • rerank 效果有限
  • 拼接上下文后信息冲突
  • 模型引用不到原文出处
  • 某些问题命中率始终很差

这些问题往往表面发生在在线检索阶段,但根因却在离线索引阶段。

举几个典型例子

例 1:top-k 重复严重

表面看像召回参数问题。
实际上可能是:

  • overlap 过大
  • chunk 太碎
  • 去重没做好
  • 同一内容多来源重复入库

例 2:召回很相关,但回答不精确

表面看像生成模型问题。
实际上可能是:

  • chunk 太大
  • 多主题混合
  • 标题没有进入 metadata
  • 答案片段被噪声包裹

例 3:某些字段类问题总答不好

表面看像语义检索弱。
实际上可能是:

  • 本质需要 keyword match
  • metadata 没建好
  • 没做 hybrid search

所以 RAG 的优化顺序,不应该永远是“先调模型”,而应该是:

  1. 先查数据质量
  2. 再查 chunk 质量
  3. 再查 metadata
  4. 再查召回与 rerank
  5. 最后才是生成策略

十三、一个我更推荐的工程实践方案

如果你现在正准备做一个企业内知识库 RAG,我会建议从下面这个版本起步:

13.1 文档处理

  • 优先保留结构化中间表示
  • PDF 不要只抽纯文本
  • HTML 不要直接抓全页字符串
  • 对表格和 FAQ 单独处理

13.2 清洗规则

  • 去页眉页脚
  • 去菜单 / 导航 / 版权区
  • 去高频重复模板
  • 合并异常断行
  • 保留标题层级
  • OCR 错误做字典式修复

13.3 Chunk 策略

  • 默认:按标题分组 + 递归切分
  • 过长 section:再做 token 级限制
  • overlap:先从 10%~25% 试起
  • 复杂长文:可引入语义切分
  • FAQ / 工单:按问答对切
  • 表格:按表头 + 行切

13.4 Metadata 设计

至少包含:

  • doc_id
  • chunk_id
  • title
  • section_path
  • page_num
  • source_url
  • language
  • version
  • updated_at

有条件再加:

  • summary
  • keywords
  • tags
  • entities
  • questions_this_chunk_can_answer

13.5 检索设计

  • 向量检索不是唯一入口
  • 要支持 keyword + vector + filter
  • rerank 尽量接在 recall 之后
  • 最终上下文拼接前要做去重和压缩

十四、如何评估你的 Chunk 切分到底好不好

不要只看“系统能不能跑通”,而要建立一套评估方法。

14.1 离线评估指标

可以观察:

  • Recall@K
  • MRR
  • nDCG
  • 命中率
  • 同源重复率
  • 平均 chunk 长度
  • 平均候选去重率

14.2 人工检查维度

非常建议抽样做人工分析,看:

  • chunk 是否语义完整
  • 是否经常截断定义 / 答案
  • 是否混入无关主题
  • 是否丢失标题上下文
  • 是否能回溯来源
  • 是否存在解析错误

14.3 线上体验指标

关注:

  • 首次答案命中率
  • 引用准确率
  • 幻觉率
  • 平均上下文长度
  • 平均响应耗时
  • 用户追问率

一个很重要的认知是:

chunking 没有“标准答案”,只有对你的数据和查询分布更合适的答案

因此它本质上是一个实验驱动问题,不是一个“抄参数”问题。


十五、最常见的 10 个 RAG 数据工程坑

坑 1:把 PDF 当纯文本处理

结果:版面结构全丢,标题、表格、图注混乱。

坑 2:所有文档统一一个 chunk_size

结果:FAQ 太碎,长报告太粗,整体失衡。

坑 3:只存向量,不存 metadata

结果:无法过滤、无法引用、无法解释。

坑 4:overlap 设太大

结果:召回重复严重,索引体积暴涨。

坑 5:只做向量检索,不做关键词检索

结果:字段类、版本类、精确术语类问题效果差。

坑 6:表格直接展开成杂乱文本

结果:表头语义丢失,数值问答表现很差。

坑 7:不做清洗直接 embedding

结果:向量空间被噪声污染。

坑 8:不区分离线和在线阶段

结果:系统难优化,也难排障。

坑 9:只调 top_k,不回头检查 chunk 质量

结果:一直在错误表示上做微调。

坑 10:没有评估闭环

结果:每次改策略都靠感觉,不知道到底变好了还是变坏了。


十六、给工程落地者的一套起步模板

如果你现在就要上线一个第一版系统,我建议先这么做:

文档类型

  • 产品文档
  • 技术手册
  • FAQ
  • 工单总结
  • PDF 规范

推荐起步配置

  • 优先结构化解析
  • 去页眉页脚、去导航、去重复模板
  • 按标题切分,再递归细分
  • chunk size 从 400~800 tokens 开始试
  • overlap 从 10%~25% 开始试
  • 记录完整 metadata
  • 采用向量检索 + 关键词检索 + 过滤
  • recall 后接 rerank
  • context packing 前先去重

验证方式

  • 准备 50~200 个真实问题
  • 标注标准答案与来源文档
  • 比较不同 chunk 策略的召回质量
  • 不只看最终回答,更要看中间召回结果

十七、结语:RAG 的本质,不是“把文档塞进向量库”,而是“重构知识的表示方式”

写到这里,其实结论已经很清楚了:

RAG 从来不是一个简单的 prompt 技巧,
而是一套围绕“知识表示、知识检索、知识使用”展开的信息工程系统。

在这个系统里:

  • 清洗:决定噪声是否会污染表示
  • 结构恢复:决定语义边界能否被保留
  • Chunk 切分:决定知识颗粒度是否合理
  • Metadata 增强:决定检索是否可控、可解释、可过滤
  • Embedding 与索引设计:决定知识能否被稳定召回
  • 在线检索与 rerank:决定知识能否被正确送到模型面前

所以真正高质量的 RAG,不是:

“我接了一个向量库”

而是:

“我构建了一套把原始文档转化为高质量可检索知识单元的数据工程体系”

这,才是 RAG 从 Demo 走向生产的分水岭。