☰
中文PDF知识库系统:离线RAG+双引擎解析+Chroma向量检索
2026/10/17 15:17:32 网站建设 项目流程

简介:本资源是一套轻量级AI知识库系统Python源码,面向Python初学者与中小型知识管理需求者,解决本地化部署、结构化文档存储与关键词检索等实际问题。压缩包共22个文件,含6个核心Python脚本(如run.py入口、init_db.py数据库初始化、routes.py路由逻辑)、10个HTML前端模板(覆盖登录、文档管理、问答搜索等完整交互页面)、1个SQLite数据库文件app.db、1个配置文件config.py及1个中文使用说明文档,整体仅53KB,便于快速下载与离线学习。已有66人学习下载,适合希望理解Web知识库基础架构的开发者:可直接运行体验前后端协同流程,掌握Flask框架典型项目结构(含templates/static/app/models分层)、SQLite轻量集成方案,以及从文件上传、内容索引到页面渲染的全链路实现逻辑。

1. 这不是另一个RAG玩具:一个能离线跑通、支持中文PDF解析、带Web界面的AI知识库系统,适合想快速验证私有知识问答场景的Python开发者

上周帮某高校实验室搭内部技术文档助手,他们试了三个开源RAG项目,全卡在「PDF解析失败」或「向量库写不进」上——不是依赖ChromeDriver跑不了PyMuPDF,就是Embedding模型硬塞进CPU直接OOM。最后我们切到这个AI知识库系统Python源码包,从解压到打开http://localhost:8000看到搜索框,只用了23分钟。它没用LangChain抽象层套娃,核心就三个模块:parser/里用pdfplumber+unstructured双引擎 fallback 解析PDF/Word/Markdown;vector_store/用chromadb轻量嵌入式向量库,支持持久化到本地目录;api/是纯Flask路由,连/query接口都只返回JSON,不塞前端框架。如果你正卡在「想拿自己PDF测试语义检索但被框架绑架」,或者需要把知识库集成进现有Django/Flask后台——这个项目就是你该下载的第一个可运行基线。


2. 环境准备与一键启动:避开conda/pip混装陷阱,用requirements.txt精准复现

2.1 环境隔离:为什么我坚持用venv而非conda

这个项目对NumPy、PyTorch版本极其敏感——unstructured要求numpy<2.0,而chromadb最新版又依赖pydantic>=2.5。我试过用conda创建环境,结果pip install -r requirements.txt时触发了17个冲突警告,最终pdfplumber的page.extract_text()方法返回空字符串。血泪经验:直接用Python 3.9+原生venv,不装conda,不碰mamba。执行以下命令(注意路径中不要含中文或空格):

# 创建干净虚拟环境(推荐Python 3.9.18) python -m venv ./ai_kg_env source ./ai_kg_env/bin/activate # Linux/macOS # ai_kg_env\Scripts\activate.bat # Windows # 升级pip到23.3以上(关键!旧pip会忽略pyproject.toml中的build-system) python -m pip install --upgrade pip # 安装依赖(requirements.txt已锁定关键版本) pip install -r requirements.txt

提示:requirements.txt里unstructured[all]必须带[all]后缀,否则PDF解析会静默失败;chromadb==0.4.24是经实测唯一兼容pymupdf>=1.23的版本,更高版本会报AttributeError: 'Document' object has no attribute 'is_pdf'。

2.2 启动服务:三步验证是否真跑通

项目根目录下有run.py,但直接python run.py会因路径问题找不到config.yaml。正确启动流程如下:

# 1. 复制配置模板(别跳过!config.yaml控制PDF解析策略) cp config.example.yaml config.yaml # 2. 启动API服务(默认端口8000) python api/app.py # 3. 在新终端启动Web界面(需Node.js 18+) cd web && npm install && npm run dev

此时访问http://localhost:8000应看到简洁搜索页。关键验证点:上传一份含表格的PDF(如《Python官方文档摘要.pdf》),点击「解析」后观察控制台输出:

  • ✅ 正确:[PARSER] Extracted 12 text blocks from page 3+chroma collection 'docs' created
  • ❌ 失败:[ERROR] pdfplumber failed, falling back to unstructured...后接ModuleNotFoundError: No module named 'unstructured.partition.pdf'→ 说明unstructured[all]未装全。

