← Kembali ke Blog

Bikin Chatbot RAG Lokal dengan Ollama dan Chroma

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:

  1. Indexing. embed() ngubah tiap chunk jadi vektor, terus index() nyimpennya dengan id di Chroma.
  2. Retrieval. retrieve() ngembed pertanyaan dengan model yang sama terus minta Chroma kasih k vektor terdeket. Chroma balikin chunk yang diurut berdasarkan jarak.
  3. 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-text handle 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 k dan pendekin chunk sebelum nyalahin model.
  • Client Chroma in-memory buang semua data pas keluar. Pake PersistentClient kecuali 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.

Referensi

Butuh Bantuan Implementasi?

Saya membantu tim mendesain dan membangun infrastruktur cloud scalable, pipeline DevOps, dan sistem production-grade.

Konsultasi Gratis