自托管文档聊天:构建私有PDF问答助手

放入你的PDF和文档,提问,获得带引用的答案——全部运行在你自己的机器上,不发送给任何人。

Aquila 团队 更新于 2026年6月19日

一个自托管的“文档聊天”助手,让你放入PDF、Word文件和Markdown,然后用自然语言提问,获得带引用的答案——完全在你的基础设施上运行,没有任何信息发送给第三方。底层是一个RAG管道(摄入→嵌入→存储→检索→回答),连接到本地LLM和简单的聊天UI。本指南用真实命令和具体技术栈端到端构建一个,并解释这个文档问答层与通用RAG服务器的区别。

这是构建在自托管RAG完全指南在VPS上构建私有RAG教程之上的应用层。那些指南涵盖引擎,而本指南涵盖产品:一个你的团队真正会打开使用的私人助手。

▶ 自己跑一遍 —— aquila-starter 是这篇指南的一键自托管版本:Ollama + Qdrant + FastAPI,docker compose up 即起。Fork 下来据为己有。

为什么要自托管文档聊天

“与PDF聊天”这类工具有众多SaaS产品,它们都有一个共同问题:你把文档上传到别人的服务器上。对于营销一页纸,没问题。但对于合同、患者记录、董事会材料、未发布源代码、财务文件或任何涉及保密协议的内容,上传到第三方的“文档聊天”服务正是你的安全团队担心的数据泄露。

自托管从架构上消除了这种风险。你的文档被本地嵌入、本地存储,并由本地运行的模型回答。隐私不是供应商页面上的政策承诺,而是字节物理存放位置的属性。这正是本网站所基于的私有/自托管利器:拥有你的索引,而非租用它。

诚实的权衡:SaaS工具是五分钟注册,而自托管需要一个下午的配置加上机器所有权。如果你的文档不敏感且只想要答案,就用SaaS工具。如果敏感,请继续阅读。

文档聊天与VPS支柱的区别

VPS支柱构建的是通用RAG服务——一个你可以从自己应用程序调用的API。本指南在此基础上构建一个文档问答应用,区别体现在三个方面:

  • 文档摄取是难点,而非事后考虑。 通用RAG演示加载的是干净的文本。真正的文档助手必须从混乱的PDF中提取文本——扫描页、双栏布局、表格、页眉页脚——这正是大多数“对我的文件无效”的痛点所在。文档解析在此处比管道的任何其他部分都更重要。
  • 它是对话式的,而非一次性问答。 用户会追问。“那第4条呢?”只有在“总结终止条款”之后才有意义。这意味着你需要携带聊天历史,并在检索前将追问重写为独立查询。
  • 它自带UI。 通用RAG API没有前端。文档助手是非开发者在浏览器中打开,拖入文件,然后聊天的工具。UI是产品的一部分,而非可选的点缀。

牢记这三点,下面的构建就会很直接。

技术栈

这里所有东西都是开源的,并且运行在单台机器上。一台8–16 GB RAM的工作站或VPS就足够了;GPU可以让本地生成变得流畅,但对适度使用不是必需的。

选择原因
LLM运行时Ollama一条命令即可运行本地聊天和嵌入模型。
嵌入模型nomic-embed-text小型、快速,在CPU上运行;768维。参见最佳本地嵌入模型
聊天模型llama3.1:8b(或更大)有能力的本地作答器;如果VRAM足够可换更大的。
文档解析PyMuPDF / Unstructured / Docling从PDF、DOCX等中稳健提取文本。
向量存储Chroma(持久化)零配置本地存储;后续可升级到Qdrant或pgvector
编排LlamaIndex将分块、检索和聊天结合起来。
UIOpen WebUI,或小型Streamlit/Gradio应用带文件上传的浏览器聊天前端。

如果你不想自己组装,成熟的可自托管应用已经打包好了所有功能:AnythingLLM、带有文档功能的Open WebUI,以及Khoj(一个AGPL许可的“AI第二大脑”,能从你的PDF、Markdown和Word文件中回答,并运行在pgvector上,截至2026年6月)都提供了开箱即用的文档聊天。当你需要控制分块和检索时,从零构建;当你只想今天就能使用时,安装打包好的应用。本指南的其余部分从零构建,以便你理解每一层。

步骤1 —— 本地运行模型

安装Ollama,然后拉取嵌入模型和聊天模型:

# install Ollama (Linux); see ollama.com for macOS/Windows
curl -fsSL https://ollama.com/install.sh | sh

ollama pull nomic-embed-text   # embeddings
ollama pull llama3.1:8b        # the answerer

确认它在localhost:11434上运行:

ollama list

两个模型现在完全在你的机器上运行。无需API密钥,无出站流量。

步骤2 —— 摄入并解析你的文档

将文件放入文件夹并提取干净的文本。这一步决定文档助手的成败,因此使用真正的解析器而非简单的文本提取。以下是最小化的Python摄入,使用LlamaIndex(底层可调用PyMuPDF/Unstructured):

pip install llama-index llama-index-embeddings-ollama \
            llama-index-vector-stores-chroma chromadb pymupdf
from llama_index.core import SimpleDirectoryReader

# Reads PDFs, DOCX, Markdown, TXT, HTML from the folder
docs = SimpleDirectoryReader("./documents").load_data()
print(f"Loaded {len(docs)} document(s)")

对于扫描版PDF(图像而非文本),你需要OCR——在此之前用布局感知提取器如Unstructured或Docling处理,否则无论怎么分块都无法恢复从未提取出的文本。