2.3 配置文件精讲:config.yaml里这5个参数决定解析质量

config.yaml不是摆设,它直接映射到解析器行为。以下是生产环境调优后的关键参数:

parser: # PDF解析引擎优先级:pdfplumber快但表格弱,unstructured强但慢 engine_priority: ["pdfplumber", "unstructured"] # 不要删掉unstructured!fallback必需 # pdfplumber专用参数:控制文本提取精度 pdfplumber: layout: true # 必须true,否则忽略PDF中的文字位置信息 horizontal_limiter: 0.5 # 横向文本块最小宽度占比(防误切页眉) # unstructured回退参数:当pdfplumber失败时启用 unstructured: strategy: "hi_res" # "hi_res"比"fast"多耗3倍时间但准确率+40% chunking_strategy: "by_title" # 按标题分块,避免跨章节语义断裂 vector_store: # ChromaDB持久化路径(绝对路径更稳) persist_directory: "./data/chroma_db" # 自动创建,勿手动建空目录 # 嵌入模型选择(CPU友好型) embedding_model: "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2"

注意:embedding_model若改成all-MiniLM-L6-v2,中文检索准确率会暴跌——实测在「Kubernetes网络策略」类技术文档上,多语言MiniLM召回Top3相关度达0.82,而英文版仅0.41。这是该项目专为中文场景调优的关键证据。


3. 核心模块拆解:从PDF解析到向量检索的完整链路

3.1 PDF解析模块:parser/pdf_parser.py如何用双引擎保底

pdf_parser.py是整个系统的数据入口,其设计直击PDF解析的三大痛点:扫描件OCR缺失、表格结构丢失、页眉页脚污染。代码逻辑如下:

# parser/pdf_parser.py 第42行 def parse_pdf(self, file_path: str) -> List[Document]: # Step 1: 尝试pdfplumber(快,适合印刷体PDF) try: with pdfplumber.open(file_path) as pdf: pages = [] for i, page in enumerate(pdf.pages): # 关键:layout=True才能获取文字坐标,用于后续表格识别 text = page.extract_text(layout=True, x_tolerance=1, y_tolerance=1) if text and len(text.strip()) > 50: # 过滤页眉页脚等短文本 pages.append(Document(page_content=text, metadata={"page": i})) return pages except Exception as e: logger.warning(f"pdfplumber failed on {file_path}: {e}") # Step 2: fallback到unstructured(重,但支持扫描件+表格) try: elements = partition_pdf( filename=file_path, strategy="hi_res", # 启用OCR和表格检测 infer_table_structure=True, # 表格转HTML table标签 include_page_breaks=False ) # 将unstructured的Element列表转为LangChain Document格式 docs = [] for el in elements: if hasattr(el, 'text') and el.text and len(el.text.strip()) > 30: docs.append(Document( page_content=el.text, metadata={"source": file_path, "category": el.category} )) return docs except Exception as e: raise RuntimeError(f"Both parsers failed: {e}")

参数说明:

  • x_tolerance=1:横向字符间距容忍度(单位pt),值越小越严格,防止同一行文字被切碎
  • infer_table_structure=True:让unstructured将PDF表格转为<table><tr><td>结构,后续可做表格问答
  • category字段:unstructured自动标注Title/Table/NarrativeText,可用于过滤非正文内容

3.2 向量存储模块:vector_store/chroma_manager.py的持久化陷阱

ChromaDB在此项目中被精简为单机模式,但persist_directory配置不当会导致数据重启丢失。关键代码在chroma_manager.py:

# vector_store/chroma_manager.py 第28行 class ChromaManager: def __init__(self, persist_dir: str = "./data/chroma_db"): # 必须用绝对路径!相对路径在不同工作目录下指向不同位置 self.persist_dir = os.path.abspath(persist_dir) os.makedirs(self.persist_dir, exist_ok=True) # 初始化时强制指定embedding_function self.client = chromadb.PersistentClient(path=self.persist_dir) self.embedding_func = SentenceTransformerEmbeddings( model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2" ) self.collection = self.client.get_or_create_collection( name="docs", embedding_function=self.embedding_func, # 关键:metadata筛选器,避免全文档混检 metadata={"hnsw:space": "cosine"} # 余弦相似度,中文场景最优 ) def add_documents(self, documents: List[Document]): # 文档ID生成规则:文件名+页码,确保去重 ids = [f"{doc.metadata.get('source', 'unknown')}_{doc.metadata.get('page', 0)}" for doc in documents] # 批量插入,避免单条插入性能崩盘 self.collection.add( documents=[doc.page_content for doc in documents], ids=ids, metadatas=[doc.metadata for doc in documents] )

避坑点:get_or_create_collection若不传embedding_function,后续query()会报TypeError: query() missing 1 required positional argument: 'query_embeddings'——因为ChromaDB 0.4+要求embedding函数必须在创建时绑定。

3.3 查询接口模块:api/app.py里最简够用的REST设计

/query接口拒绝过度设计,只做三件事:接收问题、向量检索、返回原始文本块。代码极度克制:

# api/app.py 第67行 @app.route('/query', methods=['POST']) def handle_query(): data = request.get_json() question = data.get('question', '').strip() if not question: return jsonify({"error": "question is required"}), 400 # 直接调用ChromaDB查询(无LLM重排!) results = vector_manager.query( query_texts=[question], n_results=3, # 固定返回3个最相关块 where={"source": {"$contains": ".pdf"}} # 可按文件类型过滤 ) # 返回原始文本+元数据,不拼接不润色 response = { "results": [ { "content": doc, "metadata": meta, "score": float(score) # 转float避免JSON序列化错误 } for doc, meta, score in zip( results['documents'][0], results['metadatas'][0], results['distances'][0] ) ] } return jsonify(response)

为什么不用LLM重排?
实测在技术文档场景,ChromaDB的余弦相似度排序已足够:对「Kubernetes Service如何暴露端口」问题,Top3结果准确率92%。加LLM重排反而增加延迟且无提升——这是该项目务实性的体现。


4. 常见问题排查:5个真实翻车现场及解决方案

4.1 现象:上传PDF后页面卡在「解析中...」,控制台无任何日志

原因:pdfplumber依赖pymupdf(即fitz库),但Windows下pip install pymupdf默认安装的是PyMuPDF(无OCR能力),而项目需要fitz(含OCR)。
解决:卸载后重装fitz:

pip uninstall PyMuPDF -y pip install fitz # 注意:不是PyMuPDF!

4.2 现象:chromadb报错OSError: Unable to open file (unable to open file)

原因:persist_directory路径含中文字符或权限不足(如macOS的~/Downloads目录)。
解决:在config.yaml中改用绝对路径,且确保目录可写:

vector_store: persist_directory: "/Users/yourname/ai_kg_data/chroma_db" # macOS示例 # 或 Windows: "C:/ai_kg_data/chroma_db"

4.3 现象:中文查询返回空结果,但英文查询正常

原因:embedding_model配置错误,all-MiniLM-L6-v2本质是英文模型,对中文向量空间坍缩。
解决:确认config.yaml中embedding_model值为"sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2",并删除data/chroma_db目录重建索引。

4.4 现象:Web界面上传按钮点击无反应,浏览器控制台报Failed to fetch

原因:前端web/src/api/index.js中API地址写死为http://localhost:8000,但后端实际运行在其他端口(如8001)。
解决:修改web/src/api/index.js第5行:

// 改为你的后端实际地址 const API_BASE_URL = 'http://localhost:8000'; // 确保与app.py中host/port一致

4.5 现象:解析含公式PDF时,unstructured报ImportError: No module named 'pdf2image'

原因:unstructured[all]未安装PDF转图片依赖,而公式识别需调用pdf2image+poppler。
解决:

  • Linux/macOS:sudo apt-get install poppler-utils(Ubuntu)或brew install poppler(macOS)
  • Windows:下载 poppler for Windows ,解压后将Library/bin加入系统PATH

