本文将带你从零开始,一步步搭建一个功能完整的本地 RAG 智能问答系统。我们将使用最流行的开源工具栈,全程不需要任何 API 密钥,所有数据都保存在你的电脑上。

目录

一、RAG 技术原理:为什么它是大模型的 "外挂大脑"

1.1 大模型的固有局限

在深入 RAG 之前,我们需要先理解为什么单纯的大模型无法满足私有数据问答的需求:

  • 知识截止问题:所有大模型都有训练数据的截止日期,无法获取最新信息
  • 幻觉问题:当大模型不知道答案时,会编造看似合理但完全错误的内容
  • 数据隐私问题:将企业内部文档上传到第三方 API 存在严重的安全风险
  • 专业知识不足:通用大模型在特定领域的专业知识深度有限

1.2 RAG 的核心工作流程

RAG 的核心思想非常简单:先检索,再生成。整个流程分为两个主要阶段:

索引阶段(离线)

  1. 加载你的私有文档(PDF、Word、Markdown、TXT 等)
  2. 将文档分割成大小合适的文本块(Chunks)
  3. 使用嵌入模型(Embedding Model)将每个文本块转换为向量
  4. 将向量存储到向量数据库中,建立索引

查询阶段(在线)

  1. 用户输入问题
  2. 使用相同的嵌入模型将问题转换为向量
  3. 在向量数据库中搜索与问题向量最相似的前 K 个文本块
  4. 将问题和检索到的文本块一起组装成提示词
  5. 发送给大语言模型生成最终答案

这个流程就像我们考试时先翻书找到相关知识点,再结合自己的理解回答问题一样。大模型不再需要记住所有知识,只需要学会如何 "查阅" 资料并组织语言。

1.3 本地 RAG 的优势

相比基于云 API 的 RAG 方案,本地部署有以下不可替代的优势:

  • 绝对的数据安全:所有数据都在本地处理,不会泄露给任何第三方
  • 无 API 成本:不需要为每次查询付费,适合高频使用场景
  • 离线可用:不需要互联网连接,在断网环境下也能正常工作
  • 完全可控:可以根据自己的需求定制和优化每一个环节

二、技术选型:打造最佳本地 RAG 栈

为了实现一个高性能、易部署的本地 RAG 系统,我们选择了以下开源工具组合:

表格

组件 工具选择 版本 优势说明
大语言模型 Llama 3 8B Instruct v1.0 Meta 最新开源模型,中文能力优秀,8B 参数适合消费级显卡
嵌入模型 BGE-M3 v1.0 目前最好的开源多语言嵌入模型,支持中英双语
向量数据库 Chroma v0.5.0 轻量级嵌入式向量数据库,无需单独部署
文档加载器 LangChain v0.2.0 支持几乎所有常见文档格式的加载和解析
本地模型运行 Ollama v0.1.38 一行命令即可运行本地大模型,管理极其方便
Web 界面 Gradio v4.29.0 快速构建美观的交互式 Web 界面

这个技术栈的最大特点是简单易用且性能强大。特别是 Ollama 的出现,彻底解决了本地大模型部署复杂的问题,让普通用户也能轻松运行。

三、环境准备:5 分钟完成基础配置

3.1 安装 Ollama

Ollama 是一个开源的本地大模型运行工具,支持 Windows、macOS 和 Linux。

Windows/macOS 用户: 直接从官网下载安装包:https://ollama.com/download

Linux 用户

bash

运行

curl -fsSL https://ollama.com/install.sh | sh

安装完成后,打开终端运行以下命令下载我们需要的模型:

bash

运行

# 下载Llama 3 8B中文优化版(约4.7GB)
ollama pull llama3:8b-instruct-q4_0

# 下载BGE-M3嵌入模型(约1.2GB)
ollama pull bge-m3

这两个模型都是量化后的版本,在保证性能的同时大幅降低了硬件要求。

3.2 硬件要求

表格

硬件配置 运行效果
8GB 内存 + 无独立显卡 可以运行,但速度较慢(约 5-10 token/s)
16GB 内存 + NVIDIA 4GB 显存显卡 流畅运行(约 15-20 token/s)
16GB 内存 + NVIDIA 8GB 显存显卡 非常流畅(约 30-40 token/s)
32GB 内存 + NVIDIA 16GB 显存显卡 极速体验(约 60+ token/s)

