Skip to content

5.4 RAG·真实文档处理

demo 里的知识库是几条干净的字符串。真实项目里是一堆 PDF、扫描件、带表格的 Word、网页、代码——RAG 效果差,一大半死在"文档没处理好"这一步。这一节讲怎么把脏乱的真实文档变成高质量的可检索内容。

真实文档的麻烦

理想:干净的纯文本段落
现实:PDF(有的能复制、有的是图片)、表格、页眉页脚水印、
      多栏排版、扫描件、Word/PPT、HTML 标签、代码……

处理流水线大致是:抽取 → 清洗 → 分块 → (加上下文)→ Embedding → 入库。下面逐段说。


第一步:抽取(按类型分流)

文档类型怎么抽
可复制文字的 PDFpdf-parse 等库直接抽文本(最便宜,优先
扫描件 / 图片 PDF走多模态 OCR(1.8,如 qwen-vl)识别
Word / PPT用对应解析库转成文本/Markdown
HTML 网页去标签、去导航/广告,留正文
表格单独处理,见下文

⚠️ 不要无脑全走多模态。能复制文字的就别 OCR——又贵又慢还可能引入识别错误。先判断类型再分流。

javascript
import { readFile } from "fs/promises"
import pdf from "pdf-parse"

async function extractPdf(path) {
  const buf = await readFile(path)
  const data = await pdf(buf)
  return data.text   // 纯文字 PDF 直接拿到文本;若几乎为空,说明是扫描件→转 OCR
}

第二步:清洗

抽出来的文本常带噪音,直接嵌入会污染检索:

  • 页眉页脚 / 页码 / 水印(每页重复出现的那几行)
  • 合并被换行硬切断的句子
  • 去掉乱码、连续空白、目录点线. . . . . 23

清洗的目标:让每一段读起来是完整、连贯、自包含的意思。


第三步:分块(按结构,不要无脑定长)

2.2 讲了分块基础。真实文档里,按结构切 > 按定长切

优先按文档自身的结构边界切:
  Markdown 标题 / 章节 / 段落 / 列表项
  → 每块是一个"完整的意思单元",而不是从句子中间切断
定长切分只作为没有结构时的兜底(记得加 overlap)
javascript
// 按 Markdown 标题切分(保留标题作为该块的上下文)
function splitByHeading(md) {
  const parts = md.split(/(?=^#{1,3}\s)/m)   // 在标题前切
  return parts.map(s => s.trim()).filter(Boolean)
}

第四步(关键):上下文检索(Contextual Retrieval)

这是 Anthropic 提出的、效果显著的一招,专治"块太碎、缺上下文"。

🔍 问题:一个块写着"营收同比增长了 3%"。单看这句,哪家公司?哪个季度? 都不知道——检索时匹配不上,模型看到也用不好。

做法:嵌入每个块之前,先让 LLM 给它生成一小段(50-100 字)上下文说明,拼在块前面再做 embedding。

javascript
async function addContext(fullDoc, chunk) {
  const res = await chat.chat.completions.create({
    model: "deepseek-v4-flash",
    messages: [{
      role: "user",
      content: `<文档>${fullDoc}</文档>\n\n<块>${chunk}</块>\n\n` +
        `请用一两句话说明"块"在整篇文档里的背景(属于哪部分、涉及什么主体/时间),只输出这段说明。`
    }]
  })
  const context = res.choices[0].message.content.trim()
  return `${context}\n\n${chunk}`   // 上下文 + 原块,一起拿去 embedding
}
// "本段出自2024年报A公司第三季度部分。营收同比增长了3%。"

Anthropic 实测:上下文嵌入 + 上下文 BM25 让 top-20 检索失败率降约 49%,再加 Reranking 降约 67%。代价是建库时每块多一次 LLM 调用(可用便宜模型 + 缓存摊薄)。


第五步:带上元数据

入库时把来源、页码、章节、时间作为元数据一起存(5.2payload / 列)。好处:

  • 检索时能按条件过滤(只查某产品线、某时间段)
  • 回答时能精确引用("见《退款政策》第 3 页",呼应 5.3 强制引用

完整处理流水线

真实文档
  │ 抽取(按类型分流:文字PDF直抽 / 扫描件OCR)
  │ 清洗(去页眉页脚、合并断句、去乱码)
  │ 分块(按结构切,标题/段落为界)
  │ 加上下文(每块前拼一段背景说明)          ← 显著提升召回
  │ + 提取元数据(来源/页码/分类)

Embedding(百炼)→ 入库(pgvector/Qdrant,5.2)

🛠️ 实战练习:处理一篇真实 PDF

拿一篇你手头的真实 PDF(产品手册、政策文档)跑一遍:

  1. pdf-parse 抽文本;看看有没有页眉页脚噪音,写几行清洗
  2. 按标题/段落分块,打印出来检查每块是不是"完整的意思"
  3. 给其中几个"缺上下文"的块加上 addContext 生成的背景,对比检索效果
  4. 入库时带上 source 和页码元数据

进阶挑战:找一页带表格的内容,把表格转成 Markdown 表格再入库,测试"表格里某个数值"能不能被问出来。


📌 关键结论

  1. RAG 效果一大半取决于文档处理:抽取 → 清洗 → 分块 → 加上下文 → 元数据
  2. 抽取按类型分流,能复制文字的别 OCR;扫描件才走多模态
  3. 分块优先按文档结构(标题/段落)切,定长只是兜底
  4. 上下文检索(嵌入前给每块加背景)是性价比很高的提质手段,实测大幅降低检索失败
  5. 元数据(来源/页码/分类)一起存,支撑过滤和精确引用

下一节:5.5 RAG·实战案例集

写给自己的 AI 学习地图