构建你的第一个私有知识库问答系统
一、零基础环境准备与依赖安装先说你电脑得达到什么配置。最低要求4核CPU、8GB内存、50GB硬盘。推荐配置16GB内存运行7B模型没问题有NVIDIA显卡RTX 3060以上推理速度会快不少。如果没有独显也别慌纯CPU也能跑就是慢一点。操作系统方面Windows建议开WSL2、macOS、Ubuntu 20.04都行。第一步装Python环境建议用conda管理环境隔离依赖不容易打架# 安装conda如果还没装# 去 https://www.anaconda.com/download 下载安装包# 创建专用环境conda create-nknowledge_ragpython3.10conda activate knowledge_ragPython版本选3.9或3.10都行别用太新的3.12以上有些库还没适配。第二步装Ollama本地跑大模型Ollama是目前在本地运行大语言模型最简单的工具。去官网 https://ollama.com/download 下载对应系统的安装包。装完后打开终端验证一下ollama-v看到版本号就说明装好了。然后下载一个模型。新手推荐Qwen2.5-7B阿里开源的中文模型对中文支持好或DeepSeek-R1-7B国产模型中文能力强ollama run qwen2.5:7b第一次运行会自动下载模型文件大约4-5GB等它下完。下载完成后会直接进入对话界面你可以随便问两句确认它能正常工作。按CtrlD退出。第三步装Python依赖库在项目目录下创建requirements.txtlangchain0.3.26 langchain-community0.3.27 langchain-text-splitters0.3.8 faiss-cpu1.7.0 chromadb0.5.0 sentence-transformers3.0.0 unstructured0.18.11 pdfplumber0.11.0 python-dotenv1.0.0然后一条命令装完pipinstall-rrequirements.txt装unstructured的时候可能会遇到一些系统依赖问题尤其是PDF解析Windows用户如果报错可以单独装pdfplumber来替代。二、核心概念解析与系统架构初探在写代码之前先弄明白这套系统到底是怎么工作的。整个流程可以用四个字概括存、搜、生成。存把你的文档PDF、Word、Markdown等拆成小片段然后用Embedding模型把每个片段变成一串数字叫“向量”存到向量数据库里。搜用户提一个问题同样把问题变成向量去向量数据库里找到最相似的那几个文档片段。生成把“用户的问题”和“检索到的文档片段”一起喂给大模型让它综合这些信息给出回答。这套方案叫RAGRetrieval-Augmented Generation检索增强生成。它的核心价值是大模型不再凭空瞎编而是基于你提供的文档来回答。几个关键术语你得知道Chunk文本块把长文档切成的小片段。不能太长模型上下文窗口有限也不能太短语义不完整。Embedding向量嵌入把文本转换成数字向量的过程。向量数据库专门存向量、算相似度的数据库。我们教程里用 ChromaDB 或 FAISS。三、本地文档清洗与向量化处理这是整个流程里最影响最终效果的一步别马虎。第一步准备你的文档把你想要让AI学习的文档都放到一个文件夹里比如./knowledge_base/。支持 PDF、Word.docx、Markdown.md、TXT 等格式。第二步加载文档用 LangChain 的DirectoryLoader可以自动识别文件夹里各种格式的文件fromlangchain_community.document_loadersimportDirectoryLoader loaderDirectoryLoader(./knowledge_base/,glob**/*.*,# 加载所有文件silent_errorsTrue# 遇到读不了的文件跳过不报错)raw_documentsloader.load()print(f成功加载了{len(raw_documents)}份文档)如果你的PDF解析报错可以单独用pdfplumber来读importpdfplumberwithpdfplumber.open(./knowledge_base/手册.pdf)aspdf:textforpageinpdf.pages:textpage.extract_text()第三步文本切分Chunking这是关键。一整篇文章直接喂给模型效果很差。需要用RecursiveCharacterTextSplitter智能切分它会尽量按段落、句子边界来切保持语义完整fromlangchain.text_splitterimportRecursiveCharacterTextSplitter text_splitterRecursiveCharacterTextSplitter(chunk_size500,# 每个文本块最多500个字符chunk_overlap50,# 块与块之间重叠50个字符避免切断关键信息separators[\n\n,\n,。,,, ]# 优先按这些符号切)docstext_splitter.split_documents(raw_documents)print(f切分成了{len(docs)}个文本块)chunk_size和chunk_overlap这两个参数直接影响问答质量。一般建议chunk_size 500-1000overlap 50-200。文档越规整比如技术手册chunk_size 可以大一点文档越散比如聊天记录就小一点。第四步向量化并存入数据库把每个文本块变成向量存到 ChromaDB 里fromlangchain_community.embeddingsimportHuggingFaceEmbeddingsfromlangchain_community.vectorstoresimportChroma# 用开源的Embedding模型在本地跑不需要联网embeddingsHuggingFaceEmbeddings(model_nameBAAI/bge-small-zh-v1.5# 中文向量模型大概几百MB)# 创建向量数据库数据存在 ./chroma_db 目录下vectorstoreChroma.from_documents(documentsdocs,embeddingembeddings,persist_directory./chroma_db)vectorstore.persist()print(向量数据库创建完成)第一次运行时会自动下载 Embedding 模型大概几百MB。如果你网络不好可以换成sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2这个模型更小。四、搭建检索增强生成核心流程向量数据库有了现在把“检索”和“生成”串起来。第一步创建检索器# 从之前保存的向量数据库加载fromlangchain_community.vectorstoresimportChromafromlangchain_community.embeddingsimportHuggingFaceEmbeddings embeddingsHuggingFaceEmbeddings(model_nameBAAI/bge-small-zh-v1.5)vectorstoreChroma(persist_directory./chroma_db,embedding_functionembeddings)# 创建检索器每次检索返回最相似的4个文本块retrievervectorstore.as_retriever(search_kwargs{k:4})k4表示每次检索返回4个最相关的片段。这个数字不是越大越好——片段太多会塞爆模型的上下文窗口还会引入噪音。第二步连接本地大模型用 Ollama 跑起来的模型通过 LangChain 来调用fromlangchain_community.llmsimportOllama llmOllama(modelqwen2.5:7b,# 和前面 ollama run 用的模型保持一致temperature0.1,# 温度越低回答越确定越高越“有创意”base_urlhttp://localhost:11434)temperature建议设低一点0.1-0.3问答场景需要准确性不需要模型发挥想象力。第三步组装RAG链fromlangchain.chainsimportRetrievalQAfromlangchain.promptsimportPromptTemplate# 自定义Prompt模板告诉模型怎么用检索到的内容template你是一个基于知识库的问答助手。请根据以下参考内容来回答用户的问题。 如果参考内容中没有相关信息请直接说根据当前知识库无法回答这个问题不要编造答案。 参考内容 {context} 用户问题{question} 回答promptPromptTemplate(templatetemplate,input_variables[context,question])# 组装RAG链qa_chainRetrievalQA.from_chain_type(llmllm,chain_typestuff,retrieverretriever,chain_type_kwargs{prompt:prompt},return_source_documentsTrue# 返回引用的文档片段方便溯源)五、编写首个交互式问答脚本把上面所有的代码整合到一个完整的脚本里保存为ask.pyimportosfromlangchain_community.document_loadersimportDirectoryLoaderfromlangchain.text_splitterimportRecursiveCharacterTextSplitterfromlangchain_community.embeddingsimportHuggingFaceEmbeddingsfromlangchain_community.vectorstoresimportChromafromlangchain_community.llmsimportOllamafromlangchain.chainsimportRetrievalQAfromlangchain.promptsimportPromptTemplate# -------- 配置 ----------KNOWLEDGE_PATH./knowledge_base# 你的文档放这里PERSIST_PATH./chroma_db# 向量数据库存这里EMBEDDING_MODELBAAI/bge-small-zh-v1.5LLM_MODELqwen2.5:7bCHUNK_SIZE500CHUNK_OVERLAP50# -------- 第一步加载和切分文档 ----------print( 正在加载文档...)loaderDirectoryLoader(KNOWLEDGE_PATH,glob**/*.*,silent_errorsTrue)raw_docsloader.load()print(f 加载了{len(raw_docs)}份文档)text_splitterRecursiveCharacterTextSplitter(chunk_sizeCHUNK_SIZE,chunk_overlapCHUNK_OVERLAP,separators[\n\n,\n,。,,, ])docstext_splitter.split_documents(raw_docs)print(f 切分成了{len(docs)}个文本块)# -------- 第二步向量化并存储 ----------print( 正在生成向量并存入数据库...)embeddingsHuggingFaceEmbeddings(model_nameEMBEDDING_MODEL)# 如果数据库已存在直接加载否则新建ifos.path.exists(PERSIST_PATH):vectorstoreChroma(persist_directoryPERSIST_PATH,embedding_functionembeddings)print( 从已有数据库加载)else:vectorstoreChroma.from_documents(documentsdocs,embeddingembeddings,persist_directoryPERSIST_PATH)vectorstore.persist()print( 新建数据库完成)retrievervectorstore.as_retriever(search_kwargs{k:4})# -------- 第三步连接大模型 ----------print( 连接大模型...)llmOllama(modelLLM_MODEL,temperature0.1,base_urlhttp://localhost:11434)# -------- 第四步组装问答链 ----------template你是一个基于知识库的问答助手。请根据以下参考内容来回答用户的问题。 如果参考内容中没有相关信息请直接说根据当前知识库无法回答这个问题不要编造答案。 参考内容 {context} 用户问题{question} 回答promptPromptTemplate(templatetemplate,input_variables[context,question])qa_chainRetrievalQA.from_chain_type(llmllm,chain_typestuff,retrieverretriever,chain_type_kwargs{prompt:prompt},return_source_documentsTrue)# -------- 第五步交互式问答 ----------print(\n✅ 系统已就绪输入问题开始问答输入 exit 退出\n)whileTrue:questioninput( 你)ifquestion.lower()in[exit,quit,退出]:breakifnotquestion.strip():continuetry:resultqa_chain.invoke({query:question})print(f 助手{result[result]}\n)# 可选打印引用的文档来源# for doc in result[source_documents]:# print(f [引用] {doc.metadata.get(source, 未知)[:50]}...)exceptExceptionase:print(f❌ 出错了{e}\n)运行方式python ask.py六、运行测试与回答效果验证第一次运行脚本会稍微慢一点因为要下载 Embedding 模型、处理文档。测试建议放几份你熟悉的文档进去。比如你放了一本产品说明书就问“这个产品的保修期是多久”——你能判断答案对不对。问一些文档里没有的内容看它会不会瞎编。好的RAG系统应该诚实地说“不知道”。问一些需要综合多处信息的问题比如“A功能和B功能有什么区别”——看它能不能把分散在不同段落的信息串起来。如果回答质量不理想按这个顺序排查答非所问chunk_size太大或太小调整到 500 左右试试漏掉关键信息增大k值比如改成 6 或 8回答太啰嗦或太简短调整temperature0.1 更确定0.7 更灵活引用了错误的内容检查你的文档本身有没有噪声或重复七、常见报错排查与连接问题解决报错1ModuleNotFoundError: No module named langchain依赖没装全。在项目目录下执行pip install -r requirements.txt重新装一遍。报错2Ollama 连接失败Connection refusedOllama 服务没启动。开一个新终端窗口运行ollama serve或者在原终端确认模型正在运行ollama list看看有没有模型ollama run qwen2.5:7b启动它。报错3PDF 解析失败unstructured在某些系统上依赖比较多。可以换成pdfplumber单独处理 PDFimportpdfplumberwithpdfplumber.open(你的文件.pdf)aspdf:text.join([page.extract_text()forpageinpdf.pages])报错4内存不足OOMchunk_size 设小一点300-400或者换更小的 Embedding 模型paraphrase-multilingual-MiniLM-L12-v2只有 120MB。如果跑 7B 模型内存不够可以换 3B 或 1.5B 的蒸馏版本。报错5向量数据库加载失败检查./chroma_db目录是否存在且有读写权限。如果数据库损坏删掉这个目录重新运行脚本会重新构建。八、提升回答准确率的实用技巧技巧1优化文档切分策略不同文档用不同的切分方式。技术文档按章节切FAQ按条目切。RecursiveCharacterTextSplitter的separators参数可以调整切分优先级。技巧2增加检索数量再筛选先检索 10 个片段再用一个重排序模型Reranker选出最相关的 3 个。这样比直接取 top-3 更准。技巧3在Prompt里给模型立规矩Prompt 模板里明确告诉模型“如果不知道就说不知道不要编造”。还可以加上“请用简洁的语言回答”“请分点列出”等指令。技巧4文档质量决定一切垃圾进垃圾出。确保你的文档没有乱码、没有重复内容、排版清晰。PDF 如果是扫描件图片格式需要先用 OCR 转成文字unstructured本身不处理图片型 PDF。技巧5定期更新知识库文档有更新时删掉./chroma_db目录重新跑一遍脚本或者写增量更新的逻辑。九、扩展多格式文件支持方法除了 PDF 和 TXT你可能还需要处理更多格式。Word 文档.docxfromlangchain_community.document_loadersimportUnstructuredWordDocumentLoader loaderUnstructuredWordDocumentLoader(文件.docx)docsloader.load()Markdown.mdfromlangchain_community.document_loadersimportUnstructuredMarkdownLoader loaderUnstructuredMarkdownLoader(文件.md)docsloader.load()Excel.xlsximportpandasaspdfromlangchain.schemaimportDocument dfpd.read_excel(数据.xlsx)docs[]for_,rowindf.iterrows():text .join([f{col}:{val}forcol,valinrow.items()ifpd.notna(val)])docs.append(Document(page_contenttext,metadata{source:数据.xlsx}))网页链接fromlangchain_community.document_loadersimportWebBaseLoader loaderWebBaseLoader(https://example.com/page)docsloader.load()如果你觉得每种格式都要写不同的加载代码太麻烦DirectoryLoader配合unstructured可以自动识别大部分常见格式。十、系统安全部署与隐私保护建议这套系统最大的价值就是数据不出本地。所有文档、向量、模型都在你自己的电脑上运行。隐私保护要点断网也能用整套系统不依赖任何云端APIEmbedding模型和LLM都是本地跑的内网环境完全可用。不要用云端API如果为了追求效果去调用了 OpenAI 或百度千帆的接口你的文档内容就上传到云端了。本地部署的意义就是守住数据主权。敏感文档单独建库不同部门的文档分开建向量数据库用不同的persist_directory路径隔离。访问控制如果要把系统部署成服务加一层鉴权用户名密码或API Key不要裸奔。审计日志记录谁在什么时候问了什么问题、系统返回了什么答案方便事后追溯。生产环境部署建议用 Docker 把整个环境打包方便迁移和扩容数据盘做定期备份./chroma_db目录和你的原始文档如果有多人同时使用考虑用 Milvus 替代 ChromaDB模型服务Ollama用 systemd 或 supervisor 托管保证开机自启和异常重启WEB项目地址演示地址安卓APP下载地址演示地址把上面第5节的完整脚本保存下来放几份文档进去跑一遍你就能拥有一个完全离线、数据不出本地、能回答你私人文档问题的AI助手了。有问题按照第7节的排查表对照处理基本都能解决。