即使你没有独立显卡,只要有足够的内存,也可以在 CPU 上运行整个系统。

3.3 安装 Python 依赖

创建一个新的 Python 虚拟环境,然后安装所需的依赖包:

bash

运行

# 创建虚拟环境
python -m venv rag-env

# 激活虚拟环境
# Windows
rag-env\Scripts\activate
# macOS/Linux
source rag-env/bin/activate

# 安装依赖
pip install langchain==0.2.0
pip install langchain-community==0.2.0
pip install langchain-ollama==0.1.0
pip install chromadb==0.5.0
pip install pypdf==4.2.0
pip install python-docx==1.1.2
pip install gradio==4.29.0
pip install sentence-transformers==2.7.0

四、核心实现:一步步构建 RAG 系统

现在我们开始编写代码,实现 RAG 系统的各个核心模块。

4.1 文档加载与分割

首先,我们需要实现一个通用的文档加载器,支持多种常见格式:

python

运行

from langchain_community.document_loaders import (
    PyPDFLoader,
    TextLoader,
    Docx2txtLoader,
    DirectoryLoader
)
from langchain_text_splitters import RecursiveCharacterTextSplitter
import os

def load_documents(directory_path):
    """加载指定目录下的所有文档"""
    loaders = {
        ".pdf": PyPDFLoader,
        ".txt": TextLoader,
        ".docx": Docx2txtLoader,
        ".md": TextLoader,
    }
    
    documents = []
    
    for root, dirs, files in os.walk(directory_path):
        for file in files:
            file_extension = os.path.splitext(file)[1].lower()
            if file_extension in loaders:
                file_path = os.path.join(root, file)
                loader_class = loaders[file_extension]
                loader = loader_class(file_path)
                documents.extend(loader.load())
                print(f"已加载: {file_path}")
    
    print(f"\n总共加载了 {len(documents)} 个文档")
    return documents

def split_documents(documents):
    """将文档分割成合适大小的文本块"""
    text_splitter = RecursiveCharacterTextSplitter(
        chunk_size=1000,  # 每个块的大小(字符数)
        chunk_overlap=200,  # 块之间的重叠部分
        separators=["\n\n", "\n", ".", " ", ""],  # 分割优先级
        length_function=len
    )
    
    chunks = text_splitter.split_documents(documents)
    print(f"分割成了 {len(chunks)} 个文本块")
    return chunks

4.2 创建向量数据库

接下来,我们使用 Chroma 创建向量数据库并存储文档向量:

python

运行

from langchain_ollama import OllamaEmbeddings
from langchain_chroma import Chroma

def create_vector_database(chunks, persist_directory="./chroma_db"):
    """创建向量数据库并存储文档向量"""
    # 使用BGE-M3嵌入模型
    embeddings = OllamaEmbeddings(model="bge-m3")
    
    # 创建向量数据库
    db = Chroma.from_documents(
        documents=chunks,
        embedding=embeddings,
        persist_directory=persist_directory
    )
    
    print(f"向量数据库创建完成,存储位置: {persist_directory}")
    return db

def load_vector_database(persist_directory="./chroma_db"):
    """加载已存在的向量数据库"""
    embeddings = OllamaEmbeddings(model="bge-m3")
    
    db = Chroma(
        persist_directory=persist_directory,
        embedding_function=embeddings
    )
    
    return db

4.3 构建 RAG 问答链

现在我们将检索器和大模型结合起来,创建完整的 RAG 问答链:

python

运行

from langchain_ollama import ChatOllama
from langchain.chains import RetrievalQA
from langchain.prompts import PromptTemplate

