Kamu muter-muter jawab pertanyaan yang sama dari dokumen sendiri, terus nyatet jawabannya manual. Assistant SaaS sih ngilangin repetisi itu, tapi dokumen privat kamu ikut dikirim ke pihak ketiga dan ditagih per token. Stack RAG lokal ngindarin dua-duanya: Ollama jalanin model di mesin kamu, Chroma nyimpen vektor, dan semuanya offline setelah modelnya ke-download.
RAG itu kepanjangan retrieval-augmented generation. Kamu index banyak dokumen, cari yang paling deket dari pertanyaan, lalu kasih itu ke model sebagai konteks sebelum model nulis jawaban. Model gak perlu seluruh arsip masuk ke jendela konteks, dan dia bisa nyebut teks persis yang dia pake.
Prerequisites
- Python 3.10+
- Ollama keinstall (macOS/Linux:
curl -fsSL https://ollama.com/install.sh | sh, atau download dari ollama.com/download) - Sekitar 4 GB disk buat dua model
- Terminal dan pip
Langkah 1: Pull dua model
Kamu butuh dua: satu buat chat, satu buat embedding.
ollama pull llama3.2
ollama pull nomic-embed-text
llama3.2 itu model chat 3B parameter, kira-kira 2 GB, lancar di 8 GB RAM. nomic-embed-text model 274 MB yang ngubah teks jadi vektor; dia gak bisa chat, cuma embed. Pastiin model chat jalan:
ollama run llama3.2 "Reply with: ready"
Langkah 2: Cek output embedding
Endpoint embedding balikin vektor buat teks apa aja:
curl -s http://localhost:11434/api/embeddings -d '{"model":"nomic-embed-text","prompt":"hello world"}'
Kalau Ollama jalan, kamu dapet objek JSON berisi array embedding berisi angka floating. Array itu yang di-index Chroma.
Langkah 3: Siapin vector store
pip install ollama chromadb
Chroma punya client in-memory yang ngehapus data pas script kelar, sama persistent client yang nulis ke folder. Pake yang persistent biar index kamu awet.
import chromadb
client = chromadb.PersistentClient(path="./chroma")
col = client.get_or_create_collection(name="notes")
Langkah 4: Script RAG lengkap
Simpen ini sebagai rag.py:
import ollama
import chromadb
EMBED_MODEL = "nomic-embed-text"
CHAT_MODEL = "llama3.2"
COLLECTION = "notes"
client = chromadb.PersistentClient(path="./chroma")
col = client.get_or_create_collection(name=COLLECTION)
def embed(text: str) -> list[float]:
return ollama.embeddings(model=EMBED_MODEL, prompt=text)["embedding"]
def index(documents: list[str]) -> None:
ids = [f"doc-{i}" for i in range(len(documents))]
col.upsert(
ids=ids,
documents=documents,
embeddings=[embed(d) for d in documents],
)
def retrieve(query: str, k: int = 4) -> list[str]:
res = col.query(query_embeddings=[embed(query)], n_results=k)
return res["documents"][0]
def ask(question: str) -> str:
context = "\n---\n".join(retrieve(question))
prompt = (
"Answer the question using only the context below.\n\n"
f"Context:\n{context}\n\n"
f"Question: {question}\n\nAnswer:"
)
return ollama.chat(model=CHAT_MODEL, messages=[
{"role": "user", "content": prompt}
])["message"]["content"]
if __name__ == "__main__":
index([
"Deploys run on Fridays at 18:00 via a GitHub Actions workflow named build-deploy.",
"Rollback is automatic: if the health check fails for 30 seconds, the pipeline redeploys the previous image.",
"Secrets live in the project's CI variables, never in the repository.",
])
print(ask("When does the deploy run, and what happens if it fails?"))
Ganti tiga dokumen contoh itu dengan konten kamu sendiri, terus jalanin:
python rag.py
Script-nya nge-index chunk, ambil empat yang paling deket dari pertanyaan, dan nyuruh llama3.2 jawab pakai konteks itu doang.
Cara kerja retrieval
Ada tiga bagian:
- Indexing.
embed()ngubah tiap chunk jadi vektor, terusindex()nyimpennya dengan id di Chroma. - Retrieval.
retrieve()ngembed pertanyaan dengan model yang sama terus minta Chroma kasih k vektor terdeket. Chroma balikin chunk yang diurut berdasarkan jarak. - Generation.
ask()ngepack chunk hasil retrieval ke dalam prompt yang nyuruh model pakai konteks itu doang, terus balikin jawabannya.
Jaga embedding dan query di model yang sama. Nyampur nomic-embed-text pas index dengan model lain pas query bakal diam-diam ngerusak retrieval.
Kapan pindah ke store lain
Persistent client Chroma cukup sampe ratusan ribu chunk di satu mesin. Kalau butuh akses multi-tenant, auth, atau kamu udah pake Postgres, liat:
- Chroma mode client-server buat akses bersama
pgvector, ekstensi vektor buat Postgres, pas kamu mau satu database buat baris dan vektor- Qdrant kalau butuh throughput query yang lebih tinggi atau deployment terdistribusi
Loop RAG-nya sama aja; cuma panggilan query yang beda.
Gotcha
nomic-embed-texthandle sekitar 2048 token konteks. Potong dokumen panjang di bawah itu, kalau tidak kualitas retrieval turun.- Kalau jawaban kedengeran kosong, chunk kamu kemungkinan kepanjangan atau terlalu dikit yang kedapet. Naikin
kdan pendekin chunk sebelum nyalahin model. - Client Chroma in-memory buang semua data pas keluar. Pake
PersistentClientkecuali kamu cuma mau demo.
Langkah selanjutnya
- Bungkus
ask()dalam endpoint Flask atau FastAPI dan kasih web UI. - Index PDF dan file Markdown langsung dengan motong jadi chunk dulu.
- Tambah reranker, atau jawab dari top-k chunk plus query mentah, biar sitasi lebih presisi.