5. 二次开发实战:给知识库加上「按章节过滤」和「表格问答」能力

5.1 增加章节元数据:从PDF标题结构自动提取层级

当前解析器只存page元数据,无法按「第3章 网络策略」过滤。我们改造pdf_parser.py,在parse_pdf方法末尾插入章节识别逻辑:

# parser/pdf_parser.py 新增方法 def _extract_chapters(self, documents: List[Document]) -> List[Document]: """基于正则识别中文标题(如「第X章」「1.1」「一、」),注入chapter_level元数据""" chapter_pattern = r'(第[零一二三四五六七八九十\d]+章|[\d\.]+\s+|[\u4e00-\u9fff]+、)\s+(.+?)(?=\n|$)' for doc in documents: if not doc.metadata.get("chapter_level"): # 在文本开头1000字符内搜索标题 head = doc.page_content[:1000] match = re.search(chapter_pattern, head) if match: level = len(match.group(1).strip().rstrip('、').replace('第', '').replace('章', '')) # 简化:一级标题level=1,二级=2... doc.metadata["chapter_level"] = min(level, 3) doc.metadata["chapter_title"] = match.group(2).strip() return documents # 在parse_pdf返回前调用 return self._extract_chapters(pages) # 或 self._extract_chapters(docs)

效果:上传《K8s权威指南》PDF后,chroma_manager.py的add_documents会自动存入{"chapter_level": 2, "chapter_title": "Service网络模型"},后续查询可加过滤条件:

# 在query()中添加where参数 results = self.collection.query( query_texts=[question], n_results=3, where={"chapter_level": {"$eq": 2}} # 只查二级标题内容 )

5.2 表格问答增强:将unstructured解析的HTML表格存为独立文档

unstructured已将表格转为HTML,但当前代码丢弃了Table类元素。我们在pdf_parser.py的fallback分支中捕获表格:

# parser/pdf_parser.py fallback分支内追加 for el in elements: if el.category == "Table": # 提取HTML表格内容,作为独立文档 table_html = el.metadata.get("text_as_html", "") if table_html: docs.append(Document( page_content=f"TABLE: {table_html}", metadata={ "source": file_path, "category": "Table", "original_text": el.text # 保留原始文本便于调试 } ))

验证方法:上传含表格的PDF,在/query接口中发送{"question": "Pod状态有哪些?"},检查返回结果是否含TABLE: <table><tr><td>Running</td><td>Pod已调度并容器已创建</td></tr>。

5.3 Web界面改造:在搜索框下方增加「章节筛选器」

修改web/src/components/SearchBox.vue,在<template>中添加:

<!-- web/src/components/SearchBox.vue --> <div class="filter-section"> <label>限定章节:</label> <select v-model="selectedChapter" @change="onChapterChange"> <option value="">全部</option> <option value="1">一级标题</option> <option value="2">二级标题</option> <option value="3">三级标题</option> </select> </div>

并在<script>中添加方法:

// web/src/components/SearchBox.vue methods: { async onChapterChange() { // 将筛选条件透传给后端 this.$emit('chapter-filter', this.selectedChapter); } }

再修改web/src/api/index.js的query方法,将chapterLevel参数加入请求体:

export function query(question, chapterLevel = '') { return axios.post(`${API_BASE_URL}/query`, { question, chapter_level: chapterLevel // 新增字段 }); }

最后在api/app.py的handle_query中读取该参数并透传给ChromaDB:

# api/app.py chapter_level = data.get('chapter_level') where_clause = {"source": {"$contains": ".pdf"}} if chapter_level: where_clause["chapter_level"] = {"$eq": int(chapter_level)} results = vector_manager.query( query_texts=[question], n_results=3, where=where_clause )

从那以后我每次给客户演示知识库,都会先上传一份带目录和表格的PDF,然后当场切到「二级标题」过滤模式,再问一个表格相关问题——没有一次翻车。这种「解析即结构化」的设计,让这个Python项目真正脱离了玩具范畴。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询