Appearance
5.4 RAG·真实文档处理
demo 里的知识库是几条干净的字符串。真实项目里是一堆 PDF、扫描件、带表格的 Word、网页、代码——RAG 效果差,一大半死在"文档没处理好"这一步。这一节讲怎么把脏乱的真实文档变成高质量的可检索内容。
真实文档的麻烦
理想:干净的纯文本段落
现实:PDF(有的能复制、有的是图片)、表格、页眉页脚水印、
多栏排版、扫描件、Word/PPT、HTML 标签、代码……处理流水线大致是:抽取 → 清洗 → 分块 → (加上下文)→ Embedding → 入库。下面逐段说。
第一步:抽取(按类型分流)
| 文档类型 | 怎么抽 |
|---|---|
| 可复制文字的 PDF | 用 pdf-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.2 的 payload / 列)。好处:
- 检索时能按条件过滤(只查某产品线、某时间段)
- 回答时能精确引用("见《退款政策》第 3 页",呼应 5.3 强制引用)
完整处理流水线
真实文档
│ 抽取(按类型分流:文字PDF直抽 / 扫描件OCR)
│ 清洗(去页眉页脚、合并断句、去乱码)
│ 分块(按结构切,标题/段落为界)
│ 加上下文(每块前拼一段背景说明) ← 显著提升召回
│ + 提取元数据(来源/页码/分类)
▼
Embedding(百炼)→ 入库(pgvector/Qdrant,5.2)🛠️ 实战练习:处理一篇真实 PDF
拿一篇你手头的真实 PDF(产品手册、政策文档)跑一遍:
- 用
pdf-parse抽文本;看看有没有页眉页脚噪音,写几行清洗 - 按标题/段落分块,打印出来检查每块是不是"完整的意思"
- 给其中几个"缺上下文"的块加上
addContext生成的背景,对比检索效果 - 入库时带上
source和页码元数据
进阶挑战:找一页带表格的内容,把表格转成 Markdown 表格再入库,测试"表格里某个数值"能不能被问出来。
📌 关键结论
- RAG 效果一大半取决于文档处理:抽取 → 清洗 → 分块 → 加上下文 → 元数据
- 抽取按类型分流,能复制文字的别 OCR;扫描件才走多模态
- 分块优先按文档结构(标题/段落)切,定长只是兜底
- 上下文检索(嵌入前给每块加背景)是性价比很高的提质手段,实测大幅降低检索失败
- 元数据(来源/页码/分类)一起存,支撑过滤和精确引用
下一节:5.5 RAG·实战案例集