进阶 22 分钟RAG

使用 ChromaDB 和 Ollama 实现本地 RAG:教程 Python

使用 ChromaDB、Ollama 和 Python 在本地实现 RAG,需要三个相互配合的组件:一个将数据持久化到磁盘的向量存储(ChromaDB),一个将文本块转换为向量的嵌入模型(通过 Ollama 运行的 nomic-embed-text),以及一个根据检索到的段落作答的聊天大语言模型。无需 API 密钥,数据不会外泄。本指南将带您在 22 分钟内,从一份原始 PDF 构建出能够引用来源的聊天机器人。

作者: Mohamed Meguedmi·更新于 2026-08-27·已在 Windows、macOS 和 Linux 上测试

#为什么为本地 RAG 选择这套技术栈

许多RAG教程都从LangChain或LlamaIndex开始。这些框架功能强大,但掩盖了底层细节。在这里,我们仅使用三个依赖项手动编写管道。您将理解每一步,并能明确后续需要优化的内容。

ChromaDB
开源向量存储,纯Python实现,内置持久化模式(SQLite + HNSW索引)。无需启动服务器。
Ollama
同时为嵌入模型(nomic-embed-text)和聊天 LLM(Qwen 3.5、Granite 4.2、Gemma 4)提供服务。统一的 HTTP 端点位于 localhost:11434。
纯 Python
只需几个函数,无需框架。如果有需要,之后可以接入 LangChain,但起步时并不需要它。
i
您将获得的内容
一个约150行的 Python 脚本,可读取一个文件夹中的 PDF,将其分块并在 ChromaDB 中建立索引,然后用法语回答问题并附上引用来源。全程本地运行,没有任何出站请求。

#先决条件

本地副驾驶套件

本指南带你上手模型。工具包则帮你用上能在你的编辑器中编写代码的编程助手。

  • 在线空间,终身可用
  • PDF + 文件
  • 30 天内退款
Python 3.10+
ChromaDB 需要 3.10 或更高版本。请使用 python --version 检查版本。
Ollama 已安装并启动
守护进程默认监听 http://localhost:11434。如果您从零开始,请先按照 Ollama 安装指南操作。
8 GB 内存
16 GB 内存较为充裕。9B 聊天模型采用 Q4 量化时占用约 6 GB,嵌入模型占用约 300 MB。
无需GPU
CPU推理也能正常运行,只是速度较慢。在导入大型语料库时,配备至少6 GB显存的GPU能大幅加快嵌入向量的生成。

#1. 安装ChromaDB并准备 Ollama

创建一个干净的虚拟环境,安装所需的三个库,并在 Ollama 上下载模型。

Python 环境
python -m venv .venv
source .venv/bin/activate  # sous Windows : .venv\Scripts\activate
pip install chromadb ollama pypdf

三个软件包:chromadb 用于向量存储,ollama 是官方 Python 客户端,pypdf 用于读取 PDF。仅此而已。

Ollama 模型
ollama pull nomic-embed-text
ollama pull qwen3.5:9b

nomic-embed-text 是一个拥有 1.37 亿参数的多语言嵌入模型,可生成 768 维向量。它轻量、快速,法语表现良好。Qwen 3.5 9B(6.6 GB,256k 上下文,多语言,Apache 2.0)用于最终的聊天环节:它是 2026 年面向 8 GB 显存配置的默认选择。您可以将它替换为 granite4.2:8b(更节省资源)或 gemma4:12b,无需修改任何代码。

→
检查 Ollama 是否响应
只需执行 curl http://localhost:11434/api/tags,就应该能列出您的模型。如果没有输出,说明守护进程尚未启动:在另一个终端中运行 ollama serve。

#2. 配置嵌入模型

嵌入向量是表示文本语义的向量。语义相近的两个文本具有相近的向量。这是 RAG 的核心:我们寻找与问题嵌入最相似的 chunk。

embed.py — 快速测试
import ollama

resp = ollama.embeddings(
    model="nomic-embed-text",
    prompt="Le contrat est résilié de plein droit en cas de manquement grave."
)

vec = resp["embedding"]
print(f"Dimension du vecteur : {len(vec)}")
print(f"5 premières valeurs : {vec[:5]}")

您应该会看到输出「Dimension du vecteur : 768」。如果程序报错并显示 model not found,说明尚未执行 ollama pull nomic-embed-text。

