使用 Ollama + LangChain 构建本地 RAG 知识库问答系统(完整指南)

在人工智能快速发展的今天,大语言模型(LLM)已经成为了开发者必备的工具。然而,如何将 LLM 与私有数据结合,一直是企业级应用的核心挑战。RAG(Retrieval Augmented Generation,检索增强生成)技术的出现,完美解决了这个问题。

传统的 LLM 应用面临几个痛点:第一,模型的知识受限于训练数据,无法获取实时或私有信息;第二,微调模型成本高昂,需要大量 GPU 资源;第三,数据隐私是企业最关心的问题,将敏感数据上传到第三方 API 存在风险。

Ollama 的出现改变了这一切。它是一个开源的本地 LLM 运行环境,支持在个人电脑上运行各种开源模型,如 Llama 2、Mistral、Qwen 等。配合 LangChain 这个强大的 LLM 应用框架,我们可以轻松构建完全本地化的 RAG 系统,所有数据都保存在本地,无需依赖任何外部服务。

这篇文章将详细介绍如何从零开始搭建一个本地 RAG 知识库问答系统。整个过程不需要云服务,所有计算都在本地完成,既保证了数据安全,又节省了成本。

问题描述

在实际工作中,我们经常遇到这样的场景:企业有大量的内部文档、产品手册、技术文档,这些资料散落在各处,查找起来非常困难。传统的做法是建立一个全文搜索系统,但这种方案只能返回包含关键词的文档片段,无法理解用户的自然语言问题,也无法生成连贯的答案。

另一个常见场景是客服系统。企业需要训练客服人员熟悉大量产品信息,这个过程既耗时又昂贵。如果能让 AI 自动理解产品文档,并准确回答用户的问题,将大幅提升工作效率。

RAG 技术正是为了解决这些问题而设计的。它的核心思想是:首先将文档分成小块,建立向量索引;当用户提问时,通过语义相似度找到最相关的文档片段;最后将用户问题和相关文档一起发送给 LLM,让模型基于提供的上下文生成答案。

但是,搭建一个生产级的 RAG 系统并非易事。我们需要处理文档加载、文本分块、向量存储、相似度检索、prompt 模板等多个环节。每个环节都有很多细节需要考虑,比如如何选择合适的分块大小、如何优化检索精度、如何处理多种格式的文档等。

本文将通过一个完整的代码示例,展示如何使用 Ollama 和 LangChain 构建一个可用的本地 RAG 系统。我们会涵盖从环境准备到最终部署的全过程,并提供详细的代码解释和运行结果。

详细步骤

第一步:环境准备

首先,我们需要安装必要的软件和依赖。整个项目使用 Python 开发,推荐使用 Python 3.10 或更高版本。

在开始之前,请确保你的电脑上至少有 16GB 内存,因为运行本地 LLM 需要较大的内存空间。如果内存不足,可以选择较小的模型,如 Qwen-1.8B 或 Phi-2。

创建项目目录并初始化:

mkdir local-rag-demo
cd local-rag-demo
python -m venv venv
source venv/bin/activate

安装核心依赖:

pip install langchain langchain-community langchain-text-splitters langchain-ollama sentence-transformers chromadb pypdf python-docx beautifulsoup4 requests

第二步:安装和配置 Ollama

