🚀

本地文档问答机器人:Ollama + LangChain,离线也能用 AI 读文件

不用联网、不用付费 API,用 Ollama 本地模型 + LangChain 搭建一个能读取 PDF/Markdown 并回答问题的文档问答系统。隐私安全,完全免费。

👁️ - 次阅读

手上有几十份产品文档、技术手册或者合同 PDF,每次找资料都得翻半天。

能不能让 AI 帮你读?而且不用把文件传到云端——数据留在自己电脑上,安全又省事。

这个项目用 Ollama(本地大模型运行器)+ LangChain(AI 应用框架)搭一个文档问答系统。你丢一堆 PDF 进去,然后直接问问题,它会检索相关内容并回答。全程离线,零成本。

项目背景

企业文档、个人笔记、技术手册……这些数据往往不能上传到公共 AI 服务。用云端 API 有隐私风险,而且每次调用都要花钱。

本地方案的优势很明显:

  • 数据不出本机,敏感文档放心用
  • 一次安装,永久免费
  • 支持多种格式:PDF、Markdown、TXT
  • 可以换不同模型,按需调整速度和精度

技术选型

组件选型理由
本地模型Ollama + qwen2.5:7b中文理解好,7B 参数在普通笔记本上跑得动
向量数据库ChromaDB零配置,内存存储,适合个人项目
嵌入模型nomic-embed-text轻量级文本嵌入,中英文通用
框架LangChain封装了 RAG 全流程,上手快
PDF 解析PyMuPDF速度快,支持扫描件 OCR

全部依赖可以用 pip 一行装完。

实现步骤

第一步:安装 Ollama 和模型

先去 ollama.com 下载安装包。装好后打开终端:

# 下载并运行 qwen2.5 模型(中文能力强)
ollama pull qwen2.5:7b

# 下载嵌入模型
ollama pull nomic-embed-text

# 验证模型是否就绪
ollama list

启动 Ollama 服务(默认监听 11434 端口):

ollama serve &

第二步:安装 Python 依赖

pip install langchain langchain-community langchain-ollama chromadb pymupdf python-dotenv

第三步:编写文档问答脚本

在项目目录下创建 docs_qa.py

import os
import sys
from pathlib import Path
from langchain_community.document_loaders import PyMuPDFLoader, TextLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.embeddings import OllamaEmbeddings
from langchain_community.vectorstores import Chroma
from langchain_ollama import OllamaLLM
from langchain.chains import RetrievalQA

# 配置
MODEL_NAME = "qwen2.5:7b"
EMBEDDING_MODEL = "nomic-embed-text"
VECTOR_DB_PATH = "./chroma_db"
DOCS_DIR = "./documents"


def load_documents(doc_dir):
    """加载目录下的所有文档"""
    documents = []
    doc_path = Path(doc_dir)

    if not doc_path.exists():
        print(f"⚠️  文档目录不存在:{doc_dir}")
        print("创建目录并放入 PDF/TXT 文件后重试")
        doc_path.mkdir(parents=True, exist_ok=True)
        return documents

    for file_path in doc_path.iterdir():
        if file_path.suffix == ".pdf":
            print(f"📄 加载 PDF:{file_path.name}")
            loader = PyMuPDFLoader(str(file_path))
            documents.extend(loader.load())
        elif file_path.suffix in (".txt", ".md"):
            print(f"📝 加载文本:{file_path.name}")
            loader = TextLoader(str(file_path))
            documents.append(loader.load()[0])

    print(f"✅ 共加载 {len(documents)} 个文档块")
    return documents


def build_vector_store(docs):
    """切片文档并构建向量索引"""
    splitter = RecursiveCharacterTextSplitter(
        chunk_size=500,
        chunk_overlap=50,
        separators=["\n\n", "\n", "。", "!", "?", " ", ""]
    )
    chunks = splitter.split_documents(docs)
    print(f"🔪 切分为 {len(chunks)} 个片段")

    embeddings = OllamaEmbeddings(
        model=EMBEDDING_MODEL,
        base_url="http://localhost:11434"
    )

    vectorstore = Chroma.from_documents(
        documents=chunks,
        embedding=embeddings,
        persist_directory=VECTOR_DB_PATH
    )
    vectorstore.persist()
    print(f"💾 向量索引已保存至 {VECTOR_DB_PATH}")
    return vectorstore