def create_rag_chain(db):
    """创建RAG问答链"""
    # 初始化本地大模型
    llm = ChatOllama(
        model="llama3:8b-instruct-q4_0",
        temperature=0.1,  # 温度越低,回答越确定
        top_p=0.9,
        num_ctx=4096  # 上下文窗口大小
    )
    
    # 创建检索器,返回最相似的4个文本块
    retriever = db.as_retriever(search_kwargs={"k": 4})
    
    # 自定义提示词模板
    prompt_template = """
    你是一个专业的智能问答助手。请基于以下提供的上下文信息来回答用户的问题。
    
    回答规则:
    1. 只能使用上下文信息中明确提到的内容来回答问题
    2. 如果上下文信息中没有相关内容,请明确说"根据提供的资料,我无法回答这个问题"
    3. 不要编造任何不在上下文中的信息
    4. 回答要简洁、准确、有条理
    5. 如果用户的问题与上下文无关,请礼貌地说明你只能回答与提供资料相关的问题
    
    上下文信息:
    {context}
    
    用户问题:{question}
    
    你的回答:
    """
    
    PROMPT = PromptTemplate(
        template=prompt_template,
        input_variables=["context", "question"]
    )
    
    # 创建RAG问答链
    qa_chain = RetrievalQA.from_chain_type(
        llm=llm,
        chain_type="stuff",
        retriever=retriever,
        return_source_documents=True,
        chain_type_kwargs={"prompt": PROMPT}
    )
    
    return qa_chain

4.4 主程序与 Web 界面

最后,我们使用 Gradio 创建一个美观的 Web 界面,让用户可以方便地使用我们的 RAG 系统:

python

运行

import gradio as gr
import shutil

def initialize_system(directory_path):
    """初始化RAG系统"""
    try:
        # 清空之前的向量数据库
        if os.path.exists("./chroma_db"):
            shutil.rmtree("./chroma_db")
        
        # 加载和处理文档
        documents = load_documents(directory_path)
        if not documents:
            return None, "错误:没有找到任何支持的文档格式"
        
        chunks = split_documents(documents)
        
        # 创建向量数据库
        db = create_vector_database(chunks)
        
        # 创建问答链
        qa_chain = create_rag_chain(db)
        
        return qa_chain, f"系统初始化成功!已加载 {len(documents)} 个文档,分割成 {len(chunks)} 个文本块"
    
    except Exception as e:
        return None, f"初始化失败:{str(e)}"

def answer_question(qa_chain, question):
    """回答用户问题"""
    if qa_chain is None:
        return "请先上传文档并初始化系统"
    
    try:
        result = qa_chain.invoke({"query": question})
        answer = result["result"]
        
        # 添加来源信息
        sources = []
        for i, doc in enumerate(result["source_documents"]):
            source = doc.metadata.get("source", "未知来源")
            page = doc.metadata.get("page", "未知页码")
            sources.append(f"来源 {i+1}: {os.path.basename(source)} (第 {page} 页)")
        
        full_answer = f"{answer}\n\n---\n" + "\n".join(sources)
        
        return full_answer
    
    except Exception as e:
        return f"回答失败:{str(e)}"

# 创建Gradio界面
with gr.Blocks(title="本地RAG智能问答系统") as demo:
    gr.Markdown("# 🧠 本地RAG智能问答系统")
    gr.Markdown("上传你的文档,然后就可以向AI提问了!所有数据都在本地处理,绝对安全。")
    
    qa_chain_state = gr.State(None)
    
    with gr.Row():
        with gr.Column(scale=1):
            gr.Markdown("### 文档上传")
            directory_input = gr.Textbox(
                label="文档目录路径",
                placeholder="例如: ./my_documents",
                value="./documents"
            )
            init_button = gr.Button("初始化系统", variant="primary")
            status_output = gr.Textbox(label="系统状态", interactive=False)
            
            init_button.click(
                fn=initialize_system,
                inputs=[directory_input],
                outputs=[qa_chain_state, status_output]
            )
        
        with gr.Column(scale=2):
            gr.Markdown("### 智能问答")
            question_input = gr.Textbox(
                label="你的问题",
                placeholder="请输入你想询问的问题..."
            )
            answer_button = gr.Button("提问", variant="primary")
            answer_output = gr.Textbox(
                label="AI回答",
                interactive=False,
                lines=10
            )
            
            answer_button.click(
                fn=answer_question,
                inputs=[qa_chain_state, question_input],
                outputs=[answer_output]
            )
            
            # 支持回车键提交
            question_input.submit(
                fn=answer_question,
                inputs=[qa_chain_state, question_input],
                outputs=[answer_output]
            )
    
    gr.Markdown("""
    ### 使用说明
    1. 将你的文档(PDF、TXT、DOCX、MD)放在一个文件夹中
    2. 在左侧输入文件夹路径,点击"初始化系统"
    3. 等待系统处理完成后,就可以在右侧提问了
    4. 回答会显示相关文档的来源和页码
    """)

