# 本地部署 RAG 系统:使用 Ollama 和 LangChain 构建私有知识库
## 背景介绍
很多企业在搭建 AI 应用时都会遇到一个两难选择:用云端 API 吧,数据安全和隐私是问题;自己训练模型吧,资源和成本又扛不住。我自己也在这条路上踩过不少坑,所以今天想聊聊一个相对折中的方案——在本地跑 RAG。
RAG 的全称是 Retrieval-Augmented Generation,中文通常叫检索增强生成。简单来说,它让大模型在回答问题前先去查一下预先准备好的文档,然后根据查到的内容组织答案。这样做的好处是,模型不需要“记住”所有知识,需要什么文档现查就行。知识更新也方便,文档改了,答案自然就变了,不用重新训练模型。
本文要介绍的是怎么用 Ollama + LangChain 在自己电脑上搭这么一套系统。不需要什么高端显卡,普通的开发机就能跑起来。
## 问题描述
先说说为什么我们需要本地部署的 RAG,而不是直接调用 ChatGPT 或者 Claude 的 API。
最直接的原因是数据隐私。我之前帮一家律所做顾问,他们根本不可能把客户文件上传到第三方服务器。医疗、金融行业同样存在这个问题。数据一旦离开自己的服务器,后续的风险就不可控了。
响应速度是另一个考量。云端 API 的延迟受网络影响波动较大,高峰期尤其明显。本地模型响应通常更稳定,体感上会快不少。
还有成本。API 调用是按次数收费的,量大之后费用相当可观。一次性部署本地模型虽然有前期投入,但边际成本几乎为零。
当然,本地方案也有局限。模型规模受限于本地硬件配置,复杂问题的处理能力不如云端大模型。维护和更新需要一定的技术能力。这些都是需要在做决策前权衡的因素。
## 详细步骤
### 环境准备
我们用 Python 来实现。建议使用 3.10 或更高版本,内存至少 16GB,32GB 会更从容。
“`bash
# 创建虚拟环境
python -m venv rag_env
source rag_env/bin/activate # Linux/Mac
# 或 rag_env\Scripts\activate # Windows
# 安装核心依赖
pip install langchain langchain-community langchain-text-splitters
pip install faiss-cpu pypdf sentence-transformers
pip install ollama
“`
如果有 NVIDIA 显卡,可以把 faiss-cpu 换成 faiss-gpu,检索速度会快很多。
### 下载本地模型
Ollama 支持直接下载模型。我们需要两个:一个把文字转成向量(嵌入模型),一个根据上下文生成回答(生成模型)。
“`bash
# 下载嵌入模型
ollama pull nomic-embed-text
# 下载生成模型
ollama pull llama3.2:latest
“`
模型大小从几百 MB 到几 GB 不等,看你选的具体版本。第一次下载会比较慢,之后就是秒加载。
### 构建知识库
这一步做的事情是把文档 loading 进来,切成小段,转成向量,存到向量数据库里。
“`python
from langchain_community.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.embeddings import OllamaEmbeddings
from langchain_community.vectorstores import FAISS
import os
# 配置路径
DOCS_PATH = “./documents” # 存放文档的目录
VECTOR_STORE_PATH = “./vector_store” # 向量数据库保存路径
def load_documents(docs_path):
“””加载目录下的所有 PDF 文档”””
documents = []
for file in os.listdir(docs_path):
if file.endswith(“.pdf”):
loader = PyPDFLoader(os.path.join(docs_path, file))
documents.extend(loader.load())
return documents
def split_documents(documents):
“””将文档分割成较小的 chunks”””
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=50,
separators=[“\n\n”, “\n”, “。”, ” “, “”]
)
return text_splitter.split_documents(documents)
def create_vector_store(documents):
“””创建向量数据库”””
embeddings = OllamaEmbeddings(
model=”nomic-embed-text”,
base_url=”http://localhost:11434″
)
vector_store = FAISS.from_documents(
documents=documents,
embedding=embeddings
)
vector_store.save_local(VECTOR_STORE_PATH)
return vector_store
# 主流程
print(“正在加载文档…”)
docs = load_documents(DOCS_PATH)
print(f”已加载 {len(docs)} 页文档”)
print(“正在分割文档…”)
chunks = split_documents(docs)
print(f”已分割为 {len(chunks)} 个文本块”)
print(“正在创建向量数据库…”)
vector_store = create_vector_store(chunks)
print(“向量数据库创建完成!”)
“`
### 构建 RAG 链
现在把检索和生成串起来,形成一个完整的问答流程。
“`python
from langchain_community.chat_models import ChatOllama
from langchain.chains import create_retrieval_chain
from langchain.chains.combine_documents import create_stuff_documents_chain
from langchain import hub
def create_rag_chain(vector_store):
“””创建 RAG 问答链”””
llm = ChatOllama(
model=”llama3.2:latest”,
base_url=”http://localhost:11434″,
temperature=0.7
)
prompt = hub.pull(“langchain-ai/retrieval-qa-chat”)
combine_docs_chain = create_stuff_documents_chain(
llm=llm,
prompt=prompt
)
retrieval_chain = create_retrieval_chain(
retriever=vector_store.as_retriever(
search_type=”similarity”,
search_kwargs={“k”: 3}
),
combine_docs_chain=combine_docs_chain
)
return retrieval_chain
# 加载保存的向量数据库
print(“正在加载向量数据库…”)
embeddings = OllamaEmbeddings(
model=”nomic-embed-text”,
base_url=”http://localhost:11434″
)
vector_store = FAISS.load_local(
VECTOR_STORE_PATH,
embeddings,
allow_dangerous_deserialization=True
)
print(“正在初始化 RAG 链…”)
rag_chain = create_rag_chain(vector_store)
print(“RAG 系统准备就绪!”)
“`
### 交互式问答
代码跑起来后,就可以向系统提问了。
“`python
def ask_question(chain, query):
“””向 RAG 系统提问”””
print(f”\n问题: {query}”)
print(“-” * 50)
result = chain.invoke({“input”: query})
print(f”回答: {result[‘answer’]}”)
print(“-” * 50)
print(“参考来源:”)
for i, doc in enumerate(result[‘context’], 1):
source = doc.metadata.get(‘source’, ‘Unknown’)
page = doc.metadata.get(‘page’, ‘N/A’)
print(f” {i}. {source} (第 {page} 页)”)
return result
if __name__ == “__main__”:
print(“\n” + “=” * 50)
print(“本地 RAG 知识库系统”)
print(“输入问题进行查询,输入 ‘quit’ 退出”)
print(“=” * 50)
while True:
query = input(“\n请输入问题: “).strip()
if query.lower() in [‘quit’, ‘exit’, ‘q’]:
print(“感谢使用!”)
break
if not query:
continue
try:
ask_question(rag_chain, query)
except Exception as e:
print(f”发生错误: {str(e)}”)
“`
## 运行结果
跑起来之后大概是这么个效果:
“`
==================================================
本地 RAG 知识库系统
输入问题进行查询,输入 ‘quit’ 退出
==================================================
请输入问题: 公司的年假政策是什么?
————————————————–
回答: 根据员工手册的规定,公司正式员工每年享有 15 天带薪年假。
工龄满 1 年不满 10 年的,年休假 5 天;已满 10 年不满 20 年的,
年休假 10 天;已满 20 年的,年休假 15 天。年假应当在本年度内使用,
可以一次性或分段安排。
如因工作需要无法在本年度使用,经批准后可延至次年 3 月 31 日前使用。
————————————————–
参考来源:
1. ./documents/员工手册.pdf (第 15 页)
2. ./documents/员工手册.pdf (第 16 页)
3. ./documents/HR政策.pdf (第 8 页)
“`
除了答案本身,系统还会列出参考来源。这个很重要——用户可以自己点进去核实,不至于完全盲从模型的输出。这也是 RAG 相比纯生成模型的一个实际优势。
## 总结
这套方案跑下来,感觉比较适合以下几类场景:
对数据安全要求高的企业,比如律所、医疗机构、金融单位。知识库需要频繁更新的场景,比如客服 FAQ、产品文档。预算有限但有一定技术能力的团队。
当然它不是万能的。复杂问题还是云端大模型处理得更好,硬件配置也直接决定了体验。如果团队没有专职运维,可以考虑混合方案——核心的、敏感的知识库放本地,一般性查询走 API。
工具层面,Ollama 把模型部署的门槛降得很低,LangChain 的封装也很完善。两者配合,核心逻辑两百行代码就能写完。对于想自己动手试试的人来说,是个不错的起点。