# 使用 LangChain + Ollama 构建本地知识库问答系统
## 背景介绍
在企业日常运营中,我们经常需要处理大量的内部文档、技术手册、会议记录等文本数据。传统的关键词搜索方式往往难以理解用户的真实查询意图,返回的结果也不够精准。随着大语言模型(LLM)技术的快速发展,基于语义理解的智能问答系统成为了可能。
然而,将企业内部数据上传到第三方 API 服务存在数据泄露的风险。对于金融、医疗、法律等敏感行业来说,数据安全是首要考量。这就催生了对本地化部署的强烈需求,在自有服务器上运行 LLM,既能保证数据安全,又能获得智能问答的能力。
Ollama 是一个开源的本地 LLM 运行环境,它支持在本地机器上运行各种开源模型,如 Llama 2、Mistral、Gemma 等。LangChain 则是一个 LLM 应用开发框架,提供了丰富的工具来构建 RAG(检索增强生成)应用。将这两者结合,我们可以构建一个完全本地化的知识库问答系统。
## 问题描述
构建本地知识库问答系统面临以下几个核心挑战:
**第一,文档处理。** 企业的知识文档格式多样,包括 PDF、Word、Markdown、纯文本等。需要一套统一的流程来解析这些不同格式的文件,提取出可用的文本内容。
**第二,向量存储。** 为了实现语义搜索,需要将文本转换为向量并存储到向量数据库中。常用的选择包括 Chroma、FAISS、Milvus 等。
**第三,检索与生成。** 当用户提出问题时,系统需要在向量数据库中检索出最相关的文档片段,然后将这些片段作为上下文提供给 LLM,让它生成准确的回答。
**第四,本地部署。** 所有计算都必须在本地完成,不能依赖任何外部 API。这意味着需要选择适合本地硬件的模型,并优化运行效率。
本文将详细讲解如何从零开始搭建这样一个系统,涵盖环境配置、依赖安装、代码实现、运行测试等完整流程。
## 详细步骤
### 步骤一:环境准备
首先需要确认你的机器满足基本要求。由于要运行 LLM,建议内存至少 16GB,硬盘剩余空间不少于 20GB。操作系统可以是 Linux、macOS 或 Windows(需要 WSL2)。
安装 Ollama 是第一步。访问 Ollama 官网(https://ollama.com),根据你的操作系统下载对应的安装包。安装完成后,在终端运行以下命令下载模型:
“`bash
ollama pull llama2
ollama pull nomic-embed-text
“`
第一个是用于生成回答的主模型,第二个是用于生成文本嵌入的模型。这两个模型加起来需要大约 7GB 的磁盘空间。
### 步骤二:创建项目目录
“`bash
mkdir -p ~/local-kb-qa
cd ~/local-kb-qa
“`
### 步骤三:安装 Python 依赖
建议使用 Python 3.10 或更高版本。创建虚拟环境并安装所需库:
“`bash
python -m venv venv
source venv/bin/activate
pip install langchain langchain-community langchain-ollama \
chromadb beautifulsoup4 pypdf python-docx \
streamlit
“`
这些包的作用分别是:langchain 和 langchain-ollama 用于连接 Ollama 和构建问答链;chromadb 是向量数据库;beautifulsoup4 用于解析网页;pypdf 和 python-docx 用于处理 PDF 和 Word 文档;streamlit 用来快速搭建一个可视化界面。
### 步骤四:准备知识文档
在项目目录下创建一个 data 文件夹,放入你的知识文档。可以是 txt、md、pdf、docx 等格式。为了演示,我们创建几个示例文件:
“`bash
mkdir -p ~/local-kb-qa/data
“`
你也可以放置真实的业务文档进去。
### 步骤五:编写文档加载器
创建 loader.py 文件,实现对不同格式文档的加载:
“`python
from langchain_community.document_loaders import (
TextLoader,
PyPDFLoader,
Docx2txtLoader,
UnstructuredMarkdownLoader,
DirectoryLoader
)
from langchain.text_splitter import RecursiveCharacterTextSplitter
def load_documents(data_path: str):
“””
加载指定目录下的所有文档
“””
# 根据文件类型选择加载器
loaders = {
“.txt”: TextLoader,
“.md”: UnstructuredMarkdownLoader,
“.pdf”: PyPDFLoader,
“.docx”: Docx2txtLoader,
}
from pathlib import Path
documents = []
# 遍历目录下的所有文件
for file_path in Path(data_path).rglob(“*”):
if file_path.suffix in loaders:
try:
loader_class = loaders[file_path.suffix]
loader = loader_class(str(file_path))
documents.extend(loader.load())
print(f”已加载: {file_path.name}”)
except Exception as e:
print(f”加载 {file_path.name} 失败: {e}”)
return documents
def split_documents(documents, chunk_size=500, chunk_overlap=50):
“””
将文档分割成小块
“””
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=chunk_size,
chunk_overlap=chunk_overlap,
length_function=len,
)
return text_splitter.split_documents(documents)
if __name__ == “__main__”:
# 测试加载
docs = load_documents(“./data”)
print(f”共加载 {len(docs)} 个文档”)
“`
### 步骤六:创建向量存储
创建 vectorstore.py 文件,将文档内容转换为向量并存储:
“`python
from langchain_ollama import OllamaEmbeddings
from langchain_chroma import Chroma
def create_vector_store(documents, persist_directory=”./chroma_db”):
“””
创建向量存储
“””
# 使用 Ollama 的嵌入模型
embeddings = OllamaEmbeddings(
model=”nomic-embed-text”,
base_url=”http://localhost:11434″
)
# 创建 Chroma 向量数据库
vectorstore = Chroma.from_documents(
documents=documents,
embedding=embeddings,
persist_directory=persist_directory
)
print(f”向量数据库已创建,包含 {vectorstore._collection.count()} 个向量”)
return vectorstore
def load_vector_store(persist_directory=”./chroma_db”):
“””
加载已存在的向量数据库
“””
embeddings = OllamaEmbeddings(
model=”nomic-embed-text”,
base_url=”http://localhost:11434″
)
vectorstore = Chroma(
persist_directory=persist_directory,
embedding_function=embeddings
)
return vectorstore
if __name__ == “__main__”:
from loader import load_documents, split_documents
docs = load_documents(“./data”)
chunks = split_documents(docs)
vectorstore = create_vector_store(chunks)
“`
### 步骤七:构建问答系统
创建 qa_system.py 文件,实现完整的问答功能:
“`python
from langchain_ollama import ChatOllama
from langchain.chains import RetrievalQA
from langchain.prompts import PromptTemplate
class LocalQA:
def __init__(self, model_name=”llama2″, vectorstore=None):
“””
初始化本地问答系统
“””
self.llm = ChatOllama(
model=model_name,
base_url=”http://localhost:11434″,
temperature=0.7,
)
self.vectorstore = vectorstore
# 自定义提示词模板
self.prompt_template = “””你是一个专业的知识库问答助手。请根据以下参考文档回答用户的问题。
参考文档:
{context}
用户问题:{question}
请基于参考文档给出准确、详细的回答。如果参考文档中没有相关信息,请如实告知用户。”””
def set_vectorstore(self, vectorstore):
“””设置向量数据库”””
self.vectorstore = vectorstore
def answer(self, question: str) -> str:
“””
回答用户问题
“””
if self.vectorstore is None:
return “请先加载知识库”
# 检索相关文档
docs = self.vectorstore.similarity_search(question, k=3)
# 合并检索到的文档内容
context = “\n\n”.join([doc.page_content for doc in docs])
# 构建提示词
prompt = self.prompt_template.format(
context=context,
question=question
)
# 调用 LLM 生成回答
response = self.llm.invoke(prompt)
return response.content
def main():
“””测试问答系统”””
from vectorstore import load_vector_store
vectorstore = load_vector_store()
qa = LocalQA(vectorstore=vectorstore)
test_questions = [
“什么是 LangChain?”,
“Ollama 支持哪些模型?”,
“如何安装 LangChain?”
]
for question in test_questions:
print(f”\n问题: {question}”)
answer = qa.answer(question)
print(f”回答: {answer}”)
print(“-” * 50)
if __name__ == “__main__”:
main()
“`
### 步骤八:创建可视化界面
为了使用更方便,我们用 Streamlit 快速搭建一个 Web 界面。创建 app.py:
“`python
import streamlit as st
from qa_system import LocalQA
from vectorstore import load_vector_store
st.set_page_config(
page_title=”本地知识库问答系统”,
page_icon=”🤖”
)
st.title(“🤖 本地知识库问答系统”)
if qa_system not in st.session_state:
st.session_state.qa_system = None
st.session_state.vectorstore = None
with st.sidebar:
st.header(“设置”)
import requests
try:
response = requests.get(“http://localhost:11434/api/tags”)
if response.status_code == 200:
st.success(“✅ Ollama 服务正常运行”)
else:
st.error(“❌ Ollama 服务未运行”)
except:
st.error(“❌ 无法连接到 Ollama,请确保已启动服务”)
if st.button(“🔄 重新加载知识库”):
with st.spinner(“加载中…”):
try:
st.session_state.vectorstore = load_vector_store()
st.session_state.qa_system = LocalQA(vectorstore=st.session_state.vectorstore)
st.success(f”✅ 知识库已加载,包含 {st.session_state.vectorstore._collection.count()} 个文档片段”)
except Exception as e:
st.error(f”加载失败: {e}”)
if st.session_state.vectorstore is None:
with st.spinner(“首次加载知识库…”):
try:
st.session_state.vectorstore = load_vector_store()
st.session_state.qa_system = LocalQA(vectorstore=st.session_state.vectorstore)
st.success(f”✅ 知识库已加载,包含 {st.session_state.vectorstore._collection.count()} 个文档片段”)
except Exception as e:
st.error(f”加载知识库失败: {e}”)
question = st.text_input(“请输入您的问题:”, placeholder=”例如:什么是 RAG?”)
if question:
if st.session_state.qa_system is None:
st.warning(“请先等待知识库加载完成”)
else:
with st.spinner(“思考中…”):
answer = st.session_state.qa_system.answer(question)
st.markdown(“### 回答”)
st.write(answer)
with st.expander(“查看参考文档”):
docs = st.session_state.vectorstore.similarity_search(question, k=3)
for i, doc in enumerate(docs, 1):
st.markdown(f”**来源 {i}:** {doc.metadata.get(source, 未知)}”)
st.write(doc.page_content)
“`
## 运行结果
完成了所有代码编写后,按以下步骤运行系统:
首先,确保 Ollama 服务正在运行:
“`bash
ollama serve
“`
然后,在项目目录下运行 Streamlit 应用:
“`bash
streamlit run app.py
“`
浏览器会自动打开 http://localhost:8500,你应该能看到一个简洁的 Web 界面。在输入框中输入你的问题,系统会从本地知识库中检索相关内容,并结合 LLM 生成回答。
首次运行时,系统会加载你放入 data 目录中的所有文档,将其分割成小块,转换为向量并存储到 Chroma 数据库中。这个过程可能需要几分钟时间,取决于文档数量和大小。加载完成后,你可以在侧边栏看到已处理的文档片段数量。
系统支持连续提问,每次问题都会基于知识库中的内容给出回答。如果知识库中没有相关信息,系统会如实告知,而不是编造答案。
## 总结
本文详细介绍了如何使用 LangChain 和 Ollama 构建一个完全本地化的知识库问答系统。整个系统运行在本地机器上,不需要连接任何外部 API,有效保护了企业数据的隐私和安全。
核心技术点包括:使用 LangChain 的文档加载器处理多种格式的文件;通过 RecursiveCharacterTextSplitter 将长文档分割成适合处理的小块;利用 Ollama 的嵌入模型将文本转换为向量;使用 Chroma 向量数据库存储和检索相似文档;最后通过 LangChain 的 RetrievalQA 链结合 LLM 生成最终回答。
这个系统具有很强的扩展性。你可以根据实际需求更换不同的开源模型,如 Mistral、Gemma 等;也可以添加更多的文档格式支持;还可以在此基础上实现文件上传、批量问答等功能。对于有更高性能要求的场景,可以考虑使用 GPU 加速或部署到显存更大的服务器上。