导语:你是否曾想让大模型读懂你私有的 PDF、TXT 文档,却又担心数据泄露?你是否受够了通用大模型的“幻觉”,渴望一个能基于你指定材料精准回答的专家?本文将带你从零开始,利用 LangChain 框架,结合 Ollama 本地部署的国产大模型(如 Qwen2),搭建一个完全运行在本地的 RAG(检索增强生成)知识库问答系统。全程干货,附完整源码,助你轻松踏入 AI 应用开发的大门!


一、为什么你需要 RAG ?

大语言模型(LLM)虽然强大,但存在两个致命短板:

  1. 知识截止:训练数据停留在过去,无法回答最新问题。

  2. 幻觉问题:对于不懂的内容,会一本正经地胡说八道,这在企业场景中是灾难性的。

RAG (Retrieval-Augmented Generation) 正是解决这两大痛点的银弹。它的核心思想非常简单:先检索,再增强,后生成
当用户提问时,系统会先从你的私有知识库中精准检索出最相关的文档片段,然后将这些片段作为“参考资料”连同原问题一起塞给大模型,强制模型基于这些资料进行回答。这样既利用了 LLM 的强大理解与生成能力,又牢牢锁定了答案的边界,大大减少了幻觉。

今天,我们就用目前最主流的框架 LangChain,打造一个 100% 本地运行的 RAG 系统。

二、技术栈选型与环境准备

为了做到完全免费、数据不出本机,我们选用以下全链路开源/本地方案:

  • LLM 与 Embedding 模型Ollama 本地部署 qwen2:7b(大语言模型)和 nomic-embed-text(文本转向量模型)。

  • 框架LangChain(负责串联检索与生成流程)。

  • 向量数据库FAISS(Meta 开源的高效向量检索库)。

  • 文档加载:PyPDF2, unstructured 等。

  • UI 界面Gradio 或 Streamlit(本文提供 Streamlit 版本的轻量代码)。

2.1 安装 Ollama 并拉取模型

首先前往 Ollama 官网 下载并安装客户端。安装完成后,打开终端拉取所需模型:

bash

复制

下载

# 拉取通义千问 7B 模型(性能平衡,对中文友好)
ollama pull qwen2:7b

# 拉取轻量级 Embedding 模型,用于将文本转换为向量
ollama pull nomic-embed-text

验证模型是否就绪:

bash

复制

下载

ollama list

2.2 安装 Python 依赖库

建议使用 Conda 创建虚拟环境,然后安装以下核心库(本文写作时基于最新版):

bash

复制

下载

pip install langchain langchain-community langchain-ollama faiss-cpu pypdf unstructured streamlit

提示:unstructured 加载复杂文档依赖较多,如果仅需处理简单 TXT 或 PDF 纯文本,也可使用 PyPDFLoader,更轻量。

三、系统搭建核心步骤

我们将整个流程拆解为四个模块:

  1. 文档加载与智能切分

  2. 向量化存储(索引构建)

  3. 检索增强链组装

  4. Streamlit 前端交互

3.1 第一步:文档加载与智能切分

良好的切分是 RAG 成功的基石。切分太短会丢失上下文,太长则检索精度下降。我们使用 LangChain 的 RecursiveCharacterTextSplitter,它会按照段落、句子、字符的优先级递归拆分。

python

复制

下载

from langchain_community.document_loaders import PyPDFLoader, TextLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter

def load_and_split_documents(directory_path: str):
    """
    加载指定目录下的 PDF 和 TXT 文件,并切分为文档块。
    """
    documents = []
    # 遍历目录,根据文件类型选用不同的加载器
    import os
    for file in os.listdir(directory_path):
        file_path = os.path.join(directory_path, file)
        if file.endswith(".pdf"):
            loader = PyPDFLoader(file_path)
            documents.extend(loader.load())
        elif file.endswith(".txt"):
            loader = TextLoader(file_path, encoding='utf-8')
            documents.extend(loader.load())
    
    # 文本切分:chunk_size=500,chunk_overlap=50 是实践中常用的折中值
    text_splitter = RecursiveCharacterTextSplitter(
        chunk_size=500,
        chunk_overlap=50,
        separators=["\n\n", "\n", "。", "!", "?", ";", ",", " ", ""]
    )
    chunks = text_splitter.split_documents(documents)
    print(f"共加载 {len(documents)} 个文档,切分为 {len(chunks)} 个块。")
    return chunks

3.2 第二步:向量化与向量库构建

利用 Ollama 的 embedding 模型将文本块转为向量,存入 FAISS。LangChain 提供了极其简便的接口。

python

复制

下载

from langchain_ollama import OllamaEmbeddings
from langchain_community.vectorstores import FAISS

# 初始化 Embedding 模型
embedding_model = OllamaEmbeddings(
    model="nomic-embed-text",  # 与上面 pull 的模型名一致
    base_url="http://localhost:11434"  # Ollama 本地服务地址
)

def create_vector_store(chunks, persist_path="./faiss_index"):
    """
    将文档块向量化,并保存本地 FAISS 索引。
    """
    # 直接从文档列表构建 FAISS 向量库
    vectorstore = FAISS.from_documents(
        documents=chunks,
        embedding=embedding_model
    )
    # 持久化到本地,下次可直接加载,无需重新向量化
    vectorstore.save_local(persist_path)
    print("向量库已构建并保存。")
    return vectorstore

# 快速加载已有索引的函数
def load_vector_store(persist_path="./faiss_index"):
    return FAISS.load_local(persist_path, embedding_model, allow_dangerous_deserialization=True)

注意:allow_dangerous_deserialization=True 仅在加载自己信任的索引时使用。

3.3 第三步:组装 RAG 问答链

