用一个简短的 Python 脚本,自己动手实现“与文档对话”:读取 PDF,把内容切分成段落,用 Ollama 中的本地模型生成向量(embedding),存入本地向量数据库,然后带引用地回答问题。所有数据都不会离开你的电脑。
各页面的分工。 想不写代码就用起来?请看操作指南 Chat with your PDFs privately(AnythingLLM、Open WebUI 或 LM Studio;英文页面)。想先了解什么是 RAG?请阅读 RAG 入门。本页是“自己动手”的版本,适合想看清并掌控每一步的读者。
你将构建什么
- 用
pypdf提取每一页的文本。 - 把每一页切分成相互重叠的段落。
- 用 Ollama 中的向量模型(
embeddinggemma)为每个段落生成向量。 - 把段落、向量以及文件名和页码存入 Chroma——一个保存在你本地磁盘文件夹中的向量数据库。
- 检索与问题最接近的段落,再用本地聊天模型回答,并要求它注明来源页。
1. 安装
你需要较新版本的 Python 3 和 Ollama。
pip install ollama chromadb pypdf
ollama pull embeddinggemma然后下载一个用于回答问题的聊天模型。模型查找器 会为你的电脑挑选合适的模型;你在 Ollama 中已有的任何聊天模型也都可以。
2. 脚本
将以下内容保存为 rag.py:
"""Local RAG: index a folder of PDFs with Ollama embeddings into Chroma, then ask questions."""
import argparse
import pathlib
import chromadb
import ollama
from pypdf import PdfReader
EMBED_MODEL = "embeddinggemma" # an Ollama embedding model; change it and you must re-index
DB_PATH = "rag-db" # where Chroma keeps the index on disk
CHUNK_CHARS = 1200 # passage length, in characters
OVERLAP_CHARS = 200 # how much consecutive passages share
def chunks(text: str):
step = CHUNK_CHARS - OVERLAP_CHARS
for start in range(0, len(text), step):
yield text[start:start + CHUNK_CHARS]
def embed(texts: list[str]) -> list[list[float]]:
return ollama.embed(model=EMBED_MODEL, input=texts)["embeddings"]
def collection():
return chromadb.PersistentClient(path=DB_PATH).get_or_create_collection("docs")
def index(folder: str) -> None:
col = collection()
for pdf in sorted(pathlib.Path(folder).glob("*.pdf")):
for page_no, page in enumerate(PdfReader(pdf).pages, start=1):
text = (page.extract_text() or "").strip()
if not text:
print(f" skipped {pdf.name} page {page_no}: no text layer (a scan needs OCR first)")
continue
parts = list(chunks(text))
col.upsert(
ids=[f"{pdf.name}:{page_no}:{i}" for i in range(len(parts))],
documents=parts,
embeddings=embed(parts),
metadatas=[{"file": pdf.name, "page": page_no}] * len(parts),
)
print(f"indexed {pdf.name}")
def ask(question: str, model: str, k: int) -> None:
hits = collection().query(query_embeddings=embed([question]), n_results=k)
context = "\n\n".join(
f"[{meta['file']} p.{meta['page']}]\n{doc}"
for doc, meta in zip(hits["documents"][0], hits["metadatas"][0])
)
reply = ollama.chat(model=model, messages=[
{"role": "system", "content": "Answer only from the context. After each claim, cite the [file p.N] tag it came from. "
"If the context does not contain the answer, say you cannot find it."},
{"role": "user", "content": f"Context:\n{context}\n\nQuestion: {question}"},
])
print(reply["message"]["content"])
if __name__ == "__main__":
p = argparse.ArgumentParser()
sub = p.add_subparsers(dest="cmd", required=True)
sub.add_parser("index").add_argument("folder")
a = sub.add_parser("ask")
a.add_argument("question")
a.add_argument("--model", required=True, help="a chat model you have pulled in Ollama")
a.add_argument("-k", type=int, default=4, help="how many passages to retrieve")
args = p.parse_args()
if args.cmd == "index":
index(args.folder)
else:
ask(args.question, args.model, args.k)各部分的作用:
chunks()把一页切成长度为CHUNK_CHARS个字符的段落,相邻段落共享OVERLAP_CHARS个字符,这样在边界处被切断的句子仍会完整地出现在某一个段落中。embed()调用 Ollama 的向量 API。文档和问题必须使用同一个向量模型。index()保存每个段落及其文件名和页码,并使用upsert,因此对同一文件夹再次运行时会更新索引,而不会重复写入。ask()检索最接近的k个段落,并要求聊天模型只根据这些段落作答,同时注明[文件 p.N]。
3. 运行
把 PDF 放进一个文件夹,然后运行:
python rag.py index ./my-pdfs
python rag.py ask "What is the notice period for ending the lease?" --model <your-chat-model>好的回答会引用正确的段落,并以 [lease.pdf p.1] 这样的标记结尾。打开那一页核对一下。你也可以直接用中文提问。
4. 进一步改进
- 扫描版 PDF 没有文本层。
pypdf无法读取图片中的文字;脚本会跳过这些页面并给出提示。请先做 OCR(例如 Tesseract),再为结果建立索引。 - 段落长度。 短段落更容易精确命中事实,长段落能保留更多上下文。修改
CHUNK_CHARS和OVERLAP_CHARS,删除rag-db文件夹,然后重新建立索引。 - 检索的段落数量。 如果答案遗漏了分散在多页中的信息,请调大
-k。段落越多,聊天模型需要的上下文窗口就越长。 - 更换向量模型就要重建索引。 不同模型生成的向量不能混用:删除
rag-db,再运行一次index。 - 其他语言。
embeddinggemma使用多种语言的文本训练,因此中文文档无需任何修改即可使用。 - 总结不等于检索。 “总结整篇文档”需要整篇文档,而不是四个段落。请逐页总结。
延伸阅读
- Chat with your PDFs privately:无需代码的操作指南(英文)。
- RAG 入门:用通俗的话解释什么是 RAG。
- 让本地大模型稳定输出 JSON:当回答需要交给另一个程序处理时。
- Open WebUI + Ollama 设置:内置文档检索的浏览器界面。
Sources, checked 2026-10-15: Ollama download · Ollama embeddings: ollama.embed, the embeddings field · embeddinggemma, multilingual embedding model · Ollama Python library: pip install ollama, chat · Chroma: own embeddings alongside documents · Chroma: query_embeddings, results grouped per query · Chroma: PersistentClient · pypdf: extract_text, no text from images.