在本地构建 RAG 问答系统:使用 Ollama 和 LangChain 实现私有知识库

背景介绍

很多企业在做内部文档问答时,第一反应是把文件丢给 ChatGPT 或者 Claude。这确实方便,但把公司内部资料上传到第三方服务这件事,足以让法务部门跳起来。数据隐私这事儿不是闹着玩的。

Ollama 出现之后,情况变了。我们可以在自己的电脑上跑大语言模型,配合 LangChain 的 RAG 框架,整个问答系统完全可以私有化部署。本文记录的就是这个过程,从环境搭建到跑通第一个问答。

问题描述

实际需求很具体:

  1. 数据不能出门:公司内部的敏感文档不可能上传到云端
  2. 离线也要能用:有些场景完全没网络,必须本地提供服务
  3. 省钱:按 token 收费的 API 调用,长期看是一笔不小的开销
  4. 垂直领域:需要针对特定业务领域做优化,通用模型有时答不到点子上

把文档喂给云端 API 是最省事的办法,但隐私风险始终是个坎。RAG 的思路是先把文档转成向量存到本地,然后只把用户的问题发送给本地的大模型。这样既保住了数据安全,又能用到 LLM 的能力。

详细步骤

环境准备

依赖包就这些,基本都是常规操作:

# 创建虚拟环境
python -m venv rag_env
source rag_env/bin/activate

# 安装依赖
pip install langchain langchain-community langchain-chroma ollama chromadb beautifulsoup4 pypdf pydantic

电脑内存建议 16GB 以上,磁盘空间看你要跑多大的模型。

第一步:下载并运行 Ollama

Ollama 支持 macOS、Linux 和 Windows。Linux 安装比较简单:

# 安装 Ollama
curl -fsSL https://ollama.com/install.sh | sh

# 看看已经有哪些模型
ollama list

# 拉取 LLaMA 3,8B 版本大概 4.7GB
ollama pull llama3

# 中文效果更好的话,可以试试 qwen
ollama pull qwen:7b

# 启动服务,默认端口 11434
ollama serve

另一个终端试试看服务有没有跑起来:

curl http://localhost:11434/api/generate -d '{
  "model": "llama3",
  "prompt": "Hello",
  "stream": false
}'

返回一串 JSON 就说明没问题。

第二步:准备文档加载器

LangChain 的文档加载器支持 PDF、txt、Markdown、网页等各种格式。下面是加载 PDF 和纯文本文件的代码:

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

def load_documents(file_paths):
    documents = []
    for file_path in file_paths:
        if file_path.endswith('.pdf'):
            loader = PyPDFLoader(file_path)
        elif file_path.endswith('.txt'):
            loader = TextLoader(file_path, encoding='utf-8')
        elif file_path.endswith('.md'):
            loader = TextLoader(file_path, encoding='utf-8')
        else:
            continue
        documents.extend(loader.load())
    return documents

def split_documents(documents, chunk_size=1000, chunk_overlap=200):
    text_splitter = RecursiveCharacterTextSplitter(
        chunk_size=chunk_size,
        chunk_overlap=chunk_overlap,
        separators=["\n\n", "\n", " ", ""]
    )
    return text_splitter.split_documents(documents)

文档不能直接丢给向量数据库,必须先拆成小块。这个步骤叫做 chunking,分块的大小直接影响后续检索的效果。

第三步:创建向量存储

Chroma 是纯本地的向量数据库,整个过程不依赖任何外部服务:

from langchain_chroma import Chroma
from langchain_ollama import OllamaEmbeddings

def create_vector_store(documents, persist_directory="./chroma_db"):
    embeddings = OllamaEmbeddings(
        model="nomic-embed-text",
        base_url="http://localhost:11434"
    )
    vector_store = Chroma.from_documents(
        documents=documents,
        embedding=embeddings,
        persist_directory=persist_directory
    )
    return vector_store