这是灵魂所在。我们使用 LangChain 的 检索器 (Retriever) 和 提示模板 (Prompt Template) 来构建 Chain。

python

复制

下载

from langchain_ollama import ChatOllama
from langchain.prompts import ChatPromptTemplate
from langchain.schema.runnable import RunnablePassthrough
from langchain.schema.output_parser import StrOutputParser

# 初始化本地千问模型
llm = ChatOllama(
    model="qwen2:7b",
    temperature=0.1,  # 偏保守,降低幻觉
    base_url="http://localhost:11434"
)

# 自定义 Prompt 模板:明确要求模型仅基于上下文回答,不知道就说不知道
prompt_template = """
你是一个严谨的文档知识助手。请仅根据以下【上下文】中的信息回答用户问题。
如果你从上下文中找不到足够的信息,请直接回答“根据提供的文档,我无法回答该问题”,不要编造。

【上下文】:
{context}

【用户问题】:
{question}

【回答】:
"""
prompt = ChatPromptTemplate.from_template(prompt_template)

def build_rag_chain(vectorstore):
    """
    构建 RAG 链:检索 → 填充 Prompt → LLM 生成
    """
    # 创建检索器,k=4 表示每次返回最相关的4个文本块
    retriever = vectorstore.as_retriever(search_kwargs={"k": 4})
    
    # 定义链式处理逻辑
    rag_chain = (
        {"context": retriever, "question": RunnablePassthrough()}
        | prompt
        | llm
        | StrOutputParser()  # 直接输出字符串结果
    )
    return rag_chain

链的工作流解释

  1. 用户输入 question 字符串。

  2. RunnablePassthrough() 原样传递 question,同时触发 retriever 通过相似度搜索获取 context

  3. 两者一起填充进 prompt 模板。

  4. 完整的 Prompt 发送给 ChatOllama 模型。

  5. StrOutputParser 将模型返回的对象解析成纯文本。

3.4 第四步:用 Streamlit 搭建轻量 UI

为了让系统更好用,我们花十分钟搭建一个网页交互界面。

python

复制

下载

import streamlit as st

# ---- 初始化向量库和 RAG 链(使用缓存避免重复加载) ----
@st.cache_resource
def init_system():
    # 尝试加载已保存的索引,若不存在则提示先构建
    try:
        vectorstore = load_vector_store("./faiss_index")
        print("加载已有索引成功。")
    except:
        st.error("未找到向量索引!请先运行构建索引的脚本。")
        st.stop()
    rag_chain = build_rag_chain(vectorstore)
    return rag_chain

st.set_page_config(page_title="本地知识库问答", page_icon="📚")
st.title("📚 基于本地文档的 RAG 智能问答")
st.markdown("技术栈:Ollama + LangChain + FAISS + Streamlit")

# 初始化系统
with st.spinner("正在加载模型和索引,首次加载可能需要几秒钟..."):
    rag_chain = init_system()

# 聊天输入框
query = st.text_input("请输入你的问题:", placeholder="例如:这份合同的有效期是多久?")

if query:
    with st.chat_message("user"):
        st.write(query)
    with st.chat_message("assistant"):
        with st.spinner("思考中..."):
            # 调用 RAG 链,使用 invoke 获取回答
            answer = rag_chain.invoke(query)
            st.write(answer)

运行你的应用

bash

复制

下载

streamlit run app.py

浏览器会自动打开 http://localhost:8501,一个完全本地运行的知识库问答系统就诞生了!

https://img-blog.csdnimg.cn/direct/2b3c4d5e6f7g8h9i0j1k.png
(图:Streamlit 问答界面效果示意)

四、进阶优化与避坑指南

4.1 检索质量优化

  • 元数据过滤:当文档有类别、时间等属性时,可在检索时使用 filter 参数精准缩小范围。

  • 混合检索:结合关键词(BM25)与向量检索,覆盖精确匹配和语义匹配,LangChain 有 BM25Retriever

  • 重排序:检索回 TopK 个片段后,用更精细的 CrossEncoder 模型重新打分排序,提升最相关片段的顺位。

4.2 大模型回答调优

  • 调整 temperature:事实问答类设为 0~0.2,创意生成类可适当调高。

  • Prompt 工程:一定要在 Prompt 中加上“如果你不知道,就明确说不知道”之类的强制约束,这是对抗幻觉最直接有效的方式。

  • 多轮对话:若需支持记忆,可集成 ConversationBufferMemory 或使用 LangChain 的 RunnableWithMessageHistory

4.3 性能与资源

  • Ollama 量化:默认拉取的可能是 4-bit 量化版,资源占用很低,普通笔记本也能运行 7B 模型。

  • 索引持久化:切记将向量库 save_local,避免每次重启都重新切片、向量化。

五、完整源码与总结

本文的所有代码均已整理,核心文件包含:

  • build_index.py:文档切分、向量化、存储索引。

  • app.py:Streamlit 前端与 RAG 链调用。

你可以这样组织项目结构

text

复制

下载

rag_local_project/
├── docs/               # 放入你的PDF、TXT文档
├── faiss_index/        # 自动生成的向量库文件夹
├── build_index.py
└── app.py

通过这个项目,你不仅掌握了 RAG 的核心理论,还落地了一套可扩展的本地知识库底座。在此基础上,你可以轻松接入更多文档类型(Word、Markdown)、替换更强大的模型(如 Qwen2.5 或 Llama3),甚至部署到企业内部服务器。

大模型时代的魅力在于,你不再仅仅是技术的使用者,而是可以亲手创造出符合自己需求的 AI 应用。 如果你在复现过程中遇到任何问题,欢迎在评论区留言,我会一一解答。如果本文对你有帮助,不要吝啬你的 点赞、收藏,这是对我最大的鼓励!

更多推荐