手把手搭建本地RAG智能问答系统:从文档处理到Web部署全流程

📅 2026/8/10 10:50:27
手把手搭建本地RAG智能问答系统:从文档处理到Web部署全流程
在业务中快速搭建一个能理解企业私有文档的智能问答系统是很多开发团队面临的实际需求。直接调用云端大模型API虽然方便但涉及敏感数据外传、网络延迟和持续成本等问题。本文将手把手带你实现一个完全运行在本地的AI客服知识库从环境搭建、文档处理、模型选择到Web界面部署全程数据不出本机并提供完整的代码和避坑指南。无论你是想学习RAG检索增强生成技术还是需要为内部系统集成一个安全的智能助手这篇教程都能提供一套可落地的闭环方案。1. 项目概述与核心架构1.1 什么是本地AI知识库本地AI知识库的核心思想是“检索增强生成”Retrieval-Augmented Generation, RAG。它不要求大模型记住所有知识而是将用户的提问与本地知识库通常是企业文档、产品手册等进行匹配找到最相关的文档片段然后将这些片段和问题一起提交给大模型让大模型基于这些“参考资料”来生成答案。这样既保证了答案的准确性又避免了大模型“胡言乱语”的问题。为什么选择本地部署数据安全所有文档处理、向量化、问答交互均在本地完成敏感数据无需上传至任何第三方服务器。成本可控无需为API调用支付持续费用一次性的硬件投入或利用现有服务器后边际成本极低。网络与性能不受网络波动影响响应速度更快尤其适合内网环境。定制化强可以自由选择嵌入模型、大语言模型和向量数据库完全掌控技术栈。1.2 系统架构设计一个典型的本地RAG系统包含以下几个核心组件我们将基于此设计我们的实现方案用户提问 ↓ [Web前端/客户端] ↓ [后端服务] → (1) 将提问转换为向量 ↓ [向量数据库] ← (2) 检索最相关的文档片段 ↓ [后端服务] → (3) 组合“提问片段”形成提示词 ↓ [本地大语言模型] → (4) 生成最终答案 ↓ [后端服务] → 将答案返回给前端 ↓ 用户获得答案组件选型说明文档加载与切分使用LangChain的DocumentLoader和TextSplitter支持PDF、Word、TXT等多种格式。文本向量化嵌入模型使用轻量级且效果不错的开源模型如BAAI/bge-small-zh-v1.5它专门针对中文优化可以在消费级GPU甚至CPU上运行。向量数据库选用ChromaDB它轻量、易用、无需单独服务适合本地快速部署。大语言模型选用Ollama管理的模型。Ollama可以方便地在本地拉取和运行多种开源模型如Qwen2.5:7b、Llama 3.2等根据硬件能力选择。后端框架使用FastAPI轻量、异步、适合构建API。前端界面使用Gradio或Streamlit快速构建一个交互式Web界面。2. 环境准备与工具安装2.1 基础环境要求操作系统Windows 10/11, macOS, 或 Linux (Ubuntu 20.04 推荐)。本文以 Ubuntu 22.04 为例其他系统命令略有不同。Python版本 3.9 或 3.10。建议使用conda或venv创建独立的虚拟环境。内存至少 8GB RAM。如果运行7B参数的大模型推荐16GB以上。存储至少10GB可用空间用于存放模型和文档。GPU可选但推荐拥有至少6GB显存的NVIDIA GPU可以显著提升嵌入模型和LLM的推理速度。CPU也可运行但会较慢。2.2 创建并激活Python虚拟环境打开终端执行以下命令# 创建项目目录并进入 mkdir local_ai_knowledge_base cd local_ai_knowledge_base # 创建Python虚拟环境使用venv python3 -m venv venv # 激活虚拟环境 # Linux/macOS source venv/bin/activate # Windows # venv\Scripts\activate激活后终端提示符前应显示(venv)。2.3 安装核心Python库我们将使用pip安装所有必要的依赖。创建一个requirements.txt文件内容如下# 核心框架与文档处理 langchain0.1.0 langchain-community0.0.10 langchain-chroma0.1.0 langchain-text-splitters0.0.1 # 嵌入模型与相关 sentence-transformers2.2.2 torch2.0.0 # 向量数据库 chromadb0.4.22 # 大模型本地运行 (Ollama集成) langchain-ollama0.1.0 # 后端API fastapi0.104.1 uvicorn[standard]0.24.0 # 前端界面 gradio4.13.0 # 文档加载器 pypdf3.17.4 # 用于PDF python-docx1.1.0 # 用于Word unstructured0.10.30 # 通用文档解析 # 其他工具 pydantic2.5.0 pydantic-settings2.1.0在终端中执行安装命令pip install -r requirements.txt这个过程可能会花费一些时间因为它会下载torch等较大的包。如果你想为GPU安装特定版本的PyTorch可以去PyTorch官网获取对应命令。2.4 安装并配置OllamaOllama是我们本地运行大语言模型的核心工具。安装OllamaLinux/macOS: 在终端运行curl -fsSL https://ollama.com/install.sh | shWindows: 从 Ollama官网 下载安装程序并运行。拉取一个中文能力较强的开源模型以7B参数模型为例对硬件要求相对友好# 拉取通义千问2.5 7B模型约4.7GB ollama pull qwen2.5:7b # 或者拉取Llama 3.2 7B模型 # ollama pull llama3.2:7b模型下载完成后可以通过以下命令测试模型是否运行正常ollama run qwen2.5:7b “你好”如果看到模型回复说明安装成功。3. 核心模块代码实现接下来我们将分步骤构建系统的各个核心模块。项目目录结构如下local_ai_knowledge_base/ ├── app.py # FastAPI 主应用 ├── knowledge_base.py # 知识库构建与检索核心类 ├── requirements.txt ├── data/ # 存放原始文档PDF, Word, TXT等 ├── vector_db/ # ChromaDB 持久化存储目录自动生成 └── static/ # 前端静态文件可选3.1 知识库构建与检索类创建knowledge_base.py文件这是整个系统的“大脑”。# knowledge_base.py import os from typing import List, Dict, Any, Optional from langchain_community.document_loaders import ( PyPDFLoader, Docx2txtLoader, TextLoader, UnstructuredFileLoader, ) from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain.embeddings import HuggingFaceEmbeddings from langchain.vectorstores import Chroma from langchain.schema import Document from langchain.chains import RetrievalQA from langchain_ollama import OllamaLLM import chromadb from chromadb.config import Settings class LocalKnowledgeBase: 本地知识库核心类负责文档加载、向量化、存储和检索。 def __init__( self, persist_directory: str ./vector_db, embedding_model_name: str BAAI/bge-small-zh-v1.5, ollama_model_name: str qwen2.5:7b, chunk_size: int 500, chunk_overlap: int 50, ): 初始化知识库。 Args: persist_directory: 向量数据库持久化目录。 embedding_model_name: 句子嵌入模型名称。 ollama_model_name: 本地Ollama运行的LLM模型名称。 chunk_size: 文本分割的大小。 chunk_overlap: 文本分割的重叠部分。 self.persist_directory persist_directory self.embedding_model_name embedding_model_name self.ollama_model_name ollama_model_name self.chunk_size chunk_size self.chunk_overlap chunk_overlap # 初始化嵌入模型 print(f正在加载嵌入模型: {self.embedding_model_name}...) self.embeddings HuggingFaceEmbeddings( model_nameself.embedding_model_name, model_kwargs{device: cpu}, # 有GPU可改为 cuda encode_kwargs{normalize_embeddings: True}, # 标准化向量提升检索效果 ) # 初始化文本分割器 self.text_splitter RecursiveCharacterTextSplitter( chunk_sizeself.chunk_size, chunk_overlapself.chunk_overlap, separators[\n\n, \n, 。, , , , , , ], length_functionlen, ) # 初始化向量数据库客户端 self.client chromadb.PersistentClient( pathself.persist_directory, settingsSettings(anonymized_telemetryFalse) # 禁用匿名遥测 ) # 初始化LangChain的Chroma向量存储对象 self.vectorstore Chroma( clientself.client, collection_nameknowledge_base, embedding_functionself.embeddings, persist_directoryself.persist_directory, ) # 初始化大语言模型 print(f正在连接本地大模型: {self.ollama_model_name}...) self.llm OllamaLLM(modelself.ollama_model_name, base_urlhttp://localhost:11434) # 初始化检索问答链 self.qa_chain RetrievalQA.from_chain_type( llmself.llm, chain_typestuff, # 将检索到的文档“塞”进提示词 retrieverself.vectorstore.as_retriever( search_typesimilarity, # 相似度检索 search_kwargs{k: 4} # 返回最相关的4个片段 ), return_source_documentsTrue, # 返回源文档便于追溯 verboseFalse, ) print(知识库初始化完成) def load_and_split_documents(self, data_dir: str ./data) - List[Document]: 加载指定目录下的所有文档并进行智能分割。 Args: data_dir: 存放文档的目录路径。 Returns: 分割后的文档列表。 documents [] supported_extensions {.pdf, .docx, .doc, .txt, .md} for root, _, files in os.walk(data_dir): for file in files: file_path os.path.join(root, file) file_ext os.path.splitext(file)[1].lower() if file_ext not in supported_extensions: print(f跳过不支持的文件格式: {file_path}) continue try: print(f正在处理文件: {file_path}) loader None if file_ext .pdf: loader PyPDFLoader(file_path) elif file_ext in [.docx, .doc]: loader Docx2txtLoader(file_path) elif file_ext in [.txt, .md]: loader TextLoader(file_path, encodingutf-8) else: # 使用通用加载器尝试 loader UnstructuredFileLoader(file_path) if loader: loaded_docs loader.load() # 对每个文档进行分割 split_docs self.text_splitter.split_documents(loaded_docs) documents.extend(split_docs) print(f 成功加载并分割得到 {len(split_docs)} 个文本块。) except Exception as e: print(f 处理文件 {file_path} 时出错: {e}) continue print(f总计加载并分割了 {len(documents)} 个文本块。) return documents def build_knowledge_base(self, data_dir: str ./data, force_rebuild: bool False): 构建或更新知识库。 Args: data_dir: 文档目录。 force_rebuild: 是否强制重建清空原有数据。 # 如果强制重建先尝试删除旧的集合 if force_rebuild: try: self.client.delete_collection(nameknowledge_base) print(已清空原有知识库集合。) except: pass # 集合不存在则忽略 # 检查集合是否已有数据 existing_count self.vectorstore._collection.count() if existing_count 0 and not force_rebuild: print(f知识库已存在包含 {existing_count} 个向量。如需重建请设置 force_rebuildTrue。) return # 加载并分割文档 documents self.load_and_split_documents(data_dir) if not documents: print(未找到任何可处理的文档知识库构建中止。) return # 将文档添加到向量数据库 print(正在将文档向量化并存入数据库...) # 注意add_documents 会自动进行向量化 self.vectorstore.add_documents(documents) # 持久化到磁盘 self.vectorstore.persist() print(f知识库构建完成共添加 {len(documents)} 个文本块。) def query(self, question: str) - Dict[str, Any]: 向知识库提问。 Args: question: 用户问题。 Returns: 包含答案和源文档的字典。 if self.vectorstore._collection.count() 0: return {answer: 知识库为空请先构建知识库。, source_documents: []} print(f用户提问: {question}) print(正在检索相关文档并生成答案...) try: result self.qa_chain.invoke({query: question}) answer result.get(result, 未能生成答案。) sources result.get(source_documents, []) # 格式化源文档信息 source_info [] for doc in sources: source_info.append({ content: doc.page_content[:200] ..., # 截取部分内容 metadata: doc.metadata }) return { answer: answer, sources: source_info } except Exception as e: print(f查询过程中发生错误: {e}) return {answer: f系统处理问题时出错: {e}, sources: []} def get_stats(self) - Dict[str, Any]: 获取知识库统计信息。 count self.vectorstore._collection.count() return { vector_count: count, embedding_model: self.embedding_model_name, llm_model: self.ollama_model_name, db_path: os.path.abspath(self.persist_directory) }3.2 FastAPI 后端服务创建app.py文件提供Web API。# app.py from fastapi import FastAPI, HTTPException, UploadFile, File, Form from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles from pydantic import BaseModel from typing import List, Optional import os import shutil import uvicorn from knowledge_base import LocalKnowledgeBase # 初始化FastAPI应用 app FastAPI(title本地AI知识库API, description一个完全运行在本地的RAG问答系统) # 允许跨域请求如果前端单独部署 app.add_middleware( CORSMiddleware, allow_origins[*], # 生产环境应限制为具体域名 allow_credentialsTrue, allow_methods[*], allow_headers[*], ) # 初始化知识库实例 # 注意在实际生产部署中应考虑使用依赖注入或单例模式进行更优雅的管理。 KB_INSTANCE LocalKnowledgeBase() # 数据模型定义 class QueryRequest(BaseModel): question: str class QueryResponse(BaseModel): answer: str sources: List[dict] class BuildRequest(BaseModel): data_dir: str ./data force_rebuild: bool False class StatsResponse(BaseModel): vector_count: int embedding_model: str llm_model: str db_path: str # API路由 app.get(/) async def root(): return {message: 本地AI知识库服务已启动, status: running} app.post(/api/build, response_modeldict) async def build_knowledge_base(request: BuildRequest): 构建或重建知识库。 try: KB_INSTANCE.build_knowledge_base( data_dirrequest.data_dir, force_rebuildrequest.force_rebuild ) stats KB_INSTANCE.get_stats() return { status: success, message: 知识库构建完成, stats: stats } except Exception as e: raise HTTPException(status_code500, detailf构建知识库失败: {str(e)}) app.post(/api/query, response_modelQueryResponse) async def query_knowledge_base(request: QueryRequest): 向知识库提问。 if not request.question or request.question.strip() : raise HTTPException(status_code400, detail问题不能为空) result KB_INSTANCE.query(request.question.strip()) return QueryResponse(**result) app.get(/api/stats, response_modelStatsResponse) async def get_knowledge_base_stats(): 获取知识库统计信息。 stats KB_INSTANCE.get_stats() return StatsResponse(**stats) app.post(/api/upload) async def upload_document(file: UploadFile File(...)): 上传单个文档到 data 目录。 # 确保上传目录存在 upload_dir ./data os.makedirs(upload_dir, exist_okTrue) # 检查文件类型 allowed_extensions {.pdf, .docx, .doc, .txt, .md} file_ext os.path.splitext(file.filename)[1].lower() if file_ext not in allowed_extensions: raise HTTPException(status_code400, detailf不支持的文件格式: {file_ext}。仅支持 {, .join(allowed_extensions)}) # 保存文件 file_path os.path.join(upload_dir, file.filename) try: with open(file_path, wb) as buffer: shutil.copyfileobj(file.file, buffer) except Exception as e: raise HTTPException(status_code500, detailf文件保存失败: {str(e)}) return { status: success, message: f文件 {file.filename} 上传成功, file_path: file_path } # 启动Gradio前端可选也可以独立运行 app.on_event(startup) async def startup_event(): print(本地AI知识库后端服务启动成功) print(f嵌入模型: {KB_INSTANCE.embedding_model_name}) print(fLLM模型: {KB_INSTANCE.ollama_model_name}) print(请确保Ollama服务正在运行 (ollama serve)) print(API文档地址: http://localhost:8000/docs) if __name__ __main__: # 启动服务默认监听 8000 端口 uvicorn.run(app, host0.0.0.0, port8000)3.3 使用Gradio构建简易前端界面虽然可以直接使用API但一个Web界面更方便测试和演示。我们可以直接在app.py末尾集成Gradio或者单独创建一个前端文件。这里我们选择集成。在app.py的if __name__ “__main__”:部分之前添加以下代码# 在 app.py 中追加以下代码位于文件末尾在启动代码之前 import gradio as gr def gradio_query(question: str, history): 供Gradio界面调用的查询函数。 result KB_INSTANCE.query(question) answer result.get(answer, 无答案) sources result.get(sources, []) # 格式化回复附带来源信息 formatted_answer f{answer}\n\n**参考来源**\n for i, src in enumerate(sources, 1): filename src.get(metadata, {}).get(source, 未知文件).split(/)[-1] formatted_answer f{i}. 来自 {filename}\n return formatted_answer def launch_gradio(): 启动Gradio Web界面。 # 创建Gradio界面 demo gr.ChatInterface( fngradio_query, title 本地AI知识库问答系统, description完全本地运行的智能客服。请先上传文档到 ./data 目录然后点击“构建知识库”按钮。完成后即可开始问答。, additional_inputs[ gr.Button(构建知识库, variantprimary), ] ) # 添加一个构建知识库的按钮回调简化处理实际应调用API def build_kb(): try: KB_INSTANCE.build_knowledge_base(force_rebuildTrue) return 知识库构建完成 except Exception as e: return f构建失败: {e} # 这里为了简化我们将构建按钮的功能独立出来。 # 在实际更复杂的Gradio布局中可以将其整合进聊天界面。 with gr.Blocks() as combined_demo: gr.Markdown(# 本地AI知识库问答系统) gr.Markdown(数据完全在本地处理安全私密。) with gr.Row(): with gr.Column(scale2): # 聊天界面 chat_interface gr.ChatInterface( fngradio_query, examples[公司今年的主要目标是什么, 请假流程是怎样的, 介绍一下产品A的核心功能。], cache_examplesFalse, ) with gr.Column(scale1): # 知识库管理侧边栏 gr.Markdown(### 知识库管理) build_btn gr.Button( 构建/重建知识库, variantprimary) output gr.Textbox(label构建状态, interactiveFalse) build_btn.click(fnbuild_kb, inputsNone, outputsoutput) gr.Markdown(---) gr.Markdown(#### 统计信息) stats_btn gr.Button( 查看统计) stats_output gr.JSON(label知识库状态) def show_stats(): return KB_INSTANCE.get_stats() stats_btn.click(fnshow_stats, inputsNone, outputsstats_output) gr.Markdown(---) gr.Markdown(#### 使用说明) gr.Markdown( 1. 将PDF、Word、TXT文档放入 ./data 文件夹。 2. 点击“构建/重建知识库”按钮。 3. 在左侧对话框开始提问。 ) # 启动Gradio与FastAPI共享端口可能冲突这里我们让Gradio运行在7860端口 print(\nGradio Web界面将在 http://localhost:7860 启动) combined_demo.launch(server_name0.0.0.0, server_port7860, shareFalse) # 修改启动部分让用户选择启动模式 if __name__ __main__: import sys if len(sys.argv) 1 and sys.argv[1] --gradio: # 启动Gradio前端同时会初始化后端知识库实例 launch_gradio() else: # 默认启动纯FastAPI后端 print(启动纯FastAPI后端模式访问 http://localhost:8000/docs 查看API文档。) uvicorn.run(app, host0.0.0.0, port8000)4. 完整部署与运行流程4.1 准备知识文档在项目根目录下创建data文件夹并放入你的文档。例如data/员工手册.pdfdata/产品介绍.docxdata/常见问题.txt文档内容将作为知识库的来源。4.2 启动Ollama服务打开一个新的终端窗口运行以下命令启动Ollama服务如果尚未运行ollama serve保持此终端窗口打开。Ollama默认会在11434端口提供服务。4.3 构建知识库首先确保你在项目根目录并且虚拟环境已激活。方式一通过Python脚本构建创建一个简单的构建脚本build_kb.py# build_kb.py from knowledge_base import LocalKnowledgeBase if __name__ __main__: kb LocalKnowledgeBase() # 构建知识库force_rebuildTrue 会清空旧数据重建 kb.build_knowledge_base(data_dir./data, force_rebuildTrue) stats kb.get_stats() print(f知识库构建完成向量数量: {stats[vector_count]})运行它python build_kb.py你会看到控制台输出文档加载、分割和向量化的过程。4.4 启动Web服务并测试启动FastAPI后端提供APIpython app.py访问http://localhost:8000/docs可以看到自动生成的API交互文档。你可以在这里直接测试/api/query接口。启动Gradio前端提供友好界面python app.py --gradio访问http://localhost:7860即可打开完整的Web界面。你可以在右侧点击“构建/重建知识库”。构建完成后在左侧聊天框提问。4.5 效果演示假设你的data/员工手册.pdf中包含关于年假的规定“员工累计工作满1年年假5天满10年年假10天。”在Gradio界面提问“请问工作满一年的员工有多少天年假”系统会将问题转化为向量。在向量数据库中搜索与之最相关的文本片段即上述规定。将问题和该片段组合成提示词发送给本地运行的qwen2.5:7b模型。模型生成答案“根据员工手册规定累计工作满1年的员工年假为5天。”答案下方会显示“参考来源来自员工手册.pdf”实现了答案的可追溯。5. 常见问题与排查思路在部署和运行过程中你可能会遇到以下问题问题现象可能原因解决思路导入langchain相关模块失败LangChain版本更新快API有变动。检查requirements.txt中的版本是否与代码兼容。可尝试固定到稍旧的稳定版本或查阅对应版本LangChain的文档。运行ollama serve提示端口占用11434端口被其他进程占用。使用lsof -i :11434(Linux/macOS) 或netstat -ano | findstr :11434(Windows) 查找并终止占用进程或修改Ollama配置使用其他端口需同步修改代码中的base_url。构建知识库时内存/显存不足嵌入模型或文档太大。1. 换用更小的嵌入模型如paraphrase-multilingual-MiniLM-L12-v2。2. 减小chunk_size(如改为300)。3. 分批处理文档。提问后回答“知识库为空”1. 未成功构建知识库。2.vector_db目录下无数据。1. 检查build_knowledge_base是否成功运行并打印了添加的文本块数量。2. 检查./vector_db目录是否存在及是否有文件。回答内容与文档无关或胡言乱语1. 检索到的文档片段不相关。2. LLM本身“幻觉”。3. 提示词效果不佳。1. 尝试调整检索数量search_kwargs{“k”: 4}增加或减少k值。2. 尝试不同的嵌入模型。3. 在RetrievalQA中调整chain_type如“map_reduce”或“refine”或自定义提示词模板。Gradio界面无法连接到后端Gradio和FastAPI服务端口冲突或未启动。确保按教程先启动了Ollama然后按需启动FastAPI或Gradio。两者不要同时使用同一个端口。处理中文PDF出现乱码PDF编码或PDF解析库问题。1. 尝试使用unstructured库的加载器。2. 确保PDF本身是文本型PDF而非扫描图片。3. 对于复杂PDF可考虑先用其他工具如Adobe Acrobat导出为TXT再处理。6. 最佳实践与进阶优化6.1 知识库构建优化文档预处理在加载前可以清洗文档中的无关字符、页眉页脚、广告等。智能分割根据文档结构如标题、章节进行分割比简单的递归字符分割效果更好。可以尝试MarkdownHeaderTextSplitter或RecursiveJsonSplitter。元数据增强在分割时为每个文本块添加更多元数据如所属章节、页码、文件类型等便于后续检索和展示。增量更新实现增量更新逻辑只对新文档或修改文档进行向量化避免全量重建。ChromaDB支持按id更新或删除。6.2 检索与问答优化混合检索结合向量检索语义相似和关键词检索BM25提升召回率。LangChain 提供了EnsembleRetriever。重排序使用更精细的模型如bge-reranker对初步检索出的多个片段进行重排序将最相关的排在前面。提示词工程定制RetrievalQA的提示词模板明确要求模型“基于以下上下文回答”并“如果上下文不包含答案请说不知道”。这能有效减少幻觉。对话历史将当前问题与之前的对话历史一起考虑实现多轮对话。这需要修改检索和提示词部分将历史问答也纳入上下文。6.3 性能与生产化部署模型量化使用GPTQ或GGUF量化格式的LLM模型可以大幅减少内存和显存占用提升推理速度。Ollama 支持运行量化模型。API服务化将核心的检索与生成功能封装成独立的gRPC或高性能HTTP服务与Web前端解耦。缓存机制对常见问题及答案进行缓存减少对LLM的重复调用。日志与监控添加详细的日志记录监控问答耗时、Token使用量、检索命中率等指标。6.4 安全与权限输入检查对用户输入的问题进行敏感词过滤或长度限制防止恶意输入。访问控制在生产环境务必为FastAPI和Gradio服务添加认证和授权例如使用API Key或JWT Token。内容审核对于生成的内容可以接入简单的规则或关键词过滤避免产生不当言论。通过以上步骤你已经成功搭建了一个功能完整、数据完全本地的AI客服知识库。这套方案不仅安全可控而且具备了良好的可扩展性。你可以根据实际业务需求更换更强大的模型、优化检索策略、集成到现有的内部系统中构建真正属于企业自己的智能知识中枢。