别忘了先下载嵌入模型:

ollama pull nomic-embed-text

嵌入模型把文本转成向量,这些向量存储在 Chroma 里。检索的时候,用户的问题也会被转成向量,然后通过向量相似度找到最相关的文档块。

第四步:组装 RAG 链

把各个部分拼起来,形成完整的问答流程:

from langchain_ollama import ChatOllama
from langchain.chains import create_retrieval_chain
from langchain.chains.combine_documents import create_stuff_documents_chain
from langchain_core.prompts import ChatPromptTemplate

def build_qa_chain(vector_store):
    llm = ChatOllama(
        model="llama3",
        base_url="http://localhost:11434",
        temperature=0.7
    )
    
    system_prompt = """你是一个专业的问答助手。请根据以下上下文来回答用户的问题。
    上下文:
    {context}
    请用中文回答问题。"""
    
    prompt = ChatPromptTemplate.from_messages([
        ("system", system_prompt),
        ("human", "{input}")
    ])
    
    document_chain = create_stuff_documents_chain(llm, prompt)
    retriever = vector_store.as_retriever(
        search_type="similarity",
        search_kwargs={"k": 3}
    )
    retrieval_chain = create_retrieval_chain(retriever, document_chain)
    return retrieval_chain

这里 temperature 参数控制输出的随机性。0.7 是比较平衡的值,既有一定创意,又不会太离谱。

第五步:跑起来

完整代码如下:

def main():
    documents = load_documents(["./docs/技术文档.pdf", "./docs/产品说明.txt"])
    chunks = split_documents(documents)
    print(f"文档已分割为 {len(chunks)} 个块")
    
    vector_store = create_vector_store(chunks, persist_directory="./chroma_db")
    qa_chain = build_qa_chain(vector_store)
    
    print("=" * 50)
    print("RAG 问答系统已启动!输入问题开始查询,输入 quit 退出")
    print("=" * 50)
    
    while True:
        query = input("\n问题: ")
        if query.lower() in ['quit', 'exit', 'q']:
            print("再见!")
            break
        result = qa_chain.invoke({"input": query})
        print("\n回答:", result["answer"])

if __name__ == "__main__":
    main()

运行结果

第一次跑的时候,输出大概是这样:

文档已分割为 156 个块
==================================================
RAG 问答系统已启动!输入问题开始查询,输入 quit 退出
==================================================

问题: 这款产品的主要功能是什么?

==================================================
回答:
根据提供的产品文档,这款产品的主要功能包括:

1. 数据处理:支持批量处理 CSV、JSON 和 Excel 格式的数据文件
2. 自动化报告:可以根据预设模板自动生成日报、周报和月报
3. API 集成:提供 RESTful API 接口,支持与第三方系统对接
4. 用户管理:支持多用户协作和权限管理

==================================================

参考来源:
[1] 产品支持多种数据格式导入,包括 CSV、JSON...
[2] 自动化报告功能可以帮助用户节省...
[3] 系统提供完整的 API 接口文档...

可以看到,系统从本地文档里找到了相关内容,并基于这些内容生成了回答。整个过程都是本地运行的,没有任何数据流出。

总结

用 Ollama + LangChain 搭建本地 RAG 系统,核心优势就四点:

  1. 数据完全可控:文档存在自己电脑上,不涉及任何第三方服务
  2. 没有 API 费用:模型跑在本地,一次投入长期使用
  3. 离线也能用:断了网依然能回答问题
  4. 可以深度定制:针对特定领域可以换不同的模型,或者调整检索策略

缺点也有:本地模型的响应速度通常不如云端 API,特别是大模型。如果对延迟敏感,可以考虑用量化版本的模型,或者升级硬件配置。

完整代码就这些,改改路径和模型名称就能直接用。后续可以探索的方向还有:流式输出、多轮对话、对话历史持久化、集成更多文档格式支持等。

暂无评论

发送评论 编辑评论


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