i
为何选择nomic-embed-text
在法国基准测试(MTEB-fr)中,nomic-embed-text 位列参数量少于 2 亿模型的 top 5。对于纯法语任务,mxbai-embed-large 通常表现更好,但体积达 6.7 亿。nomic 是入门时质量与速度的绝佳平衡选择。

#3. 导入法语PDF

文档加载执行三项操作:读取PDF页面内容,将文本分割为合理大小的块,并将每个文本块及其嵌入向量持久化存储至ChromaDB。

ingest.py
import os
import chromadb
import ollama
from pypdf import PdfReader

client = chromadb.PersistentClient(path="./chroma_db")
collection = client.get_or_create_collection(name="docs")

def chunk_text(text, size=800, overlap=100):
    chunks = []
    start = 0
    while start < len(text):
        end = min(start + size, len(text))
        chunks.append(text[start:end])
        start += size - overlap
    return chunks

def ingest_pdf(path):
    reader = PdfReader(path)
    name = os.path.basename(path)
    for page_num, page in enumerate(reader.pages):
        text = page.extract_text() or ""
        for i, chunk in enumerate(chunk_text(text)):
            emb = ollama.embeddings(
                model="nomic-embed-text",
                prompt=chunk
            )["embedding"]
            collection.add(
                ids=[f"{name}-p{page_num}-c{i}"],
                embeddings=[emb],
                documents=[chunk],
                metadatas=[{"source": name, "page": page_num + 1}],
            )
    print(f"OK : {name} ingéré ({len(reader.pages)} pages)")

if __name__ == "__main__":
    for f in os.listdir("./pdfs"):
        if f.endswith(".pdf"):
            ingest_pdf(f"./pdfs/{f}")

分块器将内容切分为每块 800 个字符,相邻块重叠 100 个字符。这是一个起点:块既不会太小(导致上下文不足),也不会太大(导致信号被稀释)。对于内容非常密集的法律文本,可减至 500 个字符;对于排版疏朗的技术手册,可增至 1200 个字符。

→
ChromaDB 的持久化模式
PersistentClient(path="./chroma_db") 会创建一个在重启后仍存在的目录。SQLite 存储元数据,HNSW 索引存储向量。无需启动服务器,无需 Docker。如需后续切换为客户端/服务器模式,只需将该部分替换为 HttpClient。

启动数据导入,导入包含您文档的 ./pdfs/ 文件夹中的内容:

启动数据导入
mkdir -p pdfs
# placez vos PDF dans ./pdfs/
python ingest.py
!
扫描的PDF = 无文本
pypdf 只能提取 PDF 中原有的文本。如果您的 PDF 是图像扫描件,extract_text() 将返回空内容。这时需要先进行 OCR 处理(使用 Tesseract,或通过 Ollama 使用 Qwen 3.5 9B 这样的多模态视觉模型),然后再导入。

#4. 在 ChromaDB 中执行 top-k 检索

文本块建立索引后,检索过程就是先将问题转换为嵌入向量,再向 Chroma 查询按余弦距离衡量最接近的 k 个向量。即使有 100,000 个文本块,这一过程也能瞬间完成。

search.py
import chromadb
import ollama

client = chromadb.PersistentClient(path="./chroma_db")
collection = client.get_collection(name="docs")

def search(question, k=4):
    q_emb = ollama.embeddings(
        model="nomic-embed-text",
        prompt=question
    )["embedding"]
    results = collection.query(
        query_embeddings=[q_emb],
        n_results=k,
    )
    chunks = results["documents"][0]
    metas = results["metadatas"][0]
    return list(zip(chunks, metas))

if __name__ == "__main__":
    hits = search("Quelles sont les conditions de résiliation ?")
    for chunk, meta in hits:
        print(f"[{meta['source']} p.{meta['page']}]")
        print(chunk[:200], "...\n")

k=4 是一个不错的默认值。太小会漏掉相关上下文;太大则会让 LLM 被噪声淹没,并超出上下文窗口的容量。对于非常具体的问题,k=2 就足够了。对于涉及多个方面的问题,将 k 提高到 6。

#5. 带引用的聊天循环

现在把各部分串起来:检索相关文本块,构建包含上下文的提示,通过 Ollama 将其发送给 Qwen 3.5,并要求模型注明信息来源。

chat.py
import ollama
from search import search

SYSTEM = """Tu es un assistant qui répond uniquement à partir du CONTEXTE fourni.
Si la réponse n'est pas dans le contexte, dis-le clairement.
Cite tes sources entre crochets sous la forme [source.pdf p.X]."""

