RAG 实战教程(三):PDF、Word、Markdown 多文档知识库实战——文档解析、清洗与智能索引

RAG 系列第三篇。
本篇将在第二篇「Python + Qdrant 搭建第一个 RAG 知识库」的基础上继续升级,实现一个更加接近真实企业场景的知识库系统。
我们将支持:
- PDF 文档
- Word 文档
- Markdown 文档
- HTML 网页
- 文档清洗
- Metadata 管理
- 增量索引
- 多格式统一入库
最终实现:
企业文档
|
|
↓
PDF / Word / Markdown / HTML
|
↓
Document Loader
|
↓
文本清洗
|
↓
结构化 Chunk
|
↓
Embedding
|
↓
Qdrant 向量数据库
|
↓
RAG 问答系统
一、为什么真实 RAG 最难的是数据,而不是模型?
很多人在第一次接触 RAG 时,会认为:
RAG 的核心就是调用大模型。
实际上,真正落地之后你会发现:
模型往往不是最大的问题。
企业真正困难的是:
我的资料在哪里?
如何读取?
如何切分?
如何保持结构?
如何避免垃圾内容进入知识库?
如何知道答案来自哪一页?
例如:
一个企业知识库可能包含:
产品文档
├── 产品介绍.pdf
├── API 接口文档.md
├── 用户手册.docx
├── 售后规则.html
├── 内部培训 PPT
└── 数据库说明文档
这些文件有几个共同问题:
1. 文件格式不同
PDF:
文字
图片
表格
扫描页
页眉页脚
Word:
标题
正文
列表
表格
图片
Markdown:
标题层级
代码块
列表
链接
HTML:
标签
脚本
广告
导航
如果直接丢给 Embedding:
效果通常不会很好。
2. 文档结构容易丢失
例如:
原始 PDF:
第三章 用户退款规则
3.1 普通退款
用户购买后 7 天内可以申请退款。
3.2 特殊情况
超过 7 天需要人工审核。
如果简单提取:
可能变成:
第三章 用户退款规则
3.1 普通退款
用户购买后7天内可以申请退款
3.2特殊情况
超过7天需要人工审核
标题关系丢失。
Embedding 不知道:
普通退款
属于:
用户退款规则
3. 企业需要引用来源
一个真正可用的 RAG:
不是:
答案:
可以退款。
而应该:
答案:
用户购买后 7 天内可以申请退款。
来源:
用户协议.pdf
第 18 页
第三章 用户退款规则
所以我们必须保存:
{
"content": "...",
"metadata": {
"source": "用户协议.pdf",
"page": 18,
"title": "退款规则"
}
}
二、第三篇最终目标
这一篇完成之后,我们的 RAG 架构:
数据接入层
PDF
|
Word
|
Markdown
|
HTML
↓
Document Loader
↓
Text Cleaner
↓
Smart Chunk
↓
Embedding
↓
Qdrant
↓
用户问题
↓
Query Embedding
↓
Vector Search
↓
Context
↓
LLM
↓
答案 + 来源
三、项目结构升级
第二篇项目:
rag-demo/
升级:
rag-enterprise/
├── data/
│
│ ├── pdf/
│ ├── word/
│ ├── markdown/
│ └── html/
│
├── loaders/
│
│ ├── pdf_loader.py
│ ├── word_loader.py
│ ├── markdown_loader.py
│ └── html_loader.py
│
├── processors/
│ ├── cleaner.py
│ └── splitter.py
├── index/
│ ├── embedding.py
│ └── vector_store.py
├── pipeline/
│ └── index_pipeline.py
├── app.py
├── requirements.txt
└── README.md
这就是一个更加接近生产环境的结构。
四、安装依赖
创建:
requirements.txt
内容:
openai
qdrant-client
python-dotenv
pymupdf
python-docx
beautifulsoup4
markdown
安装:
pip install -r requirements.txt
五、统一 Document 数据结构
这是整个系统非常重要的一步。
不要让不同 Loader 返回不同格式。
例如:
PDF 返回:
{
"text": "...",
"page": 1
}
Word 返回:
{
"content": "...",
}
后面一定会混乱。
所以我们定义统一格式:
{
"content": "",
"metadata": {
"source": "",
"type": "",
"page": None,
"title": ""
}
}
创建:
models.py
代码:
from dataclasses import dataclass
from typing import Optional
@dataclass
class Document:
content: str
source: str
doc_type: str
page: Optional[int] = None
title: Optional[str] = None
def metadata(self):
return {
"source":
self.source,
"type":
self.doc_type,
"page":
self.page,
"title":
self.title
}
以后所有 Loader:
都返回:
Document
六、PDF 文档解析
PDF 是企业 RAG 中最常见的数据来源。
安装:
pip install pymupdf
PyMuPDF 导入:
import fitz
创建:
loaders/pdf_loader.py
代码:
import fitz
from models import Document
def load_pdf(path):
pdf = fitz.open(path)
documents = []
for page_number, page in enumerate(
pdf,
start=1
):
text = page.get_text()
if not text.strip():
continue
documents.append(
Document(
content=text,
source=path,
doc_type="pdf",
page=page_number
)
)
return documents
测试:
from loaders.pdf_loader import load_pdf
docs = load_pdf(
"data/pdf/example.pdf"
)
for doc in docs:
print(doc.page)
print(
doc.content[:200]
)
输出:
1
RAG 是一种检索增强生成技术...
2
Embedding 可以把文本转换成向量...
七、PDF 最大的问题:扫描件
现实中很多 PDF:
不是文字 PDF。
例如:
扫描合同.pdf
扫描发票.pdf
扫描说明书.pdf
打开:
你能看到文字。
但是程序:
page.get_text()
返回:
空
原因:
里面其实是图片。
需要:
OCR
流程:
PDF
↓
图片
↓
OCR
↓
文字
↓
Chunk
常见方案:
PaddleOCR
Tesseract
Azure OCR
Google Vision
后续企业版 RAG 会专门讲 OCR。
八、Word 文档解析
Word 使用:
python-docx
安装:
pip install python-docx
创建:
loaders/word_loader.py
代码:
from docx import Document as DocxDocument
from models import Document
def load_word(path):
doc = DocxDocument(path)
texts = []
for paragraph in doc.paragraphs:
text = paragraph.text.strip()
if text:
texts.append(text)
content = "\n".join(
texts
)
return [
Document(
content=content,
source=path,
doc_type="word"
)
]
测试:
docs = load_word(
"data/word/manual.docx"
)
print(
docs[0].content
)
九、Markdown 文档解析
Markdown 在技术知识库里非常常见。
例如:
GitHub Wiki
技术博客
API 文档
项目 README
创建:
loaders/markdown_loader.py
代码:
from models import Document
def load_markdown(path):
with open(
path,
"r",
encoding="utf-8"
) as f:
content = f.read()
return [
Document(
content=content,
source=path,
doc_type="markdown"
)
]
但是 Markdown 有一个优势:
它有结构。
例如:
# RAG
## 什么是 RAG
内容...
## 什么是 Embedding
内容...
我们后面可以根据:
#
##
###
进行智能切分。
十、HTML 网页解析
很多企业知识来自:
官网
帮助中心
API 文档
博客
HTML 需要清理:
删除:
script
style
导航
广告
安装:
pip install beautifulsoup4
创建:
loaders/html_loader.py
代码:
from bs4 import BeautifulSoup
from models import Document
def load_html(path):
with open(
path,
"r",
encoding="utf-8"
) as f:
html = f.read()
soup = BeautifulSoup(
html,
"html.parser"
)
for tag in soup(
[
"script",
"style"
]
):
tag.decompose()
text = soup.get_text(
"\n"
)
return [
Document(
content=text,
source=path,
doc_type="html"
)
]
十一、统一 Loader 接口
现在:
PDF:
load_pdf()
Word:
load_word()
Markdown:
load_markdown()
HTML:
load_html()
虽然可以工作。
但是项目越来越大后:
调用会很乱。
所以统一:
创建:
loaders/__init__.py
代码:
from loaders.pdf_loader import load_pdf
from loaders.word_loader import load_word
from loaders.markdown_loader import load_markdown
from loaders.html_loader import load_html
def load_document(path):
if path.endswith(
".pdf"
):
return load_pdf(path)
if path.endswith(
".docx"
):
return load_word(path)
if path.endswith(
".md"
):
return load_markdown(path)
if path.endswith(
".html"
):
return load_html(path)
raise Exception(
f"不支持文件类型:{path}"
)
以后新增格式:
只需要增加:
if suffix:
return loader()
即可。
十二、文档清洗
原始文本通常很脏。
例如:
PDF:
第 1 页
公司名称
正文内容
第 2 页
公司名称
正文内容
页眉重复。
需要清洗。
创建:
processors/cleaner.py
代码:
import re
def clean_text(text):
# 删除多余空白
text = re.sub(
r"\s+",
" ",
text
)
# 删除特殊字符
text = re.sub(
r"[^\w\s\u4e00-\u9fa5,。!?:;,.!?]",
"",
text
)
return text.strip()
测试:
text = """
RAG
是一种技术。
"""
print(
clean_text(text)
)
输出:
RAG 是一种技术。
十三、为什么清洗非常重要?
因为 Embedding 不是理解人类格式。
它看到:
RAG
RAG
RAG
123456
公司名称
这些垃圾内容。
会影响:
向量质量
最终导致:
搜索不准
所以:
好的 RAG,首先需要好的数据。
(未完,下一部分继续写:智能 Chunk、Metadata 索引、多文档入库、增量更新、完整企业级 Pipeline。)

浙公网安备 33010602011771号