访问 Ollama 官网(https://ollama.com)下载适合你操作系统的版本。安装完成后,在终端中运行:

ollama --version

下载我们需要的模型。考虑到本地运行的速度和资源占用,推荐使用 Qwen-2.5 或 Phi-3 这类对硬件要求相对较低但效果不错的模型:

ollama pull qwen2.5:7b
ollama pull phi3:mini
ollama list

第三步:准备知识库文档

在项目中创建一个 data 文件夹,将需要导入的文档放进去。支持的格式包括 PDF、Word、TXT、Markdown 等。

local-rag-demo/
├── data/
│   ├── product-manual.pdf
│   ├── faq.md
│   └── tech-docs.docx
├── src/
├── main.py
└── requirements.txt

第四步:构建 RAG 流程

接下来,我们逐步实现 RAG 的各个组件。首先是文档加载器,负责读取各种格式的文件;然后是文本分割器,将长文档分成合适大小的片段;接着是向量存储,用 Embedding 模型将文本转换为向量;最后是检索和问答链,将所有组件串联起来。

完整代码示例

1. 文档加载模块 (src/loader.py)

"""文档加载模块 - 支持多种格式的文档加载"""

from pathlib import Path
from typing import List
from langchain_community.document_loaders import (
    PyPDFLoader,
    TextLoader,
    Docx2txtLoader,
    UnstructuredMarkdownLoader,
    BSHTMLLoader,
)
from langchain_core.documents import Document


class DocumentLoader:
    """统一文档加载器"""
    
    SUPPORTED_EXTENSIONS = {
        .pdf: PyPDFLoader,
        .txt: TextLoader,
        .docx: Docx2txtLoader,
        .doc: Docx2txtLoader,
        .md: UnstructuredMarkdownLoader,
        .html: BSHTMLLoader,
        .htm: BSHTMLLoader,
    }
    
    def __init__(self, encoding: str = utf-8):
        self.encoding = encoding
    
    def load_file(self, file_path: str) -> List[Document]:
        path = Path(file_path)
        suffix = path.suffix.lower()
        
        if suffix not in self.SUPPORTED_EXTENSIONS:
            raise ValueError(f"不支持的文件类型: {suffix}")
        
        loader_class = self.SUPPORTED_EXTENSIONS[suffix]
        
        if suffix in [.txt, .md, .html, .htm]:
            loader = loader_class(file_path, encoding=self.encoding)
        else:
            loader = loader_class(file_path)
        
        return loader.load()
    
    def load_directory(self, directory: str) -> List[Document]:
        path = Path(directory)
        all_docs = []
        
        for file_path in path.rglob(*):
            if file_path.is_file() and file_path.suffix.lower() in self.SUPPORTED_EXTENSIONS:
                try:
                    docs = self.load_file(str(file_path))
                    for doc in docs:
                        doc.metadata[source] = str(file_path)
                    all_docs.extend(docs)
                    print(f"已加载: {file_path.name}")
                except Exception as e:
                    print(f"加载失败 {file_path.name}: {e}")
        
        return all_docs


__all__ = [DocumentLoader]

2. 文本分割模块 (src/splitter.py)

"""文本分割模块 - 将长文档分割成小块"""

from typing import List
from langchain.text_splitter import (
    RecursiveCharacterTextSplitter,
    MarkdownHeaderTextSplitter,
)
from langchain_core.documents import Document


class TextSplitter:
    """文本分割器"""
    
    def __init__(
        self,
        chunk_size: int = 500,
        chunk_overlap: int = 50,
        separators: List[str] = None,
    ):
        self.chunk_size = chunk_size
        self.chunk_overlap = chunk_overlap
        self.separators = separators or [
            "\n\n", "\n", "。", "!", "?", ".", "!", "?", " ", "",
        ]
    
    def split_documents(self, documents: List[Document]) -> List[Document]:
        text_splitter = RecursiveCharacterTextSplitter(
            chunk_size=self.chunk_size,
            chunk_overlap=self.chunk_overlap,
            separators=self.separators,
            length_function=len,
        )
        return text_splitter.split_documents(documents)


__all__ = [TextSplitter]

3. 向量存储模块 (src/vectorstore.py)

"""向量存储模块 - 使用 Chroma 实现本地向量存储"""

from typing import List, Optional
from langchain_community.vectorstores import Chroma
from langchain_community.embeddings import OllamaEmbeddings
from langchain_core.documents import Document


class VectorStoreManager:
    """向量存储管理器"""
    
    def __init__(
        self,
        persist_directory: str = "./chroma_db",
        embedding_model: str = "nomic-embed-text",
        model_base_url: str = "http://localhost:11434",
    ):
        self.persist_directory = persist_directory
        self.embedding_model = embedding_model
        self.model_base_url = model_base_url
        
        self.embeddings = OllamaEmbeddings(
            model=embedding_model,
            base_url=model_base_url,
        )
        
        self.vectorstore = None
    
    def create_from_documents(
        self,
        documents: List[Document],
        collection_name: str = "rag-collection",
    ) -> Chroma:
        self.vectorstore = Chroma.from_documents(
            documents=documents,
            embedding=self.embeddings,
            persist_directory=self.persist_directory,
            collection_name=collection_name,
        )
        
        print(f"已创建向量存储,包含 {len(documents)} 个文档片段")
        return self.vectorstore
    
    def load_existing(self, collection_name: str = "rag-collection") -> Chroma:
        self.vectorstore = Chroma(
            persist_directory=self.persist_directory,
            embedding_function=self.embeddings,
            collection_name=collection_name,
        )
        return self.vectorstore


__all__ = [VectorStoreManager]

4. 问答链模块 (src/chain.py)

"""问答链模块 - 构建 RAG 问答流程"""

from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnableParallel, RunnablePassthrough
from langchain_community.chat_models import ChatOllama


class RAGChain:
    """RAG 问答链"""
    
    DEFAULT_TEMPLATE = """你是一个专业的技术文档助手。请根据以下参考文档回答用户的问题。

参考文档:
{context}

用户问题:{question}

要求:
1. 只根据提供的参考文档回答,不要编造信息
2. 如果参考文档中没有相关信息,请明确告知用户
3. 回答要简洁、准确
4. 在回答末尾注明信息来源

回答:"""
    
    def __init__(
        self,
        retriever,
        model_name: str = "qwen2.5:7b",
        model_base_url: str = "http://localhost:11434",
        template: str = None,
        temperature: float = 0.7,
    ):
        self.retriever = retriever
        self.model_name = model_name
        self.model_base_url = model_base_url
        self.template = template or self.DEFAULT_TEMPLATE
        self.temperature = temperature
        
        self.llm = ChatOllama(
            model=model_name,
            base_url=model_base_url,
            temperature=temperature,
        )
        
        self._build_chain()
    
    def _build_chain(self):
        prompt = PromptTemplate(
            template=self.template,
            input_variables=["context", "question"],
        )
        
        def format_docs(docs):
            return "\n\n".join(
                f"来源: {doc.metadata.get(source, 未知)}\n内容: {doc.page_content}"
                for doc in docs
            )
        
        self.chain = (
            RunnableParallel(
                context=self.retriever | format_docs,
                question=RunnablePassthrough(),
            )
            | prompt
            | self.llm
            | StrOutputParser()
        )
    
    def invoke(self, question: str) -> str:
        return self.chain.invoke(question)


__all__ = [RAGChain]

5. 主程序 (main.py)

"""本地 RAG 知识库问答系统 - 主程序"""

import os
import argparse
from src.loader import DocumentLoader
from src.splitter import TextSplitter
from src.vectorstore import VectorStoreManager
from src.chain import RAGChain


def setup_knowledge_base(data_dir: str, persist_dir: str = "./chroma_db"):
    print("=" * 50)
    print("开始构建知识库...")
    print("=" * 50)
    
    print("\n[1/4] 加载文档...")
    loader = DocumentLoader(encoding=utf-8)
    documents = loader.load_directory(data_dir)
    print(f"共加载 {len(documents)} 个文档")
    
    print("\n[2/4] 分割文本...")
    splitter = TextSplitter(chunk_size=500, chunk_overlap=50)
    split_docs = splitter.split_documents(documents)
    print(f"共分割成 {len(split_docs)} 个文本块")
    
    print("\n[3/4] 创建向量存储...")
    vectorstore_manager = VectorStoreManager(
        persist_directory=persist_dir,
        embedding_model="nomic-embed-text",
    )
    vectorstore_manager.create_from_documents(
        documents=split_docs,
        collection_name="knowledge-base",
    )
    
    retriever = vectorstore_manager.vectorstore.as_retriever(
        search_kwargs={"k": 4}
    )
    
    print("\n[4/4] 知识库构建完成!")
    return retriever, vectorstore_manager


def load_existing_knowledge_base(persist_dir: str = "./chroma_db"):
    print("加载已存在的知识库...")
    vectorstore_manager = VectorStoreManager(persist_directory=persist_dir)
    vectorstore_manager.load_existing(collection_name="knowledge-base")
    
    retriever = vectorstore_manager.vectorstore.as_retriever(
        search_kwargs={"k": 4}
    )
    
    return retriever, vectorstore_manager


def main():
    parser = argparse.ArgumentParser(description="本地 RAG 知识库问答系统")
    parser.add_argument("--data-dir", default="./data", help="文档目录")
    parser.add_argument("--persist-dir", default="./chroma_db", help="向量数据库目录")
    parser.add_argument("--model", default="qwen2.5:7b", help="LLM 模型名称")
    parser.add_argument("--rebuild", action="store_true", help="重建知识库")
    args = parser.parse_args()
    
    os.makedirs(args.data_dir, exist_ok=True)
    
    if args.rebuild or not os.path.exists(args.persist_dir):
        retriever, vectorstore = setup_knowledge_base(
            data_dir=args.data_dir,
            persist_dir=args.persist_dir,
        )
    else:
        retriever, vectorstore = load_existing_knowledge_base(
            persist_dir=args.persist_dir,
        )
    
    print("\n初始化问答链...")
    rag_chain = RAGChain(
        retriever=retriever,
        model_name=args.model,
    )
    
    print("\n" + "=" * 50)
    print("知识库问答系统已启动!")
    print("输入问题进行查询,输入 quit 或 exit 退出")
    print("=" * 50 + "\n")
    
    while True:
        question = input("你: ").strip()
        
        if question.lower() in [quit, exit, q]:
            print("感谢使用,再见!")
            break
        
        if not question:
            continue
        
        try:
            print("\nAI: ", end="", flush=True)
            response = rag_chain.invoke(question)
            print(response)
        except Exception as e:
            print(f"\n错误: {e}")
        
        print()


if __name__ == "__main__":
    main()

运行结果

完成代码编写后,我们来测试整个系统。

1. 创建测试文档

在 data 目录下创建一个测试文档,然后运行:

python main.py --rebuild

首次运行会加载文档、分割文本、创建向量存储。完成后即可进行问答测试。

2. 测试问答

问题 1:产品支持哪些数据库?

AI: 根据参考文档,本产品支持 MySQL、PostgreSQL、MongoDB 等主流数据库。

问题 2:如何安装产品?

AI: 安装步骤如下:1. 下载安装包 2. 解压到指定目录 3. 运行 install.sh 4. 配置数据库连接 5. 启动服务

问题 3:产品的价格是多少?

AI: 对不起,参考文档中没有包含产品价格信息。如果您需要了解具体价格,请联系销售人员。

从测试结果可以看出,系统能够准确理解问题,并从知识库中检索相关信息进行回答。当问题超出知识库范围时,系统也会如实告知用户。

总结

本文详细介绍了如何使用 Ollama 和 LangChain 构建一个本地 RAG 知识库问答系统。我们从环境准备开始,逐步完成了文档加载、文本分割、向量存储和问答链的搭建。

整个系统具有以下特点:

首先是本地化部署。所有计算都在本地完成,数据不需要上传到任何外部服务器,充分保证了数据隐私安全。这对于处理企业内部敏感文档尤为重要。

其次是轻量级运行。借助 Ollama 的优化,我们可以在消费级硬件上运行大语言模型。7B 参数的模型在 16GB 内存的电脑上就能流畅运行。

第三是灵活扩展。系统采用模块化设计,可以方便地添加新的文档格式、接入不同的 Embedding 模型,或者切换到其他 LLM。

第四是开箱即用。代码包含了完整的异常处理和日志输出,只需要简单的配置就能运行。

在实际应用中,这个系统还有很多可以优化的方向:比如调整文本分块的大小来平衡召回率和精确度;添加缓存机制来加速重复查询;实现增量更新来避免每次重建索引;或者添加对话历史来实现多轮对话。

RAG 技术正在快速发展,各种开源工具和模型也在不断更新。如果你对构建企业级知识库系统感兴趣,建议持续关注 LangChain、LlamaIndex 等框架的更新,以及 Mistral、Qwen 等模型的最新进展。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