Membangun Aplikasi RAG LLM untuk Produksi: Arsitektur, Chunking, dan Evaluasi

Foto oleh Unsplash

Foto oleh Unsplash
Retrieval-Augmented Generation (RAG) telah menjadi pola dominan untuk membangun aplikasi LLM yang perlu menjawab pertanyaan tentang data proprietary atau yang sering diperbarui — dokumentasi internal, katalog produk, tiket support, dan teks regulasi. Membuat prototipe bekerja di notebook cukup mudah, tetapi membawa pipeline RAG ke produksi membutuhkan keputusan cermat tentang strategi chunking, pemilihan vector store, kualitas retrieval, dan metodologi evaluasi.
Pipeline RAG produksi terdiri dari dua fase berbeda: pipeline ingestion offline yang memproses dan mengindeks dokumen, dan pipeline query online yang mengambil konteks dan menghasilkan jawaban. Fase ingestion menangani loading dokumen mentah dari berbagai sumber (PDF, database, API), memecahnya menjadi chunk, menghasilkan embedding, dan menyimpannya di vector database. Fase query mengembedding pertanyaan user, melakukan similarity search untuk mengambil chunk relevan, membuat prompt dengan konteks yang diambil, dan memanggil LLM untuk menghasilkan respons yang berdasar.
Dokumen mentah jarang datang dalam format yang siap di-chunk. PDF mengandung header, footer, dan layout multi-kolom yang merusak ekstraksi teks naif. Halaman HTML menyertakan navigasi dan boilerplate yang mengencerkan relevansi semantik. Preprocessing harus menormalisasi whitespace, menghapus boilerplate, dan mempertahankan metadata struktural (heading, nomor bagian, URL sumber, tanggal dokumen) yang akan disimpan bersama setiap chunk.
Chunking fixed-size memecah teks setiap N karakter dengan overlap yang dapat dikonfigurasi — sederhana tetapi sering memotong kalimat di tengah. Chunking semantik menggunakan batas kalimat dan mengelompokkan kalimat hingga ambang similarity turun, mempertahankan pemikiran lengkap dengan biaya ukuran chunk bervariasi. Chunking hierarkis membuat hubungan parent-child: chunk ringkasan besar untuk retrieval luas dengan chunk detail lebih kecil untuk presisi. Untuk kebanyakan use case produksi, RecursiveCharacterTextSplitter dengan chunk 512 token dan overlap 64 token memberikan baseline yang kuat.
Selalu simpan teks dokumen asli bersama chunk yang diembedding, bukan hanya chunk itu sendiri. Ketika chunk cocok saat retrieval, Anda dapat mengambil paragraf sekitarnya (pola 'parent document retriever') untuk memberikan konteks lebih kaya ke LLM tanpa mengembedding dokumen penuh sebagai satu vektor.
pgvector memperluas database PostgreSQL Anda yang sudah ada dengan pencarian approximate nearest neighbor (ANN) menggunakan index IVFFlat atau HNSW — ideal jika Anda sudah menjalankan Postgres dan ingin meminimalkan kompleksitas infrastruktur. Qdrant adalah vector database yang dibangun khusus dengan API filtering yang kaya, payload indexing, dan skalabilitas horizontal untuk workload miliaran vektor. Untuk kebanyakan startup Indonesia dengan kurang dari 10 juta dokumen, pgvector pada instance Postgres yang dioptimalkan sudah cukup.
Integrasi PGVector LangChain menangani pembuatan embedding, upsert, dan similarity search melalui interface yang konsisten. Chain RetrievalQA menyambungkan retriever, template prompt custom yang menginstruksikan LLM untuk menjawab hanya dari konteks yang disediakan, dan LLM itu sendiri. Menggunakan search_type='mmr' (Maximal Marginal Relevance) saat retrieval menyeimbangkan relevansi dengan query dan keragaman di antara chunk yang dikembalikan, mengurangi risiko mengambil lima passage hampir identik yang membuang ruang context window.
# rag_pipeline.py
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_postgres import PGVector
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.chains import RetrievalQA
from langchain.prompts import PromptTemplate
import psycopg2
# Connection string for pgvector
CONNECTION_STRING = (
"postgresql+psycopg2://user:password@localhost:5432/ragdb"
)
# 1. Chunking strategy — overlapping chunks with metadata
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=512,
chunk_overlap=64,
separators=["\n\n", "\n", ". ", " ", ""],
)
def ingest_documents(docs: list[dict]) -> None:
"""Ingest documents into pgvector with metadata enrichment."""
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vectorstore = PGVector(
embeddings=embeddings,
collection_name="knowledge_base",
connection=CONNECTION_STRING,
)
for doc in docs:
chunks = text_splitter.create_documents(
texts=[doc["content"]],
metadatas=[{
"source": doc["source"],
"doc_type": doc.get("type", "unknown"),
"created_at": doc.get("created_at", ""),
}]
)
vectorstore.add_documents(chunks)
def build_rag_chain():
"""Build a production RAG chain with custom prompt."""
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vectorstore = PGVector(
embeddings=embeddings,
collection_name="knowledge_base",
connection=CONNECTION_STRING,
)
retriever = vectorstore.as_retriever(
search_type="mmr", # Maximal Marginal Relevance
search_kwargs={"k": 5, "fetch_k": 20},
)
prompt = PromptTemplate(
input_variables=["context", "question"],
template=(
"Use only the context below to answer the question. "
"If unsure, say you don't know.\n\n"
"Context:\n{context}\n\nQuestion: {question}\nAnswer:"
),
)
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
return RetrievalQA.from_chain_type(
llm=llm,
retriever=retriever,
chain_type_kwargs={"prompt": prompt},
return_source_documents=True,
)Prompt RAG harus secara eksplisit menginstruksikan LLM untuk mendasarkan jawabannya hanya pada konteks yang disediakan dan mengatakan 'Saya tidak tahu' ketika konteks tidak cukup — tanpa batasan ini, model yang capable akan berhalusinasi jawaban yang terdengar masuk akal dari data training. Sertakan metadata sumber yang diambil (judul dokumen, nomor halaman, URL) dalam blok konteks agar LLM dapat mengutipnya dalam jawaban.
Pencarian similarity berbasis embedding (cosine atau dot-product) cepat tetapi tidak sempurna — ia mengambil teks yang serupa secara semantik meskipun sebenarnya tidak menjawab pertanyaan. Menambahkan cross-encoder re-ranker (misalnya model BERT kecil yang di-fine-tune pada MS-MARCO) sebagai filter tahap kedua secara dramatis meningkatkan presisi dengan menilai setiap chunk yang diambil terhadap teks query penuh. Pendekatan dua tahap ini (embed → ambil top-20 → re-rank → ambil top-5) adalah pola standar dalam sistem produksi.
Pencarian vektor dense unggul dalam similarity semantik tetapi kesulitan dengan pencocokan keyword yang tepat — kode produk, nama, dan pengenal teknis. Retrieval sparse (BM25 atau TF-IDF) menangani pencocokan tepat dengan baik tetapi melewatkan parafrase. Hybrid search menggabungkan kedua skor menggunakan Reciprocal Rank Fusion (RRF) untuk mendapatkan yang terbaik dari keduanya. Qdrant dan versi pgvector terbaru mendukung hybrid search secara native.
Memecah dokumen pada jumlah karakter tetap tanpa memperhatikan batas kalimat atau paragraf adalah penyebab paling umum kinerja RAG yang buruk. Chunk yang dimulai di tengah kalimat kehilangan subjeknya; chunk yang memotong tabel menghasilkan konteks yang berantakan. Selalu validasi output chunking Anda secara visual pada sampel dokumen nyata sebelum mengindeks seluruh corpus. Gunakan chunk yang tumpang tindih (minimal overlap 10–15%) untuk memastikan informasi batas tertangkap.
RAGAS menyediakan suite metrik bebas referensi yang mengevaluasi kualitas RAG tanpa memerlukan ground truth berlabel manusia yang mahal. Empat metrik inti adalah faithfulness (fraksi klaim jawaban yang didukung oleh konteks), answer relevancy (seberapa baik jawaban menjawab pertanyaan), context precision (fraksi chunk yang diambil yang benar-benar relevan), dan context recall (fraksi informasi relevan yang berhasil diambil).
Pembuatan embedding adalah biaya dominan dalam pipeline RAG — text-embedding-3-small OpenAI mengenakan biaya per token bahkan untuk dokumen yang berulang. Implementasikan cache berbasis konten (hash teksnya, simpan embedding di Redis atau tabel khusus) untuk menghindari re-embedding dokumen yang tidak berubah pada ingestion inkremental. Untuk hasil query, semantic cache dapat melayani jawaban yang di-cache untuk pertanyaan yang hampir duplikat.
Latensi pembuatan LLM untuk jawaban penuh bisa 3–8 detik, yang terasa tidak responsif di UI chat. LangChain mendukung streaming callback yang memancarkan token saat dihasilkan, memungkinkan frontend menampilkan respons parsial secara progresif. Pasangkan streaming dengan panel 'sumber' yang muncul segera (sebelum LLM selesai) untuk menunjukkan kepada user dokumen mana yang diambil sambil mereka membaca jawaban.
Jalankan evaluasi RAGAS pada 50–100 pasangan pertanyaan-jawaban yang diambil dari kasus penggunaan nyata Anda sebelum go live. Metrik faithfulness RAGAS (apakah jawaban hanya mengandung klaim yang didukung oleh konteks?) adalah sinyal terpenting — skor faithfulness di bawah 0.8 menunjukkan prompt atau retrieval Anda perlu di-tuning. Otomatiskan evaluasi ini agar berjalan di setiap perubahan signifikan pada pipeline.
Sistem RAG produksi harus menyeimbangkan kualitas jawaban dengan biaya API dan latensi. Pengungkit biaya utama adalah: pemilihan model embedding (text-embedding-3-small $0.02/1M token vs ada-002 $0.10/1M token), ukuran context window (kirim lebih sedikit tapi chunk berkualitas lebih tinggi), caching, dan batching permintaan ingestion. Untuk aplikasi volume tinggi, pertimbangkan menjalankan model embedding self-hosted (e5-large-v2 atau BGE-M3) pada instance GPU untuk menghilangkan biaya embedding per-token.
Instrumentasikan pipeline RAG Anda dengan histogram latensi (waktu retrieval, waktu LLM, total waktu respons), skor faithfulness yang dihitung secara async setelah setiap respons, dan mekanisme feedback jempol atas/bawah di UI. Ekspor metrik ini ke Prometheus dan visualisasikan di Grafana. Tetapkan ambang alert: jika faithfulness rata-rata turun di bawah 0.75 selama window bergulir 1 jam, hubungi engineer on-call.
Istilah kunci dalam artikel ini meliputi RAG, pgvector, RAGAS, and MMR (Maximal Marginal Relevance).
Sumber