🚀

用 Python + Ollama 搭一个本地知识库问答系统,离线也能问文档

把 PDF、Markdown、TXT 扔进本地知识库,用自然语言提问就能得到精准答案。零成本、全离线、数据不出本机。

👁️ - 次阅读

你有没有遇到过这种场景:公司有一份几十页的技术文档,你想快速找到某个接口的用法,翻半天找不到,问同事又不好意思打扰。

今天做一个本地知识库问答系统。把各种格式的文档丢进去,然后用自然语言提问,系统会自动检索相关内容并生成回答。全程离线运行,数据完全在你自己手里。

项目背景

RAG(检索增强生成)这几年很火,但大多数方案都需要联网调用 API。这份教程教你用纯本地方案搭建一个知识库问答系统。

适用场景:

  • 企业内部文档知识库
  • 法律法规条文快速查询
  • 技术文档智能检索
  • 学术论文辅助阅读

核心流程就三步:文档切片 → 向量化存储 → 检索生成回答。

技术选型

组件选择理由
大模型Ollama + qwen2.5开源中文能力强,本地运行
向量数据库ChromaDB零配置,Python SDK 友好
文档解析LangChain 文档加载器支持 PDF、MD、TXT 多种格式
嵌入模型nomic-embed-text轻量级,中文效果不错
Web 界面Gradio几行代码出界面

实现步骤

第一步:安装 Ollama 和模型

ollama.com 下载安装。然后拉取两个模型:

# 问答用的生成模型(选一个适合你显存的)
ollama pull qwen2.5:7b

# 文本嵌入模型(用于文档向量化)
ollama pull nomic-embed-text

第二步:安装 Python 依赖

pip install langchain langchain-community chromadb gradio PyPDF2 python-docx

第三步:创建知识库构建脚本 build_knowledge_base.py

"""
本地知识库构建脚本
将文档目录中的文件切片、向量化,存入 ChromaDB
"""
import os
import glob
from langchain_community.document_loaders import (
    PyPDFLoader,
    TextLoader,
    UnstructuredMarkdownLoader,
    Docx2txtLoader
)
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.embeddings import OllamaEmbeddings
from langchain_community.vectorstores import Chroma

# 支持的文档类型和对应的加载器
LOADER_MAP = {
    ".pdf": PyPDFLoader,
    ".txt": TextLoader,
    ".md": UnstructuredMarkdownLoader,
    ".docx": Docx2txtLoader,
}

def load_documents(doc_dir: str):
    """加载指定目录下的所有文档"""
    documents = []
    
    for filepath in glob.glob(os.path.join(doc_dir, "**/*"), recursive=True):
        ext = os.path.splitext(filepath)[1].lower()
        
        if ext not in LOADER_MAP:
            print(f"⏭️  跳过不支持的文件: {filepath}")
            continue
        
        print(f"📄 正在加载: {filepath}")
        loader = LOADER_MAP[ext](filepath)
        documents.extend(loader.load())
    
    return documents


def chunk_documents(documents, chunk_size=500, chunk_overlap=50):
    """将文档切分为小块,便于向量化"""
    splitter = RecursiveCharacterTextSplitter(
        chunk_size=chunk_size,
        chunk_overlap=chunk_overlap,
        separators=["\n\n", "\n", "。", ".", "!"]
    )
    chunks = splitter.split_documents(documents)
    print(f"✅ 文档已切分为 {len(chunks)} 个片段")
    return chunks


def build_vector_store(chunks, persist_dir="./chroma_kb"):
    """将文本块向量化并存入 ChromaDB"""
    embeddings = OllamaEmbeddings(model="nomic-embed-text")
    
    # 如果已有持久化数据,直接加载
    if os.path.exists(persist_dir):
        print(f"📂 加载已有向量库: {persist_dir}")
        db = Chroma(persist_directory=persist_dir, embedding_function=embeddings)
        # 追加新文档
        db.add_documents(chunks)
    else:
        print(f"🆕 创建新的向量库: {persist_dir}")
        db = Chroma.from_documents(
            documents=chunks,
            embedding=embeddings,
            persist_directory=persist_dir
        )
    
    db.persist()
    print("💾 向量库已保存")
    return db


def main():
    doc_dir = "./documents"  # 把文档放在这里
    
    if not os.path.exists(doc_dir):
        print(f"❌ 找不到文档目录: {doc_dir}")
        print("请在当前目录下创建 documents 文件夹,放入你的文档")
        return
    
    docs = load_documents(doc_dir)
    if not docs:
        print("❌ 没有找到任何可处理的文档")
        return
    
    chunks = chunk_documents(docs)
    build_vector_store(chunks)
    print("\n🎉 知识库构建完成!可以开始提问了。")


if __name__ == "__main__":
    main()

第四步:创建问答脚本 chat_knowledge_base.py