步骤3 —— 分块、嵌入并存储

将文档拆分,用本地模型嵌入每个分块,并持久化到Chroma。如何分块对答案质量影响很大——这是最大的质量杠杆,在RAG分块策略中有深入讨论。对于混合文档,合理的默认值是约512 token的分块,带轻微重叠。

from llama_index.core import VectorStoreIndex, Settings
from llama_index.core.node_parser import SentenceSplitter
from llama_index.embeddings.ollama import OllamaEmbedding
from llama_index.vector_stores.chroma import ChromaVectorStore
import chromadb

Settings.embed_model = OllamaEmbedding(model_name="nomic-embed-text")
Settings.node_parser = SentenceSplitter(chunk_size=512, chunk_overlap=64)

client = chromadb.PersistentClient(path="./chroma_db")
vector_store = ChromaVectorStore(
    chroma_collection=client.get_or_create_collection("documents")
)

index = VectorStoreIndex.from_documents(docs, vector_store=vector_store)

你的文档现在已嵌入并存储在磁盘上的./chroma_db中。原始文本和向量从未离开过机器。

步骤4 —— 检索并回答(带聊天历史)

对于文档聊天,使用聊天引擎而非一次性查询引擎,这样才能支持追问。LlamaIndex的CondenseQuestionChatEngine在检索前将追问加上对话历史重写为独立查询:

from llama_index.llms.ollama import Ollama

Settings.llm = Ollama(model="llama3.1:8b", request_timeout=120.0)

chat_engine = index.as_chat_engine(
    chat_mode="condense_question",
    similarity_top_k=5,
)

resp = chat_engine.chat("What are the termination terms in this contract?")
print(resp)                 # grounded answer
print(resp.source_nodes)    # the chunks it cited

resp = chat_engine.chat("And the notice period for that?")  # follow-up works
print(resp)

两件事让答案值得信赖:仅从检索到的上下文回答,否则说“我不知道” 的提示指令,以及返回引用source_nodes),以便用户对照源文档验证。永远不要发布一个回答不显示来源的文档助手。

步骤5 —— 添加UI

浏览器前端将脚本变成团队会用的工具。两种快速方式:

打包UI(最快)。 用Docker运行Open WebUI并指向本地Ollama;它的文档功能提供拖拽上传和聊天,无需代码:

docker run -d --name open-webui -p 3000:8080 \
  --add-host=host.docker.internal:host-gateway \
  -v open-webui:/app/backend/data \
  ghcr.io/open-webui/open-webui:main

打开http://localhost:3000,连接到Ollama,然后上传文档。

自定义UI(更多控制)。 将你自己的管道封装在小型Streamlit或Gradio应用中——一个文件上传器、一个聊天框、一个显示引用源分块的面板。几十行代码,你能精准控制分块和检索。当检索质量比搭建速度更重要时选择自定义。

为实际使用加固

一个可工作的演示不是部署好的工具。在同事依赖它之前:

  • 重新索引。 文档会变更和新增。需要有一个任务来重新嵌入新文件或编辑过的文件,并删除过时的向量,否则你的助手会慢慢从过期的文本中回答。
  • 访问控制。 如果多人使用,放在认证后面(反向代理加认证,或UI的内置用户),防止错误的人查询敏感文档。
  • 评估。 构建一个小型的问题/预期来源对集合,在每次改动前后测量检索效果——完整方法见如何评估RAG。“它回答了我的测试问题”不等于“它有效”。
  • 混合搜索。 纯语义搜索会遗漏精确字符串——条款编号、案号、错误码。添加关键词/BM25匹配可以找回它们;原因见什么是语义搜索
  • 备份。 你的向量存储和源文档现在是一项知识资产。像对待资产一样备份它们。

常见问题解答

“文档聊天”和RAG是一回事吗? 是的——它是一个RAG管道,外面包装了文档上传UI和对话式聊天。检索增强生成引擎是一样的;文档聊天只是在上面增加了健壮的文件解析、聊天历史和一个前端。

我可以完全离线与PDF聊天吗? 可以。用Ollama运行本地嵌入模型和本地聊天模型,加上本地向量存储,整个管道在初始模型下载后无需互联网即可在你机器上运行。你的文档不会离开机器。

扫描版PDF和图片怎么办? 扫描版PDF包含的是图像而非文本,所以需要先做OCR。在分块之前,使用布局感知的提取器如Unstructured或Docling(它们可以OCR)。如果文本从未被提取,检索就无从下手。

我需要GPU吗? 不需要。嵌入和向量存储在CPU上运行良好,8B聊天模型在CPU上也能回答——只是较慢(每个回答几秒钟)。GPU主要让本地回答生成变快。对于少量用户,CPU可以接受。

直接安装打包好的应用是否更快? 通常是的。AnythingLLM、Open WebUI的文档功能以及Khoj都提供最小化设置的自托管文档聊天。当你需要控制分块、检索和评估时,从零构建(如上文);当你想今天就用上时,安装打包好的应用。


Aquila是私有、自托管AI搜索的独立指南——建立在这样的信念之上:你应该拥有你的索引,而不是租用它。你自己托管的文档助手意味着你的合同、演示文稿和笔记可以在不传给任何人的情况下被回答。浏览更多指南或订阅新闻通讯,获取诚实、供应商中立的内容。掌控你自己的搜索。

继续学习

更多关于自托管 AI 搜索、RAG 和向量数据库的指南。