标签:Python、RAG、大模型、Ollama、ChromaDB、本地知识库
一、项目简介
检索增强生成(RAG)是当前大模型落地应用最主流的方案。本项目全程本地离线运行,无需调用任何云端 API,不受 Token 额度限制,同时能有效保护本地文档隐私。
本文以 PDF 简历 作为实战案例,完整落地标准 RAG 全流程:PDF文档读取 → 文本切片 → 本地Embedding向量化 → 向量库持久化存储 → 相似度检索 → 本地大模型生成答案
技术栈
-
PDF 解析:
pdfplumber,解决传统 PDF 库末尾文字丢失、排版错乱问题 -
文本处理:固定长度切片 + 重叠分片,保证语义完整性
-
向量数据库:
ChromaDB,轻量易上手、支持数据持久化 -
向量模型:
Qwen3-Embedding-0.6B(通义千问嵌入模型,Ollama 本地部署) -
对话大模型:
qwen:7b(通义千问 7B 开源模型) -
环境管理:
python-dotenv,统一管理环境变量
核心功能
-
逐页解析 PDF,构建文本块与页码映射,支持内容溯源
-
自定义切片大小与重叠长度,避免长文本语义割裂
-
向量库自动检测,已存在则直接加载,防止重复入库
-
交互式循环问答,模型仅依据文档内容作答,不编造信息
-
全流程离线运行,一次部署可长期使用
二、环境部署
1. 安装 Python 依赖库
打开终端,执行以下命令安装项目所需第三方库:
pip install pdfplumber chromadb ollama python-dotenv
2. 安装 Ollama
Ollama 跨平台支持 Windows / Mac / Linux,可一键本地运行各类大模型、向量模型。
-
官网下载对应系统安装包:https://ollama.com/
-
安装完成后,终端执行命令验证是否安装成功:
ollama --version
3. 拉取本地模型
国内网络环境建议延长超时时间,避免大模型下载中断:
# Mac/Linux 临时设置超时时间(网络不稳定必加)export OLLAMA_TIMEOUT=600# 拉取通义千问 Embedding 向量模型
ollama pull dengcao/Qwen3-Embedding-0.6B:Q8_0
# 拉取通义千问 7B 对话大模型
ollama pull qwen:7b
4. 项目目录结构
将 PDF 文件、代码、环境文件放在同一目录下,参考结构:
./
├── xxx.pdf # 待解析的PDF知识库文档
├── .env # 环境变量文件(本项目可置空)
└── rag_pdf_qa.py # 项目主程序
三、完整可运行代码
代码附带详细注释,逻辑分层清晰,直接复制即可使用:
python
# ==========================
# 一、全局配置项
# 所有可修改的参数统一放在这里,便于维护
# ==========================
PDF_PATH = "./xxx.pdf" # 你的PDF文件路径
CHROMA_PATH = "./chroma_db" # 向量数据库存储目录
EMBED_MODEL = "dengcao/Qwen3-Embedding-0.6B:Q8_0" # Ollama向量模型
LLM_MODEL = "qwen:7b" # 对话大模型
CHUNK_SIZE = 300 # 文本切片长度
CHUNK_OVERLAP = 30 # 切片重叠长度,保证语义完整
TOP_K = 3 # 检索返回最相似的3条片段
# ==========================
# 二、导入依赖库
# ==========================
import pdfplumber # PDF解析工具
import os # 文件/目录操作
import pickle # 持久化存储页码映射
import chromadb # 轻量向量数据库
import ollama # 本地大模型调用接口
from dotenv import load_dotenv # 环境变量加载
# 加载.env环境变量,支持本地配置
load_dotenv("./.env", override=True)
# ==========================
# 三、PDF文档解析工具类
# 功能:读取PDF,按页提取文本 + 页码
# ==========================
class PDFParser:
@staticmethod
def extract_pages(pdf_path):
"""
从PDF文件中逐页提取文本
:param pdf_path: PDF路径
:return: 每页文本列表 + 对应页码列表
"""
page_texts = []
page_nums = []
# 打开PDF并逐页读取
with pdfplumber.open(pdf_path) as pdf:
for page_num, page in enumerate(pdf.pages, start=1):
text = page.extract_text() or "" # 提取文本,空则设为""
page_texts.append(text.strip()) # 去除多余空格
page_nums.append(page_num) # 记录页码
return page_texts, page_nums
# ==========================
# 四、文本向量化工具类
# 功能:调用Ollama生成文本向量
# ==========================
class Embedding:
@staticmethod
def get_embeddings(text_list):
"""
批量生成文本向量
:param text_list: 文本列表
:return: 向量列表
"""
resp = ollama.embed(
model=EMBED_MODEL,
input=text_list
)
return resp.embeddings
# ==========================
# 五、向量数据库管理类
# 功能:切片、入库、检索、持久化、页码映射
# ==========================
class VectorDB:
def __init__(self):
# 初始化Chroma客户端(持久化存储)
self.client = chromadb.PersistentClient(CHROMA_PATH)
self.collection = self.client.get_or_create_collection("knowledge_base")
def build_db(self, page_texts, page_nums):
"""
构建向量库:切片 → 向量化 → 入库
如果库已存在,则直接加载,不重复构建
"""
# 尝试加载已存在的向量库
try:
with open(f"{CHROMA_PATH}/chunk_page_mapping.pkl", "rb") as f:
print("✅ 向量库已存在,直接加载")
return pickle.load(f)
except:
print("📦 未检测到向量库,开始切片、向量化、入库...")
all_chunks = []
chunk_page_map = {}
# 遍历每页文本进行切片
for text, page in zip(page_texts, page_nums):
text = text.strip()
if not text:
continue
# 固定长度切片(带重叠)
start = 0
text_len = len(text)
while start < text_len:
chunk = text[start:start + CHUNK_SIZE].strip()
if chunk:
all_chunks.append(chunk)
chunk_page_map[chunk] = page # 记录文本块对应页码
start += CHUNK_SIZE - CHUNK_OVERLAP
# 批量生成向量
vectors = Embedding.get_embeddings(all_chunks)
# 存入向量库
self.collection.add(
ids=[str(i) for i in range(len(all_chunks))],
documents=all_chunks,
embeddings=vectors
)
# 保存文本块-页码映射文件
os.makedirs(CHROMA_PATH, exist_ok=True)
with open(f"{CHROMA_PATH}/chunk_page_mapping.pkl", "wb") as f:
pickle.dump(chunk_page_map, f)
print(f"✅ 向量库构建完成,总文本块数量:{len(all_chunks)}")
return chunk_page_map
def search(self, query):
"""
根据用户问题检索相似文本
:param query: 用户问题
:return: 匹配到的文档片段
"""
# 问题向量化
q_vector = Embedding.get_embeddings([query])
# 向量相似度检索
res = self.collection.query(query_embeddings=q_vector, n_results=TOP_K)
return res["documents"][0]
# ==========================
# 六、RAG问答引擎
# 功能:检索 + 构造Prompt + 调用大模型生成答案
# ==========================
class RAG:
def __init__(self):
self.db = VectorDB() # 初始化向量库
def ask(self, question):
"""
对外提供问答接口
:param question: 用户问题
:return: 模型回答 + 参考片段
"""
# 1. 检索相关文档
docs = self.db.search(question)
# 2. 拼接上下文
context = "\n".join(docs)
# 3. 构造提示词(严格约束模型不编造)
prompt = f"""
你是专业的简历问答助手,必须严格依据提供的简历内容回答,不允许编造信息。
回答简洁、准确、有条理。
简历内容:
{context}
用户问题:{question}
助手回答:
"""
# 4. 调用本地大模型生成回答
resp = ollama.chat(
model=LLM_MODEL,
messages=[{"role": "user", "content": prompt}]
)
return resp["message"]["content"], docs
# ==========================
# 七、主程序入口
# 流程:解析PDF → 构建向量库 → 启动循环问答
# ==========================
if __name__ == "__main__":
print("=" * 50)
print("🔥 PDF 本地 RAG 智能问答系统(离线版)")
print("=" * 50)
# 1. 解析PDF文档
parser = PDFParser()
texts, nums = parser.extract_pages(PDF_PATH)
# 2. 初始化/加载向量库
db = VectorDB()
db.build_db(texts, nums)
# 3. 启动RAG问答引擎
rag = RAG()
# 4. 循环交互问答
while True:
query = input("\n🗣 请输入你的问题(输入 q 退出):")
if query.lower() == "q":
print("👋 感谢使用,再见!")
break
if not query.strip():
print("⚠️ 请输入有效问题!")
continue
# 获取回答
answer, docs = rag.ask(query)
# 输出结果
print("\n🤖 回答:", answer)
print("\n📄 参考片段:")
for i, d in enumerate(docs, 1):
print(f"{i}. {d[:80]}...")
四、运行说明
-
将待解析的 PDF 文件重命名为 xxx
.pdf,和代码放在同一目录; -
确保 Ollama 服务正常运行,对应模型已成功拉取;
-
直接执行 Python 脚本,首次运行会自动完成 PDF 解析、文本切片、向量化与入库;
-
程序启动后进入交互模式,输入问题即可问答,输入
q退出程序; -
再次运行脚本会自动加载已有向量库,不会重复处理文档。
五、常见问题解决
1. Ollama 拉取模型提示 context deadline exceeded
网络不稳定导致超时,执行命令延长超时时间后重新拉取:
python
export OLLAMA_TIMEOUT=600
ollama pull 模型名称
2. 大模型无响应、卡死
-
检查设备显存 / 内存是否充足,7B 模型建议预留 4G 以上空闲资源;
-
可替换为轻量化模型
qwen:3b/qwen2:1.5b,推理速度更快; -
重启 Ollama 服务:
pkill ollama && ollama serve &。
3. 向量库数据异常
删除项目下 chroma_db 文件夹,重新运行代码重建向量库即可。
六、项目拓展方向
-
支持多 PDF 文档批量导入,搭建通用本地知识库;
-
增加语义重排、摘要优化,提升问答精准度;
-
接入 Web 界面,实现可视化问答;
-
更换不同 Embedding 模型与大模型,对比检索、生成效果。
七、总结
本项目参照 RAG实现原理,基于开源组件实现离线本地部署,无需依赖任何第三方云端接口。不消耗Token,可以帮助我们快速熟悉RAG底层原理,为后续LangChain等学习打下基础。