"""
知识库问答脚本
从向量库中检索相关内容,交给大模型生成回答
"""
import os
from langchain_community.embeddings import OllamaEmbeddings
from langchain_community.vectorstores import Chroma
from langchain_community.llms import Ollama
from langchain.chains import RetrievalQA
from langchain.prompts import PromptTemplate

def load_vectorstore(persist_dir="./chroma_kb"):
    """加载持久化的向量库"""
    embeddings = OllamaEmbeddings(model="nomic-embed-text")
    db = Chroma(persist_directory=persist_dir, embedding_function=embeddings)
    return db


def create_qa_chain(db):
    """创建检索增强问答链"""
    retriever = db.as_retriever(
        search_type="similarity",
        search_kwargs={"k": 4}  # 返回最相关的4个片段
    )
    
    llm = Ollama(model="qwen2.5:7b", temperature=0.2, max_tokens=1024)
    
    # 自定义提示词模板
    prompt_template = """基于以下已知信息,简洁而专业地回答用户的问题。
如果已知信息中不包含答案,请如实回答"抱歉,知识库中没有相关内容"。

已知信息:
{context}

用户问题:{question}

回答:"""
    
    prompt = PromptTemplate(
        template=prompt_template,
        input_variables=["context", "question"]
    )
    
    qa_chain = RetrievalQA.from_chain_type(
        llm=llm,
        chain_type="stuff",
        retriever=retriever,
        chain_type_kwargs={"prompt": prompt}
    )
    
    return qa_chain


def ask_question(qa_chain, question):
    """提问并获取回答"""
    result = qa_chain.invoke({"query": question})
    return result["result"]


def main():
    persist_dir = "./chroma_kb"
    
    if not os.path.exists(persist_dir):
        print("❌ 找不到向量库,请先运行 build_knowledge_base.py 构建知识库")
        return
    
    print("📚 正在加载知识库...")
    db = load_vectorstore(persist_dir)
    qa_chain = create_qa_chain(db)
    
    print("\n🤖 知识库问答系统已就绪")
    print("输入问题(输入 quit 退出):\n")
    
    while True:
        question = input("❓ 你的问题: ").strip()
        if question.lower() in ("quit", "exit", "q"):
            print("👋 再见!")
            break
        if not question:
            continue
        
        print("\n🔍 正在检索和生成回答...")
        answer = ask_question(qa_chain, question)
        print(f"\n💡 {answer}\n")


if __name__ == "__main__":
    main()

第五步:创建 Gradio Web 界面

"""
Web 界面 — 用 Gradio 快速搭建可视化问答页面
"""
import os
from chat_knowledge_base import load_vectorstore, create_qa_chain

persist_dir = "./chroma_kb"
db = load_vectorstore(persist_dir)
qa_chain = create_qa_chain(db)


def answer(question, history):
    """Gradio 回调函数"""
    if not question.strip():
        return history
    response = ask_question(qa_chain, question)
    history.append((question, response))
    return history


if __name__ == "__main__":
    import gradio as gr
    
    gr.Interface(
        fn=answer,
        inputs=[
            gr.Textbox(placeholder="请输入你的问题...", lines=2),
            gr.Chatbot(label="对话记录", height=400)
        ],
        title="📚 本地知识库问答系统",
        description="基于 Ollama + ChromaDB 的离线 RAG 问答系统。请先用 build_knowledge_base.py 构建知识库。",
        allow_flagging="never"
    ).launch(server_name="0.0.0.0", server_port=7860)

第六步:运行

# 1. 把文档放进 documents 目录
mkdir -p documents
cp ~/some-report.pdf documents/
cp ~/notes.md documents/

# 2. 构建知识库
python build_knowledge_base.py

# 3. 启动 Web 界面
python web_ui.py
# 访问 http://localhost:7860

运行效果

启动后打开浏览器,你会看到一个简洁的聊天界面。

提问示例:

问:这份文档里关于 API 鉴权的说明是什么?

答:根据知识库内容,API 鉴权使用 Bearer Token 方式。客户端需要在请求头中携带 Authorization: Bearer <your_token>,token 有效期为 24 小时。

系统会在回答下方标注引用来源(如果你修改 prompt 加上 source 提取的话)。

优化方向

  1. 混合检索:加入 BM25 关键词检索,和向量语义检索结合,精度更高
  2. 文档元数据:给每个文档片段加上来源标记,回答时引用原文段落
  3. 增量更新:监听目录变化,自动添加新文档到向量库
  4. 多模型切换:复杂问题用大模型,简单问题用小模型,节省资源
  5. 权限控制:对接企业 LDAP,不同部门看到不同的知识库

整个项目从零到可用大概需要 20 分钟。关键是 Ollama 的模型选择——如果你的机器显存够,qwen2.5:14b 效果会更好;如果只有 CPU,用 qwen2.5:1.5b 也能跑。

试试把你手头的文档丢进去,看看效果如何?

这篇文章对你有帮助吗?