概述
在构建 RAG 系统时,我们常把精力花在调优大模型上,却忽略了最基础也最关键的一环------数据处理。俗话说"垃圾进,垃圾出",如果文档读取乱码、切分支离破碎,再强大的模型也无法给出精准答案。
今天,我们将打通从PDF 深度解析 、智能切分 到向量入库的完整链路,并重点解析那些你可能混淆的核心组件。
核心组件大揭秘:它们到底在干什么?
在开始写代码前,我们先解决一个概念问题。很多初学者容易混淆 LangChain、Text Splitters 和 ChromaDB 的关系。
我们可以把构建 RAG 系统比作**"经营一家图书馆"**:
| 组件/库 | 角色类比 | 核心职责 | 为什么需要它? |
|---|---|---|---|
| PyMuPDF | 图书扫描仪/修复师 | 文档解析与处理 | 它能把 PDF 这种"图片/二进制"格式翻译成可编辑的文本,还能提取图片、表格,甚至给 PDF 加水印。 |
| LangChain | 图书馆总管 | 应用开发框架 | 它提供了一套标准流程,把"读取、切分、存储、问答"这些散乱的工具串联起来,不用你自己造轮子。 |
| langchain-text-splitters | 图书装订工 | 数据预处理 | 它是 LangChain 的独立分包。专门负责把厚书切成小卡片,保证语义完整,不让句子断在半截。 |
| ChromaDB | 图书管理员 | 向量存储与检索 | 它不负责切分,只负责记忆。它把切好的卡片变成数字(向量)存起来,当你提问时,它能瞬间找到最相关的那张卡片。 |
** 关系图解**
PyMuPDF(读取) →Text Splitters(切分) →ChromaDB(存储)整个流程由
LangChain框架进行编排。
第一阶段:文档读取利器------PyMuPDF
在之前的尝试中,我们可能遇到过 PDF 读取乱码的问题。这是因为 PDF 本质上是二进制文件,Python 原生的 open() 无法理解其编码。
PyMuPDF (fitz) 是目前 Python 生态中最强大、速度最快的 PDF 处理库之一(基于 MuPDF 引擎)。
1. 安装与导入
注意:库的安装名和导入名是不一样的,这是新手常踩的坑。
bash
# 安装命令
pip install pymupdf
python
import fitz # 导入名为 fitz
2. 核心功能概览
除了我们今天要用的"提取文本",它还支持:
- 文档渲染:把 PDF 页面转成高清图片(PNG/JPG),常用于做文档预览。
- 文档编辑:合并 PDF、拆分页面、添加水印、绘制图形(矩形、圆形)。
- 元数据提取:获取作者、创建时间、关键词等信息。
- 图像处理:提取 PDF 中嵌入的所有图片。
3. 代码实现
我们封装一个简单的函数,不仅能读文本,还能顺便看看文档的基本信息。
python
import fitz # PyMuPDF
def read_pdf_advanced(file_path):
"""
使用 PyMuPDF 读取 PDF,并提取文本和元数据
"""
try:
doc = fitz.open(file_path)
# 1. 提取元数据(可选)
metadata = doc.metadata
print(f" 文档标题: {metadata.get('title', '无标题')}")
print(f" 总页数: {doc.page_count}")
# 2. 提取全文文本
full_text = ""
for page in doc:
full_text += page.get_text()
return full_text
except Exception as e:
print(f" 读取失败: {e}")
return ""
# 模拟读取(如果没有真实文件,这里用长文本模拟)
raw_text = read_pdf_advanced("handbook.pdf")
# 如果没有文件,为了演示后续流程,我们使用这段模拟文本
if not raw_text:
raw_text = """
### 数据库初始化步骤
1. 首先,你需要配置环境变量。请确保在 .env 文件中填写正确的数据库地址。
2. 接下来,运行初始化脚本。在终端输入以下命令:python init_db.py --config default.json
3. 最后,检查日志。如果看到 "Success" 字样,说明启动成功。
"""
第二阶段:智能切分策略
拿到文本后,我们不能直接扔给 AI,必须切分。这里我们对比一下"暴力切分"和"智能切分"的区别。
环境准备:
由于 LangChain 的架构调整,切分器现在是一个独立的包。
bash
pip install langchain-text-splitters
切分策略对比:
| 策略 | 行为模式 | 结果预测 | 适用场景 |
|---|---|---|---|
| 固定长度切分 | 像切香肠一样,每 50 个字切一刀,不管是不是切断了单词。 | 容易出现 python initi 这种残缺代码,AI 无法理解。 |
处理纯乱码或无空格的特殊数据。 |
| 递归字符切分 | 优先找段落换行符 \n\n,找不到再找句号 。,最后才按字符切。 |
保持了句子和段落的完整性,AI 读起来更顺畅。 | 绝大多数 RAG 场景(推荐) |
代码实战:
注意看 chunk_overlap(重叠区)的设置,它就像"上集回顾",保证了上下文的连贯性。
你可以把 overlap 理解为"砌墙时的水泥"或者"拼图的重叠边" :
它不仅仅是为了回顾剧情。
更是为了粘合。它确保即使切分刀法再狠,关键的句子结构(比如主语、谓语)也不会被彻底切断,让 AI 在阅读每一个片段时,都能稍微"瞥见"一点前一个片段的内容。
python
from langchain_text_splitters import CharacterTextSplitter, RecursiveCharacterTextSplitter
print("=== 切分策略对比演示 ===\n")
# --- 策略 A:固定长度(反面教材) ---
# separator="" 意味着强制按字符切,不按语义
fixed_splitter = CharacterTextSplitter(
chunk_size=50,
chunk_overlap=0,
separator=""
)
fixed_chunks = fixed_splitter.split_text(raw_text)
print(" 固定长度切分 (注意看代码被切断了):")
print(f"[块1] -> {fixed_chunks[0]}")
print("-" * 50)
# --- 策略 B:递归切分(正确姿势) ---
recursive_splitter = RecursiveCharacterTextSplitter(
chunk_size=80, # 每个块的目标大小
chunk_overlap=20, # 重叠 20 个字符,保留上下文
separators=["\n\n", "\n", "。", " ", ""] # 切分优先级:段落 > 换行 > 句号 > 空格
)
recursive_chunks = recursive_splitter.split_text(raw_text)
print(" 递归智能切分 (语义完整,带有重叠):")
for i, chunk in enumerate(recursive_chunks):
print(f"[块 {i+1}] (长度: {len(chunk)}) -> {chunk}")
print("-" * 50)
第三阶段:向量入库(ChromaDB)
切分好的文本块,我们需要把它们存起来,并且要存成"向量"格式,以便进行语义搜索。
流程图:数据是如何入库的?
翻译成数字向量
存储
文本块
嵌入模型 Embedding Model
向量
ChromaDB 数据库
索引完成
代码实现:
这里我们需要安装 chromadb 和 sentence-transformers(用于提供嵌入模型)。
bash
pip install chromadb sentence-transformers
python
import chromadb
from sentence_transformers import SentenceTransformer
# 1. 初始化设置
# 创建一个本地持久化数据库,数据会保存在 ./rag_db 文件夹中
client = chromadb.PersistentClient(path="./rag_db")
collection = client.get_or_create_collection(name="employee_handbook")
# 加载嵌入模型 (这里使用一个轻量级的多语言模型)
embedding_model = SentenceTransformer('paraphrase-multilingual-MiniLM-L12-v2')
# 2. 数据入库
# 我们需要把文本转换成向量,然后存入 ChromaDB
print(f"\n 正在处理 {len(recursive_chunks)} 个文本块...")
documents = []
embeddings = []
ids = []
for i, chunk in enumerate(recursive_chunks):
# 生成向量 (将文本转化为计算机可计算的数字列表)
vector = embedding_model.encode(chunk).tolist()
documents.append(chunk)
embeddings.append(vector)
ids.append(f"chunk_{i}")
# 批量添加
collection.add(
documents=documents,
embeddings=embeddings,
ids=ids
)
print(" 成功入库!知识库已就绪。")
第四阶段:检索测试(开卷考试)
最后,我们模拟用户提问,验证系统是否真的"学会"了这些知识。
python
# 用户提问
query = "怎么初始化数据库?"
# 1. 将问题向量化
query_vector = embedding_model.encode(query).tolist()
# 2. 在 ChromaDB 中搜索最相似的 Top 1 结果
results = collection.query(
query_embeddings=[query_vector], # 注意:chromadb 要求传入列表
n_results=1
)
# 3. 展示结果
retrieved_text = results['documents'][0][0]
print(f"\n 用户提问:{query}")
print(f" 系统检索到的参考资料:\n{retrieved_text}")
总结
通过今天的实战,我们不仅解决了一个 PDF 读取的问题,更重要的是理清了 RAG 系统的"三驾马车":
- PyMuPDF:负责把非结构化数据(PDF)变成结构化文本。
- LangChain Text Splitters:负责把长文本变成 AI 能消化的语义块。
- ChromaDB:负责把这些块变成向量并建立索引。
现在,你的系统已经具备了"记忆"能力,下一步就是接入大模型(LLM),让它根据这些记忆来回答问题了!