前端转 Agent-【Python】-19 构建私有 RAG 知识库:让 AI 读懂你的所有文档(含精准 OCR 识别)
在前面的文章里,我们用 ChromaDB 存了几条模拟文档,让 Agent 能回答产品问题。
但真正的企业场景是:上百份 PDF 合同、Markdown 技术文档、Word 手册散落在文件夹里,其中很多还是扫描件 ------图片里的文字用普通 PDF 解析根本读不出来。
今天我们就来搭一个私有 RAG 知识库 ,把这些文档全部"喂"给 AI,并且加入图片文字精准识别(OCR)方案 ,全程数据不出本地,安全、免费。
如果你在 Vue 3 项目里做过文件上传 + 搜索,你会惊喜地发现:后端的处理管道完全对应前端的交互流程。
什么是私有 RAG 知识库?
RAG = 检索增强生成。私有 RAG 就是:
- 你的文档(PDF/Word/Markdown/TXT)→ 切片 → 向量化 → 存入本地向量库
- 用户提问 → 语义检索 → 把最相关的片段注入提示词 → 本地 LLM 生成答案
全程不需要 OpenAI,不需要联网,所有数据留在你的机器上。
Vue 3 类比 :你在前端做文件上传,后端存文件、建索引;用户在搜索框输入问题,后端返回 AI 生成的答案。我们这次写的就是整个后端管道。
环境准备
bash
pip install langchain langchain-ollama langchain-chroma pypdf python-docx unstructured
pip install pdf2image pytesseract paddlepaddle paddleocr # OCR 相关
ollama pull qwen3:4b
ollama pull nomic-embed-text
系统依赖(OCR 必备):
整体架构
ini
docs/ 文件夹
├── 产品手册.pdf ← 混合内容(文字 + 扫描页)
├── 技术白皮书.docx
└── 公司政策.md
↓
智能文档加载器(PyPDF 文本提取 + OCR 图片文字识别)
↓
文本分割器 (RecursiveCharacterTextSplitter)
↓
本地嵌入 (OllamaEmbeddings model="nomic-embed-text")
↓
ChromaDB 向量存储 (持久化到 ./my_knowledge_db)
↓
用户提问 → 检索相关文档片段 → 拼接上下文 → Qwen3:4b 生成答案
第一步:构建文档索引管道(含 OCR 增强)
我们写一个脚本,扫描指定文件夹,自动判断 PDF 是普通文字版还是扫描版,并精准识别图片中的文字 ,然后全部入库。只跑一次,或文档有更新时重跑。
python
# build_index.py ------ 构建私有知识库索引(含图片文字 OCR)
import os
import sys
sys.stdout.reconfigure(encoding='utf-8') # 看情况加
from langchain_ollama import OllamaEmbeddings
from langchain_chroma import Chroma
from langchain_community.document_loaders import (
PyPDFLoader,
Docx2txtLoader,
TextLoader,
UnstructuredMarkdownLoader,
)
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_core.documents import Document
# OCR 相关
from pdf2image import convert_from_path
import pytesseract
# 如果追求更高精度(尤其中文),可选用 PaddleOCR
# from paddleocr import PaddleOCR
# 1. 配置
_SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
DOCS_DIR = os.path.join(_SCRIPT_DIR, "docs")
CHROMA_DIR = os.path.join(_SCRIPT_DIR, "my_knowledge_db")
EMBEDDING_MODEL = "nomic-embed-text"
CHUNK_SIZE = 500
CHUNK_OVERLAP = 50
# ---------- OCR 精准识别模块 ----------
def extract_text_from_scanned_pdf(file_path: str) -> list[Document]:
"""
将扫描型 PDF 的每一页转为图片,用 OCR 提取文字。
返回 LangChain 的 Document 列表(每页一个 Document)。
"""
docs = []
try:
images = convert_from_path(file_path, dpi=300) # 提高 DPI 提升识别精度
for i, img in enumerate(images):
# 方案 A:Tesseract OCR(开箱即用,支持中英文需事先安装语言包)
text = pytesseract.image_to_string(img, lang='chi_sim+eng') # 中文+英文
# 方案 B:PaddleOCR(对中文更友好,布局分析更强,注释掉上面一行,取消下面注释)
# ocr = PaddleOCR(use_angle_cls=True, lang='ch', use_gpu=False, show_log=False)
# result = ocr.ocr(img, cls=True)
# text = "\n".join([line[1][0] for line in result[0]]) if result[0] else ""
if text.strip():
docs.append(Document(
page_content=text,
metadata={"source": file_path, "page": i+1, "type": "scanned_pdf"}
))
print(f"🖼️ OCR 完成: {file_path},共识别 {len(docs)} 页")
except Exception as e:
print(f"❌ OCR 失败 {file_path}: {e}")
return docs
def load_pdf_smart(file_path: str) -> list[Document]:
"""
智能加载 PDF:先用 PyPDF 提取文本,若文本过少(疑似扫描件),
则自动回退到 OCR 方案。
"""
# 先尝试普通文本提取
text_docs = []
try:
loader = PyPDFLoader(file_path)
text_docs = loader.load()
# 合并所有页面文字,判断有效字符数
full_text = "".join([d.page_content for d in text_docs])
# 如果平均每页有效字符少于 50,认为它是扫描版
if len(full_text) < 50 * max(1, len(text_docs)):
print(f"🔍 检测到 {file_path} 可能是扫描件,切换至 OCR")
return extract_text_from_scanned_pdf(file_path)
else:
print(f"📄 文本提取: {file_path}")
for doc in text_docs:
doc.metadata["source"] = file_path
doc.metadata["type"] = "text_pdf"
return text_docs
except Exception as e:
print(f"⚠️ PyPDF 失败,尝试 OCR: {e}")
return extract_text_from_scanned_pdf(file_path)
# ---------------------------------------
def load_documents(docs_dir: str) -> list[Document]:
all_docs = []
for root, _, files in os.walk(docs_dir):
for file in files:
path = os.path.join(root, file)
try:
if file.endswith(".pdf"):
# 使用智能加载器(自动识别文字/扫描件)
docs = load_pdf_smart(path)
elif file.endswith(".docx"):
docs = Docx2txtLoader(path).load()
for d in docs: d.metadata["source"] = path
elif file.endswith(".md"):
docs = UnstructuredMarkdownLoader(path).load()
for d in docs: d.metadata["source"] = path
elif file.endswith(".txt"):
docs = TextLoader(path, encoding="utf-8").load()
for d in docs: d.metadata["source"] = path
else:
continue
all_docs.extend(docs)
print(f"✅ 已加载: {file}")
except Exception as e:
print(f"❌ 加载失败 {file}: {e}")
return all_docs
# 2. 分割文档
raw_docs = load_documents(DOCS_DIR)
splitter = RecursiveCharacterTextSplitter(
chunk_size=CHUNK_SIZE,
chunk_overlap=CHUNK_OVERLAP,
separators=["\n\n", "\n", "。", ";", ",", " ", ""]
)
chunks = splitter.split_documents(raw_docs)
print(f"📊 共生成 {len(chunks)} 个文本块")
# 3. 向量化并存入 ChromaDB
embedding = OllamaEmbeddings(model=EMBEDDING_MODEL)
vectorstore = Chroma.from_documents(
documents=chunks,
embedding=embedding,
persist_directory=CHROMA_DIR
)
print(f"💾 向量库已保存到 {CHROMA_DIR}")
执行一次:
bash
python build_index.py
你会看到包含 OCR 的详细输出,扫描件会被自动识别并转换。
第二步:搭建 RAG 问答接口
索引建好后,问答环节和之前一样,但我们可以利用元数据告诉用户答案来自哪一页、是文字还是扫描件。
python
# ask_knowledge.py ------ 向私有知识库提问
from langchain_ollama import ChatOllama, OllamaEmbeddings
from langchain_chroma import Chroma
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
embedding = OllamaEmbeddings(model="nomic-embed-text")
vectorstore = Chroma(
persist_directory="./my_knowledge_db",
embedding_function=embedding
)
retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
llm = ChatOllama(model="qwen3:4b", temperature=0)
prompt = ChatPromptTemplate.from_template("""你是一个知识库助手,只能根据以下已知信息回答问题。
如果已知信息中找不到答案,请直接说"知识库中暂时没有相关信息",不要编造。
已知信息:
{context}
用户问题:{question}
回答:""")
def format_docs(docs):
formatted = []
for doc in docs:
source = doc.metadata.get('source', '未知')
page = doc.metadata.get('page', '?')
doc_type = doc.metadata.get('type', '文档')
formatted.append(f"【来源:{source} 第{page}页 ({doc_type})】\n{doc.page_content}")
return "\n\n".join(formatted)
rag_chain = (
{"context": retriever | format_docs, "question": RunnablePassthrough()}
| prompt
| llm
| StrOutputParser()
)
if __name__ == "__main__":
print("📚 私有知识库问答系统 (输入 /bye 退出)")
while True:
q = input("\n🧑 你的问题: ")
if q == "/bye":
break
answer = rag_chain.invoke(q)
print(f"🤖 回答: {answer}")
第三步:增量更新与自动重索引(可选)
和之前一样,重新运行 build_index.py 即可。OCR 部分会再次识别扫描件,若有更新自动覆盖。
Vue 3 横向对比
| RAG 流程 | Vue 3 前端类似操作 |
|---|---|
| 选择文件夹、上传文档 | <input type="file" webkitdirectory> 或拖拽上传组件 |
| 显示索引进度(含 OCR 进度) | ElProgress 组件 + SSE 推送 |
| 提问并显示答案 | 搜索框 + 结果列表,类似 ChatGPT 对话界面 |
| 显示引用来源(含页码、类型) | 答案下方用 v-for 列出源文件,可点击预览对应页面 |
| 后端管道 | Pinia action 调用 FastAPI 接口 POST /ask |
前端开发者完全可以基于这套 Python 管道,用 FastAPI 封装 POST /upload(接收文件并触发重索引)和 POST /ask,打造一个内部 AI 知识库平台。
进阶:更精准的 OCR 方案对比
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Tesseract + chi_sim | 离线免费,部署简单 | 密集文字、特殊排版易出错 | 清晰扫描件、简单表格 |
| PaddleOCR | 中文识别极强,支持倾斜、密集文字 | 安装稍重,首次运行需下载模型 | 复杂排版、手写体、合同扫描件 |
| Surya OCR | 多语言、检测行级准确 | 需要 GPU 加速才快 | 学术论文、多栏 PDF |
| Ollama 视觉模型 | 端到端,无需单独 OCR,理解上下文 | 慢,成本高,不适合大规模索引 | 少量图片实时问答 |
推荐组合 :批量索引用 PaddleOCR(精准+纯本地),实时图片问答才考虑视觉模型。
总结
- 私有 RAG 是让 AI 理解企业内部文档的最实用方案,全程本地运行,数据安全。
- 今天加入了 图片文字精准识别(OCR),自动区分文本 PDF 和扫描件,补齐了真实场景的最大短板。
- 整体流程 = 智能加载(文本+OCR)→ 分割 → 嵌入 → 存储 → 检索 → 生成。
- 前端开发者可以轻松把这套管道包装成 Web 应用,做一个内部"ChatGPT"。
- 结合之前学的人机协同、可视化、评估,你完全可以交付一个生产级的企业知识库系统。
现在就去把你电脑里积灰的文档文件夹"喂"给 AI 吧,让它成为你的第二大脑!
bash
python build_index.py # 先建索引(自动 OCR 扫描件)
python ask_knowledge.py # 再提问