利用 Ollama + Chroma 向量库构建本地 RAG,文档经分块嵌入后存储,查询时只召回最相关段落送入 LLM,解决每次会话重复粘贴上下文的痛点。
每次都要向 AI 解释自己的代码库,我受够了。
"这是架构,这是 README,这是我上次尝试的方法。"每次会话都是如此。
所以我搭建了一个本地 RAG(Retrieval-Augmented Generation,检索增强生成)系统,它了解我的项目、笔记和文档——永久性地。不上云、不花 API 费用、不反复重置上下文窗口。
下面详细说明它是如何工作的,以及我这几个月运行下来学到了什么。
LLM 没有记忆。每次会话你都要粘贴相同的 200 行上下文,达到 token 上限后一切重来。一次性提问还好, ongoing 项目就让人精疲力竭。
标准解决方案是 RAG:不是把所有东西都塞进 prompt,而是将文档存入向量数据库,在提问时只检索相关的片段。模型看到的不是整个代码库,而是 3-5 段精准的上下文。
结果:更快、更便宜,而且 AI 真的能回答对问题。
你的文档(markdown、代码、PDF、笔记)
→ 分块 + 向量化(Ollama nomic-embed-text)
→ 存入 Chroma(本地向量数据库)
查询
→ 向量化(同一模型)
→ 检索 Top-5 相关片段
→ 塞入 Ollama prompt(Qwen 3.5 9B)
→ 回答
零云服务、零 API 密钥。在 Mac Mini 或任何 8GB 内存的机器上都能运行。
所有本来会吃掉我上下文窗口的内容:
总计索引:约 4,800 个片段。查询时间:2 秒以内。
# Ollama(已安装?跳过)
curl -fsSL https://ollama.com/install.sh | sh
# 拉取模型
ollama pull qwen3.5:9b # LLM 用于回答
ollama pull nomic-embed-text # 向量化模型
# Python 依赖
pip install chromadb langchain ollama pypdf markdown
这就是全部技术栈。不需要 Docker(不过 Chroma 有 Docker 选项,如果你想要持久化服务器的话)。
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.document_loaders import DirectoryLoader
from langchain_community.embeddings import OllamaEmbeddings
from langchain_community.vectorstores import Chroma
# 加载你的 docs 文件夹
loader = DirectoryLoader("~/projects/", glob="**/*.md", recursive=True)
docs = loader.load()
# 分块(400 tokens,50 重叠)
splitter = RecursiveCharacterTextSplitter(chunk_size=400, chunk_overlap=50)
chunks = splitter.split_documents(docs)
print(f"Indexed {len(chunks)} chunks from {len(docs)} documents")
# 本地向量化和存储
embeddings = OllamaEmbeddings(model="nomic-embed-text")
db = Chroma.from_documents(
chunks,
embeddings,
persist_directory="./my-knowledge-base"
)
db.persist()
跑一次就完成了。你的文档现在可以按语义搜索,而不只是关键词。
import ollama
from langchain_community.embeddings import OllamaEmbeddings
from langchain_community.vectorstores import Chroma
# 加载已有数据库
embeddings = OllamaEmbeddings(model="nomic-embed-text")
db = Chroma(persist_directory="./my-knowledge-base", embedding_function=embeddings)
def ask(question: str) -> str:
# 检索 Top-5 相关片段
results = db.similarity_search(question, k=5)
context = "\n\n".join([r.page_content for r in results])
# 用上下文查询本地 LLM
response = ollama.chat(
model="qwen3.5:9b",
messages=[{
"role": "user",
"content": f"Based on this context:\n\n{context}\n\nAnswer: {question}"
}]
)
return response['message']['content']
# 示例
print(ask("How does my Garmin watch face fetch stock data?"))
print(ask("What's the API rate limit for the crypto bot?"))
print(ask("How do I deploy the Telegram bot to the VPS?"))
来自你自己的文档的真实回答。不会对你特定配置的事情产生幻觉。
一个文件变了不要重新索引全部。只更新新增的部分:
import hashlib
import json
from pathlib import Path
def get_file_hash(path):
return hashlib.md5(Path(path).read_bytes()).hexdigest()
def update_index(docs_dir, db, index_cache="./index-cache.json"):
cache = json.loads(Path(index_cache).read_text()) if Path(index_cache).exists() else {}
changed_files = []
for f in Path(docs_dir).rglob("*.md"):
h = get_file_hash(f)
if cache.get(str(f)) != h:
changed_files.append(str(f))
cache[str(f)] = h
if changed_files:
print(f"Re-indexing {len(changed_files)} changed files...")
# [load, chunk, embed, upsert only changed files]
json.dumps(cache) and Path(index_cache).write_text(json.dumps(cache))
把它作为 cron job 每小时跑一次。你的知识库自动保持最新。
"解释一下我 Garmin 项目中的后台服务内存限制" → 粘贴 200 行 → 等待 → 回答
每次新聊天会话:上下文重置,重新开始解释
ask("Garmin background service memory limit") -> "64KB sandbox, pass data via Background.exit(dictionary)" — 1.8 秒
我的 LLM 现在能回答关于我 6 个月没碰过的项目的问题。无需上下文管理,无需粘贴,直接问就行。
向量化步骤(索引)是慢的部分——跑一次,然后就很快了。
| 场景 | 设备 | 时间 |
|---|---|---|
| 首次索引(约 4,800 片段) | Mac Mini(M1/M2) | 45 分钟 |
| 首次索引(约 4,800 片段) | 有 GPU 的机器 | 8 分钟 |
| 查询 | 任意 | < 2 秒 |
Chunk 大小很重要 — 400 tokens 对 prose 和文档效果不错。对代码,试试 200 并增加重叠。
元数据是你的朋友 — 在 chunk 元数据中存储文件名和章节。当 AI 说"见部署笔记"时,你确切知道去哪找。
准确度要求高时重新排序 — 如果 Top-5 片段不够,加一个重排序步骤(Cohere 有免费 API,或者用本地 cross-encoder)。
注意你的 embed 模型 — nomic-embed-text 在 RAG 场景下比大多数更大的模型表现更好。不要用你的 chat LLM 做向量化。
混合搜索 — 将向量搜索与 BM25 关键词搜索结合,对带有特定名称/函数名的技术查询效果更好。
CPU 上索引很慢。我在 Mac Mini 上首次跑 ~4,800 个片段花了 45 分钟。有 GPU 的机器 8 分钟搞定。不是致命问题,但计划好喝杯咖啡的时间。
代码片段很棘手。RAG 对 prose 效果很好。对代码,你往往需要完整的函数上下文,而不只是 400 token 的片段。我最后同时索引了小片段(用于搜索)和完整文件(用于上下文)——双存储,但值得。
它不会取代 Google。如果你问的东西不在你的文档里,它会自信地编造答案。我加了一个简单的置信度检查:如果检索到的片段相似度分数低,系统会说"我在你的文档里没看到这个",而不是胡编。
一个随你的项目一起成长而不是每次会话重置的 personal AI。
下一阶段我要做的是:从 Git commit 自动索引(在编码时实时索引 diffs)+ 一个简单的 Web UI 用于非终端查询。
目前这套设置的总成本:$0/月。跑在我已有的同一台 Mac Mini 上。
如果你有以下情况,答案是肯定的:
那么是的。搭建需要一下午,索引需要一杯咖啡的时间,回报是永久性的。
如果你只有一个项目且记忆力不错?可能就过头了。先继续复制粘贴吧。
Sam Hartley 是一名独立开发者,在土耳其运营一个多机器 AI 家庭实验室。写的是让本地 AI 真正可用的基础设施。
如果你搭建过类似的——或者尝试过但遇到了我没提到的障碍——写在评论区吧。我一直很好奇别人如何解决"上下文重置"问题。