从零搭建本地 RAG 知识库:Ollama + LangChain 处理私有文档的完整实践 去年开始接触大模型应用开发时第一个遇到的实际问题就是怎么让模型回答我自己的文档内容直接把文档塞进提示词里肯定不行——上下文窗口就那么大几十页 PDF 根本放不下。后来才了解到 RAG检索增强生成是解决这个问题的标准做法。这里把从零搭建一个本地 RAG 系统的完整过程记录下来所有组件都是开源的数据不出本机。一、什么时候需要自己搭 RAG内部知识库问答公司有几十份技术文档、产品手册想让 AI 基于这些文档回答问题个人笔记检索自己写了半年的工作笔记想用自然语言找到相关内容敏感数据不外传文档涉及客户信息或商业机密不能上传到 ChatGPT 等云端服务定制化回答想让模型基于特定的技术规范或行业标准来回答问题RAG 的核心思路很简单先把文档拆成小块chunk并向量化存起来用户提问时把问题也向量化去库里找到最相关的几块内容连同问题一起丢给 LLM 去回答。模型不是记住了你的文档而是每次都在你的文档里查到答案再回答。二、方案一本地部署 RAGOllama LangChain ChromaDB这是今天要搭建的方案完全本地运行数据不出机器。环境准备# 安装 OllamamacOS / Linux curl -fsSL https://ollama.com/install.sh | sh # Windows 去官网下载安装包 # 下载一个本地模型7B 参数普通电脑够用 ollama pull qwen2.5:7b # 安装 Python 依赖 pip install langchain langchain-community chromadb pypdf sentence-transformers第一步文档加载与分割from langchain_community.document_loaders import PyPDFLoader from langchain.text_splitter import RecursiveCharacterTextSplitter from pathlib import Path def load_documents(doc_dir: str): 加载指定目录下的所有 PDF 文档 docs [] for pdf_file in Path(doc_dir).glob(*.pdf): print(f加载文档: {pdf_file.name}) loader PyPDFLoader(str(pdf_file)) docs.extend(loader.load()) print(f共加载 {len(docs)} 页) return docs def split_documents(docs, chunk_size500, chunk_overlap100): 将文档切分成小块块之间保留重叠 splitter RecursiveCharacterTextSplitter( chunk_sizechunk_size, chunk_overlapchunk_overlap, separators[\n\n, \n, 。, , , , , ] ) chunks splitter.split_documents(docs) print(f分割为 {len(chunks)} 个文本块) return chunks # 使用示例 documents load_documents(./my_docs) chunks split_documents(documents)chunk_size和chunk_overlap是两个需要调的核心参数。我试了不同组合chunk_sizechunk_overlap效果20050块数多但上下文容易断长段落被切碎500100平衡大部分场景适用1000200块数少但一次性返回的信息量大500/100 的组合对大多数技术文档表现比较平衡。第二步向量化存储from langchain_community.embeddings import HuggingFaceEmbeddings from langchain_community.vectorstores import Chroma # 使用本地 embedding 模型不需要联网 embeddings HuggingFaceEmbeddings( model_nameBAAI/bge-small-zh-v1.5, model_kwargs{device: cpu}, encode_kwargs{normalize_embeddings: True} ) def build_vector_store(chunks, persist_dir./chroma_db): 构建向量数据库并持久化到磁盘 vector_store Chroma.from_documents( documentschunks, embeddingembeddings, persist_directorypersist_dir ) vector_store.persist() print(f向量库已保存到 {persist_dir}) return vector_store vector_store build_vector_store(chunks)初次运行会下载 bge-small-zh-v1.5 模型到本地~/.cache/huggingface/目录之后离线也能用。向量库保存在./chroma_db目录下下次启动可以直接加载不需要重新处理文档。第三步检索与问答from langchain_ollama import OllamaLLM from langchain.chains import RetrievalQA from langchain.prompts import PromptTemplate # 连接 Ollama 本地模型 llm OllamaLLM( modelqwen2.5:7b, temperature0.3, num_predict2048, ) # 创建检索器每次返回最相关的 4 个文本块 retriever vector_store.as_retriever(search_kwargs{k: 4}) # 自定义提示模板 prompt_template 你是一个技术文档助手请基于以下文档内容回答问题。 文档内容 {context} 问题{question} 请用中文回答如果文档中没有相关信息请直接说文档中没有找到相关信息不要编造答案。 prompt PromptTemplate( templateprompt_template, input_variables[context, question] ) # 组装 QA 链 qa_chain RetrievalQA.from_chain_type( llmllm, chain_typestuff, retrieverretriever, return_source_documentsTrue, chain_type_kwargs{prompt: prompt} ) # 开始问答 def ask(question: str): result qa_chain.invoke({query: question}) print(f回答: {result[result]}) print(f\n参考来源: {len(result[source_documents])} 个文档片段) for i, doc in enumerate(result[source_documents], 1): print(f [{i}] {doc.metadata.get(source, 未知)} 第{doc.metadata.get(page, ?)}页) return result ask(我们的产品支持哪些导出格式)第四步用 Gradio 搭个简单的 Web 界面import gradio as gr def answer_question(question, history): result qa_chain.invoke({query: question}) sources \n.join([ f- {doc.metadata.get(source, ?)} 第{doc.metadata.get(page, ?)}页 for doc in result[source_documents] ]) return result[result] f\n\n**参考来源**\n{sources} with gr.ChatInterface( answer_question, title本地知识库助手, description基于私有文档的问答系统所有数据在本地运行, themesoft, ) as demo: demo.launch(server_name0.0.0.0, server_port7860)跑起来后浏览器打开 http://localhost:7860 就能用。我往里面塞了 5 份产品技术文档共 120 页问如何配置数据备份大概 3-4 秒出答案比翻 PDF 快多了。适合有 Python 基础、需要处理私密文档、希望完全掌控数据流向的场景。不太适合不想折腾环境配置、只需要在线问答一次性的场景。三、方案二Dify 社区版——Web 界面可视化搭建如果不想写代码Dify 是一个开源MIT 协议的 LLM 应用开发平台提供了可视化的 RAG 工作流编排界面。# 使用 Docker 部署 git clone https://github.com/langgenius/dify.git cd dify/docker cp .env.example .env docker compose up -d启动后访问 http://localhost:3000创建知识库 → 上传文档支持 PDF、TXT、Markdown、网页抓取系统自动切分文档并向量化内置 embedding 模型创建对话型应用 → 关联知识库 → 发布 API 或 Web 页面Dify 的切分策略比手动调参友好——提供了通用和父子切分两种模式。父子切分是比较实用的功能父块包含完整段落子块是小片段用于精确检索检索到子块后把父块内容送给 LLM既命中准了又保留完整上下文。Dify 还支持多用户权限管理如果部门里几个人都要用同一个知识库Dify 比脚本方案方便。适合不想写代码、需要 Web 管理界面、团队协作使用。不太适合需要深度定制检索逻辑、或者不想引入 Docker 依赖的场景。四、方案三使用 LangChain 自建 API 服务如果想把 RAG 能力封装成 API 供其他系统调用可以基于 FastAPI LangChain 搭建推理服务from fastapi import FastAPI, HTTPException from pydantic import BaseModel from typing import List, Optional import uvicorn app FastAPI(titleRAG API Service) class QueryRequest(BaseModel): question: str top_k: Optional[int] 4 class SourceInfo(BaseModel): source: str page: int content_preview: str class QueryResponse(BaseModel): answer: str sources: List[SourceInfo] app.on_event(startup) async def startup(): 启动时加载模型和向量库 global qa_chain qa_chain init_qa_chain() # 复用前文的初始化函数 app.post(/ask, response_modelQueryResponse) async def ask_question(request: QueryRequest): if not request.question.strip(): raise HTTPException(status_code400, detail问题不能为空) result qa_chain.invoke({query: request.question}) sources [] for doc in result[source_documents]: sources.append(SourceInfo( sourcedoc.metadata.get(source, 未知), pagedoc.metadata.get(page, 0), content_previewdoc.page_content[:100] )) return QueryResponse(answerresult[result], sourcessources) if __name__ __main__: uvicorn.run(app, host0.0.0.0, port8000)API 跑起来后其他系统可以这样调用curl -X POST http://localhost:8000/ask \ -H Content-Type: application/json \ -d {question: 如何安装和配置系统, top_k: 3}返回结果包含回答和引用来源可以在前端展示带引用的答案。适合需要将 RAG 能力嵌入到现有系统、需要多人多系统调用的场景。不太适合只需要一次性的问答需求方案一更直接。五、选型建议维度Python 脚本方案Dify 社区版FastAPI 自建服务代码量中等约 100 行零代码中等约 200 行部署复杂度Python 环境即可需要 DockerPython ASGI 服务器Web 界面Gradio 可选自带完整 UI需自建前端定制灵活度高可改任意环节中受限于平台功能高全栈可控团队协作单机支持多用户需自建用户体系文档更新需手动重建向量库支持增量更新需自实现更新逻辑embedding 模型本地 BGE本地或云端本地 BGE怎么选个人的技术文档需要做问答——Python 脚本方案100 行代码搞定想怎么调就怎么调团队内部要共享知识库且不想写前端——Dify 社区版开箱即用要把 RAG 能力嵌入已有的产品系统——FastAPI API 方案封装成微服务供其他模块调用六、几个实践中的坑chunk_size 不是越大越好我一开始设了 2000结果一个问题返回的内容填满了上下文模型反而抓不住重点。中文用 bge 系列 embedding试过用英文 embedding 模型处理中文文档检索准确率明显下降。source 信息要保留刚开始没保存文档来源信息模型回答的内容缺少可追溯性不敢直接用。加了 source_documents 后效果好很多。首次加载慢是正常的bge 模型下载 文档向量化初次运行要几分钟后续加载 Chroma 的持久化数据就快多了。七、总结RAG 是目前落地大模型应用最务实的方案——它不需要微调模型不需要昂贵的 GPU 训练只需要把文档处理好、向量库搭起来、检索策略调优就能让模型基于你的数据回答问题。从 Python 脚本到可视化平台再到 API 服务不同的复杂度对应不同的场景自己评估一下需求和资源选性价比最高的方式开始。搭完第一个 RAG 系统之后你会发现让 AI 理解我的文档这件事没有想象的那么复杂。本文涉及的组件Ollama、LangChain、ChromaDB、Dify、Gradio均为开源项目可在各自官方仓库查阅最新文档。