Sebagian besar demo RAG gagal di produksi. Demo tersebut berhasil pada dek demo berisi 10 pertanyaan dan rusak saat pengguna menanyakan sesuatu yang baru. Panduan ini membahas arsitektur dan pola yang tahan digunakan di produksi: chunking yang menghormati batas semantik, retrieval hibrida yang menangkap kueri samar maupun literal, struktur prompt yang mencegah halusinasi, serta disiplin biaya yang menjaga tagihan tetap stabil saat Anda melakukan penskalaan.
Lima keputusan yang penting
- Strategi chunking — lebih memengaruhi kualitas retrieval daripada model
- Model embedding — menentukan batas atas recall
- Strategi retrieval — vector saja tidak cukup
- Struktur prompt — menentukan apakah model berhalusinasi
- Model generasi + caching — menentukan ekonomi unit
1. Chunking — yang sederhana mengalahkan yang canggih
Jangan terlalu memikirkan ini. Gunakan recursive character splitter dengan 1000-1500 karakter per chunk dan overlap 100-200 karakter. Splitter ini mencoba memecah berdasarkan batas paragraf terlebih dahulu, kemudian kalimat, lalu kata. Stabil di berbagai domain:
# Recursive character splitter with overlap — the boring choice that wins
from langchain_text_splitters import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=1200,
chunk_overlap=200,
separators=["\n\n", "\n", ". ", " ", ""],
)
chunks = splitter.split_text(document)
# 1200 chars ≈ 300-400 tokens — fits 5+ chunks in a Sonnet context window
# with room for system prompt + question + answerHal-hal yang tampak lebih cerdas tetapi jarang membantu: model yang menyadari kalimat, splitter yang menyadari markdown, serta sliding window dengan embedding. Itu bukan pendekatan yang buruk — hanya sedikit lebih baik dengan usaha yang sangat besar. Gunakan usaha tersebut untuk retrieval.
2. Model embedding — text-embedding-3-large adalah default
Embedding tidak disediakan oleh Kunavo — panggil penyedia embedding secara langsung untuk langkah ini. /v1/embeddings adalah format wire yang telah diimplementasikan tanpa model yang diaktifkan di belakangnya, sehingga permintaan kepadanya gagal; GET /v1/models selalu menjadi otoritas mengenai apa yang dapat dipanggil. Dalam praktiknya, ini tidak menambah biaya apa pun pada pipeline RAG selain kunci kedua: panggilan embedding dan panggilan generasi memang merupakan permintaan terpisah, jadi lakukan embedding melalui OpenAI, Voyage, atau Cohere dan generasi melalui Kunavo.
text-embedding-3-large mencapai titik seimbang multibahasa + akurasi yang ideal untuk sebagian besar kasus penggunaan produksi, dengan tarif yang dipublikasikan oleh OpenAI sendiri — periksa di halaman harga mereka, bukan di sini, karena kami tidak menjualnya kembali dan seharusnya tidak menyebutkan angkanya. Lakukan re-embedding ketika Anda mengubah strategi chunking atau model itu sendiri; bukan ketika konten diperbarui (cukup tambahkan chunk baru).
Pengecualian ketika embedding yang lebih kecil berfungsi: retrieval teks pendek berbahasa Inggris murni (FAQ, kartu produk). Untuk itu, text-embedding-3-small 4x lebih murah dengan penurunan kualitas yang dapat diabaikan. Untuk konten multibahasa atau teknis, tetap gunakan model besar.
3. Retrieval — vector murni kalah dari hibrida
Kemiripan vector sangat baik untuk pencocokan semantik samar ("bagaimana cara membatalkan" → "kebijakan pembatalan langganan"). Namun, vector sangat buruk untuk istilah literal ("SKU-A92837" atau "order #4729"). Solusinya adalah hibrida: lakukan pencarian kata kunci BM25 secara paralel, lalu gunakan reciprocal rank fusion:
# Hybrid retrieval: vector similarity + BM25 keyword scoring
# Pure vector misses literal keywords (product SKUs, IDs, dates)
def retrieve(question: str, k: int = 5) -> list[dict]:
q_embed = embed([question])[0]
vector_hits = vector_db.similarity_search(q_embed, k=k * 2)
keyword_hits = bm25_search(question, k=k * 2)
# Reciprocal rank fusion — simple, robust
scores: dict[str, float] = {}
for rank, hit in enumerate(vector_hits):
scores[hit["id"]] = scores.get(hit["id"], 0) + 1 / (rank + 60)
for rank, hit in enumerate(keyword_hits):
scores[hit["id"]] = scores.get(hit["id"], 0) + 1 / (rank + 60)
top_ids = sorted(scores, key=scores.get, reverse=True)[:k]
return [chunk_by_id[i] for i in top_ids]Ini adalah peningkatan kualitas terbesar setelah memilih model embedding yang layak. Lacak recall@5 pada evaluasi 100 pertanyaan yang disisihkan — jika meningkat dari 70% menjadi 90% setelah menambahkan BM25, Anda baru saja menghilangkan sepertiga kegagalan "Saya tidak tahu".
4. Struktur prompt — tiga pola yang mencegah halusinasi
# Final RAG prompt structure — three sections, citations enforced
SYSTEM_PROMPT = """You answer based exclusively on the supplied Context.
- Cite the [doc_id] for each factual claim.
- If the context doesn't answer the question, say "I don't have that
information" — do not extrapolate or use general knowledge.
- Be concise. No throat-clearing."""
def answer(question: str) -> dict:
chunks = retrieve(question, k=5)
context = "\n\n---\n\n".join(
f"[doc:{c['id']}] {c['text']}" for c in chunks
)
resp = client.chat.completions.create(
model="claude-sonnet-4-6",
messages=[
{"role": "system", "content": [{
"type": "text",
"text": SYSTEM_PROMPT,
"cache_control": {"type": "ephemeral"},
}]},
{"role": "user", "content": f"# Context\n{context}\n\n# Question\n{question}"},
],
max_tokens=600,
)
return {
"text": resp.choices[0].message.content,
"sources": [c["id"] for c in chunks],
}Tiga hal yang tidak dapat ditawar dalam system prompt:
- Sitasi sumber — minta model melampirkan
[doc:42]pada setiap klaim. Jika ID yang disitasi tidak nyata, Anda telah menangkap halusinasi - Tolak secara eksplisit — "jika konteks tidak memberikan jawaban, katakan saya tidak memiliki informasi tersebut." Tanpa ini, model melengkapi jawaban dari pengetahuan dunia
- Output ringkas — jawaban singkat berkorelasi dengan jawaban yang akurat. Maksimum 600 token adalah default produksi yang baik
5. Model generasi + caching — lapisan biaya
Claude Sonnet 4.6 adalah default. Claude Haiku 4.5 adalah fallback murah jika Anda tertekan oleh biaya. Selalu bungkus system prompt dalam cache_control — system prompt sama pada setiap panggilan, sehingga caching memangkas bagian tersebut menjadi 10% dari tarif input.
Biaya realistis per kueri pada skala produksi (konteks 5K, output 500, caching aktif):
| Model | Biaya per kueri | Pada 10.000 kueri/hari | Kualitas dibandingkan dengan Sonnet |
|---|---|---|---|
claude-sonnet-4-6 | ~$0,007 | ~$70/hari | Acuan |
claude-haiku-4-5 | ~$0,001 | ~$10/hari | ~85%, 4× lebih murah |
Pada 10.000 kueri/hari → $70/hari pada Sonnet, $10/hari pada Haiku. Pilih Sonnet untuk kasus penggunaan berisiko tinggi (yang berinteraksi langsung dengan pelanggan, hukum, medis), dan Haiku untuk pemrosesan internal/massal.
Apa yang rusak di produksi (dan cara menangkapnya)
- Drift distribusi: korpus pelatihan tidak lagi sesuai dengan pertanyaan pengguna nyata. Ambil sampel 100 kueri/minggu, lalu periksa recall@5 secara manual
- Embedding usang: dokumen sumber diperbarui tetapi embedding tidak disegarkan. Lacak ukuran indeks dibandingkan ukuran sumber setiap minggu
- Sitasi palsu: model mengarang ID dokumen yang tampak nyata. Validasi setiap
[doc:N]terhadap daftar aktual ID yang diambil sebelum dirender - Tebing latensi: vector DB mengalami masalah setelah melewati satu juta vector. Gunakan pengindeksan HNSW, dan partisi berdasarkan tenant jika bersifat multi-tenant
Roadmap produksi 4 minggu
- Minggu 1: buat prototipe dengan 100 dokumen, 10 pertanyaan pengujian, dan evaluasi manual
- Minggu 2: skalakan ke seluruh korpus, bangun retrieval hibrida, dan tulis set evaluasi 100 pertanyaan
- Minggu 3: sesuaikan chunking + system prompt secara iteratif berdasarkan evaluasi, lalu rilis kepada pengguna internal
- Minggu 4: monitoring, anggaran biaya, dan rilis publik dengan UI sitasi sumber
Pertanyaan umum
Ukuran chunk berapa yang sebaiknya digunakan sistem RAG produksi?
1.000–1.500 karakter per chunk dengan overlap 100–200 karakter, dibagi menggunakan recursive character splitter yang memecah berdasarkan batas paragraf terlebih dahulu, kemudian kalimat, lalu kata. Model yang memahami kalimat, splitter yang memahami markdown, dan sliding window sedikit lebih baik dengan usaha yang jauh lebih besar — usaha tersebut lebih baik dialokasikan untuk retrieval.
Model embedding apa yang sebaiknya digunakan pipeline RAG?
text-embedding-3-large adalah default untuk konten multibahasa dan teknis, sedangkan untuk retrieval teks pendek berbahasa Inggris murni seperti FAQ dan kartu produk, text-embedding-3-small sekitar 4× lebih murah dengan penurunan kualitas yang dapat diabaikan. Perhatikan bahwa embedding tidak disediakan oleh Kunavo: /v1/embeddings adalah format wire yang diimplementasikan tanpa model yang diaktifkan di belakangnya, sehingga langkah embedding memanggil OpenAI, Voyage, atau Cohere secara langsung, sedangkan langkah pembuatan memanggil Kunavo. Hal ini tidak menambah biaya apa pun pada pipeline selain kunci kedua, karena keduanya memang merupakan permintaan terpisah.
Apakah pencarian vektor saja cukup untuk retrieval RAG?
Tidak. Kemiripan vektor menangani pencocokan semantik yang samar dengan baik, tetapi gagal pada istilah literal seperti SKU atau nomor pesanan. Menjalankan pencarian kata kunci BM25 secara paralel dan menggabungkannya dengan reciprocal rank fusion biasanya meningkatkan recall@5 dari sekitar 70% menjadi sekitar 90% — peningkatan kualitas tunggal terbesar setelah memilih model embedding yang layak.
Bagaimana cara menghentikan sistem RAG agar tidak berhalusinasi?
Tiga pola dalam system prompt: wajibkan sitasi [doc:N] pada setiap klaim faktual, instruksikan model untuk mengatakan bahwa ia tidak memiliki informasinya ketika konteks tidak memberikan jawaban, dan buat jawaban tetap singkat — maksimum 600 token output adalah default produksi yang baik. Kemudian validasi setiap ID yang disitasi terhadap himpunan hasil yang diambil sebelum dirender; ID yang tidak nyata adalah halusinasi yang berhasil ditangkap.
Berapa biaya kueri RAG produksi?
Dengan konteks 5K dan 500 token output serta prompt caching aktif: sekitar $0,007 per kueri pada Claude Sonnet 4.6 dan sekitar $0,001 pada Claude Haiku 4.5 (4× lebih murah, dengan kualitas kira-kira 85%). Pada 10.000 kueri per hari, biayanya sekitar $70/hari pada Sonnet dan sekitar $10/hari pada Haiku.