Python 本地 RAG 实战 | Ollama+ChromaDB 实现 PDF 离线智能问答

标签: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,统一管理环境变量

核心功能

  1. 逐页解析 PDF,构建文本块与页码映射,支持内容溯源

  2. 自定义切片大小与重叠长度,避免长文本语义割裂

  3. 向量库自动检测,已存在则直接加载,防止重复入库

  4. 交互式循环问答,模型仅依据文档内容作答,不编造信息

  5. 全流程离线运行,一次部署可长期使用


二、环境部署

1. 安装 Python 依赖库

打开终端,执行以下命令安装项目所需第三方库:

复制代码
pip install pdfplumber chromadb ollama python-dotenv

2. 安装 Ollama

Ollama 跨平台支持 Windows / Mac / Linux,可一键本地运行各类大模型、向量模型。

  1. 官网下载对应系统安装包:https://ollama.com/

  2. 安装完成后,终端执行命令验证是否安装成功:

    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]}...")

四、运行说明

  1. 将待解析的 PDF 文件重命名为 xxx.pdf,和代码放在同一目录;

  2. 确保 Ollama 服务正常运行,对应模型已成功拉取;

  3. 直接执行 Python 脚本,首次运行会自动完成 PDF 解析、文本切片、向量化与入库;

  4. 程序启动后进入交互模式,输入问题即可问答,输入 q 退出程序;

  5. 再次运行脚本会自动加载已有向量库,不会重复处理文档。


五、常见问题解决

1. Ollama 拉取模型提示 context deadline exceeded

网络不稳定导致超时,执行命令延长超时时间后重新拉取:

python 复制代码
export OLLAMA_TIMEOUT=600
ollama pull 模型名称

2. 大模型无响应、卡死

  1. 检查设备显存 / 内存是否充足,7B 模型建议预留 4G 以上空闲资源;

  2. 可替换为轻量化模型 qwen:3b / qwen2:1.5b,推理速度更快;

  3. 重启 Ollama 服务:pkill ollama && ollama serve &

3. 向量库数据异常

删除项目下 chroma_db 文件夹,重新运行代码重建向量库即可。


六、项目拓展方向

  1. 支持多 PDF 文档批量导入,搭建通用本地知识库;

  2. 增加语义重排、摘要优化,提升问答精准度;

  3. 接入 Web 界面,实现可视化问答;

  4. 更换不同 Embedding 模型与大模型,对比检索、生成效果。


七、总结

本项目参照 RAG实现原理,基于开源组件实现离线本地部署,无需依赖任何第三方云端接口。不消耗Token,可以帮助我们快速熟悉RAG底层原理,为后续LangChain等学习打下基础。

相关推荐
骑士雄师1 小时前
18.1 星系案例:多智能体宇宙探索系统(学习langgraph 的存储知识)
windows·python·学习
slandarer1 小时前
MATLAB | 韦恩图的高阶版: UpSet图 更新升级啦!
开发语言·matlab
m沐沐1 小时前
【深度学习】PyTorch CNN 手写数字识别(卷积神经网络)
人工智能·pytorch·python·深度学习·机器学习·pycharm·cnn
garmin Chen1 小时前
Prompt工程入门:让AI按你的要求工作(3)--Prompt工程与提示词安全评测概述
java·人工智能·python·安全·prompt
Leweslyh1 小时前
3GPP TS 28.312 意图驱动管理服务 — 极详细通俗解读
开发语言·php
nanawinona1 小时前
只会用 K 线算期货信号下一步怎么接到交易
python·区块链
swordbob1 小时前
Spring事务失效的场景
java·开发语言·spring
王莎莎-MinerU1 小时前
从 OCR 到 Context Engineering:用 MinerU 搭一个可复现文档解析评测
人工智能·深度学习·机器学习·pdf·ocr·个人开发
叫我:松哥1 小时前
基于卷积神经网络的静态手势语识别算法,在测试集上的识别准确率达到97.5%
人工智能·python·深度学习·神经网络·算法·cnn