def ask(question):
    hits = search(question, k=4)
    context = "\n\n".join(
        f"[{m['source']} p.{m['page']}]\n{c}" for c, m in hits
    )
    prompt = f"CONTEXTE :\n{context}\n\nQUESTION : {question}"
    resp = ollama.chat(
        model="qwen3.5:9b",
        messages=[
            {"role": "system", "content": SYSTEM},
            {"role": "user", "content": prompt},
        ],
        options={"temperature": 0.2, "num_ctx": 8192},
    )
    return resp["message"]["content"]

if __name__ == "__main__":
    while True:
        q = input("\nQuestion (vide pour quitter) > ").strip()
        if not q:
            break
        print("\n" + ask(q))

三个关键点。首先,temperature=0.2:我们希望得到事实性回答,而非创造性内容。其次,num_ctx=8192:当注入4个800字符的块时,Ollama(默认2048)的上下文窗口过短。第三,系统提示强制模型回答‘我不知道’,而非进行幻觉生成——这是RAG中最重要的反幻觉防护措施。

→
流式传输以提升用户体验
将 ollama.chat 替换为 ollama.chat(..., stream=True),并遍历响应以实时显示 token。一旦将此代码集成到真实界面(如 FastAPI + WebSocket 或 Streamlit)中,这一点至关重要。

#6. 具体案例:基于合同的法律聊天机器人

假设一家事务所想查询 200 份 PDF 格式的服务合同。使用上述技术栈,不到一小时就能搭建一个助手,回答以下这类问题:

典型问题
“哪些合同包含合同终止后期限超过 12 个月的竞业限制条款?”
发生的情况
问题的嵌入向量会检索出包含语义相近关键词(竞业限制、终止后、期限)的文本块。Qwen 3.5 阅读这 4 段内容,并在回答中给出相关文件的名称。
隐私保障
任何数据都不会离开这台电脑。不需要 API 密钥。没有任何遥测。这正是本地 RAG 与 OpenAI 封装工具的区别。
!
需了解的局限性
基础 RAG 能很好地回答针对性问题(如「X 条款是什么」),但不擅长回答汇总类问题(如「有多少份合同包含 X」)。对于后者,要么需要一个分多步查询数据库的智能体,要么需要 GraphRAG。这是另一个话题。

#故障排除

ChromaDB 数据导入速度慢
瓶颈几乎总是向 Ollama 发起的嵌入计算调用。请使用 ollama ps 检查 nomic-embed-text 是否在 GPU 上运行。在 CPU 上,预计每秒处理约 50 个文本块;在 GPU 上,约 500 个。
« model not found »
Ollama 找不到 nomic-embed-text。请重新运行 ollama pull nomic-embed-text,并使用 ollama list 检查。
编造来源的回复
9B模型仍偶尔会产生幻觉。建议切换至mistral-small(24B,约14 GB,法语表现优异)或qwen3.8:27b,前提是您有足够VRAM。或者在ChromaDB之后添加一个reranker(cross-encoder)以过滤误匹配结果。
法语嵌入质量差
nomic-embed-text 支持多语言,但对于纯法语内容并非最佳选择。对于法律或医疗内容,请测试 Solon-embeddings-large-0.1 或 bge-m3(通过 sentence-transformers 加载,在 Ollama 之外运行)。
ChromaDB不断膨胀,没有上限
每次重新索引都会产生重复内容。在重新导入 PDF 之前,请执行 collection.delete(where={"source": name}) 以清除旧的分块。

#深入了解

您已经有了一个可正常运行的 RAG 系统。接下来可以从以下几个方面进一步完善它:

比较法语嵌入模型
我们的指南《法语嵌入模型推荐》对比了BGE、E5、Solon和nomic在法语内容上的表现。
优化分块处理
« 分块策略 » 详细说明语义分块、按 Markdown 标题或段落进行分块——通常这种方式能带来最大的精度提升。
添加重排序器
“在自己的流程中添加重排序器”:在 Chroma 之后加入交叉编码器,可将相关性提高 15%。这是顺理成章的下一步。
混合搜索
“BM25 + 向量混合检索”结合了词汇检索与语义检索,一旦涉及大量专业术语或专有名词,就必不可少。
这份指南对您有帮助吗?

有反馈、发现了错误,或想补充说明?请告诉我们,让这份指南对每个人都更有帮助。