if __name__ == "__main__":
    # 创建默认文档目录
    if not os.path.exists("./documents"):
        os.makedirs("./documents")
    
    demo.launch(
        server_name="0.0.0.0",
        server_port=7860,
        share=False,
        inbrowser=True
    )

五、效果演示:看看我们的 RAG 系统有多强大

现在我们来测试一下这个系统的实际效果。我准备了一份关于 "人工智能发展历史" 的 PDF 文档,包含了从 1956 年达特茅斯会议到 2024 年最新进展的内容。

5.1 系统初始化

运行主程序后,浏览器会自动打开 Web 界面。在左侧输入文档目录路径,点击 "初始化系统"。系统会自动加载所有文档,分割成文本块,并创建向量数据库。

整个过程只需要几秒钟到几分钟,取决于文档的数量和大小。

5.2 问答测试

现在我们可以开始提问了:

问题 1:"人工智能这个术语是在哪一年被正式提出的?"

AI 回答: 人工智能这个术语是在 1956 年的达特茅斯会议上被正式提出的。这次会议由约翰・麦卡锡(John McCarthy)组织,马文・明斯基(Marvin Minsky)、克劳德・香农(Claude Shannon)等科学家参加。


来源 1: 人工智能发展历史.pdf (第 2 页) 来源 2: 人工智能发展历史.pdf (第 3 页)

问题 2:"2022 年发布的大模型是什么?"

AI 回答: 2022 年 11 月 30 日,OpenAI 发布了 ChatGPT,这是一个基于 GPT-3.5 架构的大型语言模型。ChatGPT 的发布标志着人工智能进入了生成式 AI 时代,引发了全球范围内的 AI 热潮。


来源 1: 人工智能发展历史.pdf (第 15 页)

问题 3:"2025 年人工智能会有什么发展?"

AI 回答: 根据提供的资料,我无法回答这个问题。


来源 1: 人工智能发展历史.pdf (第 20 页)

可以看到,我们的 RAG 系统能够准确回答文档中存在的问题,并且在遇到文档中没有的信息时,会诚实地说不知道,而不是编造答案。这正是 RAG 技术的核心价值所在。

六、优化方向:让你的 RAG 系统更强大

我们已经实现了一个基础但功能完整的 RAG 系统。如果你想进一步提升性能,可以从以下几个方面进行优化:

6.1 文档处理优化

  • 更智能的文档分割:使用语义分割代替简单的字符分割
  • 元数据增强:为每个文本块添加标题、章节、作者等元数据
  • 多模态支持:支持图片、表格等非文本内容的处理

6.2 检索优化

  • 重排序(Reranking):使用交叉编码器对初步检索结果进行重排序
  • 混合检索:结合关键词检索和向量检索的优势
  • 查询重写:让大模型先优化用户的查询语句再进行检索

6.3 生成优化

  • 更好的提示词工程:设计更专业、更详细的提示词模板
  • 上下文压缩:只保留与问题最相关的上下文信息
  • 多轮对话支持:实现对话历史记忆功能

6.4 模型优化

  • 使用更大的模型:如果硬件允许,可以使用 Llama 3 70B 或 Qwen 2 72B
  • 模型量化:使用更高效的量化方法,在性能和速度之间取得平衡
  • LoRA 微调:在你的私有数据上微调大模型,进一步提升效果

七、总结与展望

在本文中,我们从零开始搭建了一个完全本地运行的 RAG 智能问答系统。这个系统可以让你用自然语言查询自己的私有文档,所有数据都保存在本地,绝对安全可靠。

RAG 技术正在快速发展,未来我们可以期待:

  • 更高效的嵌入模型:更小的体积,更好的效果
  • 端到端的 RAG 系统:将检索和生成融合在一个模型中
  • 多模态 RAG:支持图片、视频、音频等多种模态的检索和生成
  • 自主 RAG 代理:能够自主规划检索策略和回答流程的 AI 代理

本地 RAG 系统为企业和个人提供了一种安全、高效的大模型应用方式。它让我们能够充分利用大模型的能力,同时又不必担心数据隐私问题。

希望本文能够帮助你入门 RAG 技术。如果你有任何问题或建议,欢迎在评论区留言交流。让我们一起探索 AI 的无限可能!

Logo

AtomGit AI 社区提供模型库、数据集、Agent、Token等资源

更多推荐