def ask_question(vectorstore, question):
    """向文档提问"""
    llm = OllamaLLM(
        model=MODEL_NAME,
        base_url="http://localhost:11434",
        temperature=0.2,
        num_ctx=4096
    )

    retriever = vectorstore.as_retriever(
        search_type="similarity",
        search_kwargs={"k": 3}
    )

    qa_chain = RetrievalQA.from_chain_type(
        llm=llm,
        chain_type="stuff",
        retriever=retriever,
        return_source_documents=True
    )

    result = qa_chain.invoke({"query": question})
    print(f"\n❓ 问题:{question}")
    print(f"🤖 回答:{result['result']}")
    print(f"\n📎 引用来源:")
    for doc in result["source_documents"][:2]:
        print(f"   - {doc.metadata.get('source', '未知')}")


def main():
    print("=" * 50)
    print("📚 本地文档问答机器人")
    print("=" * 50)

    # 加载文档
    docs = load_documents(DOCS_DIR)

    if not docs:
        print("没有可处理的文档,请先将文件放入 documents/ 目录")
        sys.exit(1)

    # 构建向量库
    vectorstore = build_vector_store(docs)

    # 交互式问答
    print("\n开始提问(输入 'quit' 退出):")
    while True:
        try:
            question = input("\n❓ ").strip()
        except (EOFError, KeyboardInterrupt):
            print("\n再见!")
            break

        if not question:
            continue
        if question.lower() in ("quit", "exit", "退出"):
            print("再见!")
            break

        ask_question(vectorstore, question)


if __name__ == "__main__":
    main()

第四步:准备文档并运行

创建一个测试用的 Markdown 文件:

mkdir -p documents
cat > documents/sample.md << 'EOF'
# 产品使用手册 v2.0

## 快速开始
1. 注册账号后进入控制台
2. 在「项目设置」中创建新项目
3. 获取 API Key 并开始调用接口

## 计费说明
基础版每月免费额度 1000 次调用。超出部分按 0.01 元/次计费。

## 常见问题
Q: API 响应超时怎么办?
A: 检查网络连接,确认请求体不超过 1MB。如仍超时请提交工单。
EOF

运行脚本:

python docs_qa.py

首次运行会加载文档、建立向量索引(大概 10-20 秒)。之后再次运行会直接复用缓存,几乎秒开。

交互界面像这样:

📚 本地文档问答机器人
==================================================
📄 加载 PDF:xxx.pdf
📝 加载文本:sample.md
✅ 共加载 2 个文档块
🔪 切分为 15 个片段
💾 向量索引已保存至 ./chroma_db

开始提问(输入 'quit' 退出):

❓ API 超时时应该怎么办?
🤖 回答:API 响应超时时,建议先检查网络连接,确认请求体不超过 1MB。如果问题仍然存在,可以提交工单寻求帮助。

📎 引用来源:
   - documents/sample.md

第五步:批量处理真实文档

如果你有多个 PDF 需要处理,把文件放进 documents/ 目录即可。脚本会自动扫描所有支持的格式。

想要更精细的控制,可以在脚本里加一个配置项:

# 在 load_documents 函数中添加过滤逻辑
EXCLUDE_PATTERNS = ["草稿", "废弃", "临时"]
for file_path in doc_path.iterdir():
    if any(pattern in file_path.name for pattern in EXCLUDE_PATTERNS):
        print(f"⏭️  跳过:{file_path.name}")
        continue
    # ... 正常加载

运行效果

跑起来之后,你可以:

  • 把产品手册丢进去,客服团队直接问 AI 拿答案
  • 导入技术文档,开发查接口参数不再翻 Wiki
  • 把合同和法规放进去,法务快速定位条款
  • 配合 cron 定时任务,每天自动更新文档索引

整个流程从安装到能用,大概 15 分钟。硬件要求不高——8GB 内存的笔记本就能跑 7B 模型。

优化方向

这个项目是个可用的原型,接下来可以往这些方向打磨:

  1. 流式输出:加上 stream=True 参数,回答不再是干等几秒后一次性吐出,而是逐字显示,体验更像聊天
  2. 多轮对话:引入 ConversationBufferMemory,支持上下文追问,不用每次都重复背景
  3. 混合搜索:ChromaDB 默认是向量相似度搜索,加上关键词 BM25 混合检索,召回率更高
  4. Web 界面:用 Streamlit 或 Gradio 套一层 UI,同事也能直接用浏览器访问
  5. 模型升级:硬件够的话换成 qwen2.5:14b 或 llama3.1:8b,回答质量会有明显提升
  6. 增量更新:记录文件修改时间,只重新索引变更过的文档,节省计算资源

文档放在本地,问答跑在本地,AI 能力也跑在本地。这就是完全属于自己的私人知识库。

要不要试试把你手头那份最厚的 PDF 喂给它?

这篇文章对你有帮助吗?