Episode ini menaikkan level pipeline RAG: multi-query retrieval, query rewriting, hybrid retrieval vektor plus full-text, reranking dengan cross-encoder, contextual retrieval, hingga strategi mengoptimalkan context window seperti prompt caching dan deduplikasi.

Di episode 11 kalian membangun RAG dasar yang berfungsi: buat index, jadikan retriever, tarik konteks dengan similarity_search, lalu gabungkan dengan LCEL menjadi pipeline retrieve-generate. Pipeline itu nyata dan bisa jalan — sampai diuji dengan pertanyaan yang frasanya tidak persis sama dengan isi dokumen, atau korpusnya sudah puluhan ribu chunk.
Episode 12 adalah upgrade bertahap. Kita membedah mengapa retrieval sederhana gagal, lalu memperbaikinya satu per satu: multi-query retrieval, query rewriting, hybrid retrieval (vektor + full-text), reranking dengan cross-encoder, contextual retrieval, dan terakhir mengelola context window agar anggaran token tetap sehat.
Retrieval vektor murni bekerja dengan satu asumsi: kalimat query dan dokumen yang relevan itu dekat di ruang embedding. Asumsi ini meleset karena beberapa alasan:
k tetap (misalnya 4) berarti selalu mengambil 4 chunk terdekat, padahal beberapa bisa hampir tidak relevan.Karena itu produksi memakai rangkaian teknik yang saling menambal, bukan satu trik ajaib.
Ide multi-query retrieval sederhana: minta LLM menghasilkan beberapa parafrase kueri yang menangkap sudut pandang berbeda, tarik dokumen untuk tiap kueri, lalu gabungkan hasilnya. Query "berapa lama daya tahan notebook" bisa diperluas menjadi "durasi baterai laptop", "kapasitas baterai jam pemakaian", dan seterusnya — menutup celah vocabulary mismatch. Di LangChain, ini sudah dibungkus dalam MultiQueryRetriever:
from langchain_openai import ChatOpenAI
from langchain.retrievers.multi_query import MultiQueryRetriever
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.2)
retriever = MultiQueryRetriever.from_llm(
retriever=vector_store.as_retriever(search_kwargs={"k": 4}),
llm=llm,
)
results = await retriever.ainvoke("berapa lama daya tahan notebook?")MultiQueryRetriever tetap menerima invoke dan ainvoke, jadi ia drop-in menggantikan retriever biasa di chain kalian. k dipakai per kueri dan hasil semua kueri digabung, jadi jumlah dokumennya bisa lebih banyak dari k; LangChain mencatat kueri-kueri yang dihasilkan sebagai metadata tiap dokumen — berguna saat men-debug dengan LangSmith.
Di aplikasi chat, pertanyaan sering bergantung pada konteks sebelumnya: "kalau begitu gimana cara evaluasinya?" tidak bisa langsung dicari di index tanpa tahu "itu" merujuk ke mana. Query rewriting mengubah pertanyaan mentah menjadi kueri pencarian yang mandiri dan spesifik sebelum dikirim ke retriever. Buat chain rewriting kecil dengan ChatPromptTemplate dan StrOutputParser:
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
rewrite_prompt = ChatPromptTemplate.from_messages([
("system", "Tulis ulang pertanyaan menjadi kueri pencarian mandiri, "
"tanpa kata ganti ambigu, dan pertahankan istilah teknisnya."),
("human", "{question}"),
])
rewriter = rewrite_prompt | llm | StrOutputParser()
query = rewriter.invoke({
"question": "kalau begitu, gimana cara evaluasinya?"
})
docs = retriever.invoke(query)Hasil rewriting adalah string, bukan kalimat jawaban — itulah gunanya StrOutputParser. Urutan pipeline menjadi: rewrite dahulu, lalu retrieve, lalu generate. Cek riwayat dialog dari episode 7 bila ingin pipeline yang lebih rapi.
Embedding hebat untuk menangkap makna, tapi buruk untuk kecocokan istilah yang presisi: nomor seri, kode error, nama fungsi. Sebaliknya, full-text search seperti BM25 unggul di kecocokan token persis tapi tidak paham sinonim. Hybrid retrieval menggabungkan keduanya; EnsembleRetriever menggabungkan hasilnya via Reciprocal Rank Fusion:
from langchain_community.retrievers import BM25Retriever
from langchain.retrievers import EnsembleRetriever
bm25 = BM25Retriever.from_documents(docs, k=4)
vector = vector_store.as_retriever(search_kwargs={"k": 4})
hybrid = EnsembleRetriever(
retrievers=[bm25, vector],
weights=[0.5, 0.5],
)
docs = hybrid.invoke("kode error E401 muncul saat build")Bobot 0.5 dan 0.5 adalah titik awal yang sehat; jika korpus lebih banyak istilah teknis eksak, geser ke arah BM25. BM25Retriever butuh list Document asli sebagai argumen, jadi simpan selama pembangunan index. Untuk corpus besar, ganti BM25 in-memory dengan Elasticsearch atau PostgreSQL full-text lewat integrasi yang sama-sama mengembalikan objek Retriever.
Retrieval murah (bi-encoder) sering menghasilkan banyak dokumen yang "cukup dekat" tapi tidak benar-benar relevan. Reranker berarsitektur cross-encoder menilai pasangan query-dokumen secara bersamaan — jauh lebih akurat, tapi mahal, jadi hanya dijalankan pada kandidat yang sudah sedikit. Gabungkan dengan ContextualCompressionRetriever:
from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import CrossEncoderReranker
from langchain_community.cross_encoders import HuggingFaceCrossEncoder
compressor = CrossEncoderReranker(
model=HuggingFaceCrossEncoder(model_name="BAAI/bge-reranker-v2-m3"),
top_n=3,
)
compression_retriever = ContextualCompressionRetriever(
base_compressor=compressor,
base_retriever=hybrid,
)
final_docs = compression_retriever.invoke("apa perbedaan RAG dan fine-tuning?")Pipeline retrieval ideal di produksi: ambil banyak kandidat murah (misalnya k sebesar 20) dari hybrid retrieval, lalu rerank hingga top_n kecil (3-5) sebelum masuk prompt. Konteks yang dikirim ke model jadi lebih padat, relevan, dan hemat token. Model bge-reranker-v2-m3 berjalan lokal tanpa API key — pas untuk eksperimen.
Warning
Jangan rerank seluruh korpus — reranker kalah cepat dari bi-encoder. Prinsipnya: retrieval cepat untuk mempersempit kandidat, rerank akurat untuk memilih yang terbaik.
Ketika dokumen dipecah menjadi chunk kecil, konteks global ikut terpotong — chunk tentang "tabel tersebut" kehilangan informasi bahwa itu tabel konfigurasi jaringan. Contextual retrieval (dipopulerkan riset OpenAI) menambahkan header konteks: sebelum menyimpan chunk ke index, LLM meringkas konteks dokumen dan menempelkannya di depan chunk.
from langchain_core.documents import Document
context_prompt = ChatPromptTemplate.from_messages([
("system", "Ringkas dokumen menjadi 3-5 kalimat konteks yang menjelaskan "
"posisi dan topik keseluruhan dokumen."),
("human", "{document}"),
])
context_chain = context_prompt | llm | StrOutputParser()
contextual_docs = []
for doc in docs:
context = context_chain.invoke({"document": doc.page_content})
contextual_docs.append(Document(
page_content=f"Konteks dokumen: {context}\n\n{doc.page_content}",
metadata=doc.metadata,
))Embedding dan simpan contextual_docs ke vector store seperti biasa. Biaya ekstra ada di proses indexing (satu panggilan LLM per chunk), jadi jalankan sekali di pipeline ingestion, bukan per request. Saat retrieval, header konteks membuat chunk yang terisolasi tetap masuk akal secara mandiri.
Semakin banyak teknik yang dipasang, semakin besar risiko context window meledak dan biaya per request membengkak. Empat pengendali utama:
Contoh pengendalian budget bersama deduplikasi:
def dedupe_documents(docs):
seen = set()
unique = []
for doc in docs:
key = hash(doc.page_content)
if key not in seen:
seen.add(key)
unique.append(doc)
return unique
TOKEN_BUDGET = 6000
selected = []
used = 0
for doc in dedupe_documents(final_docs):
n = llm.get_num_tokens(doc.page_content)
if used + n > TOKEN_BUDGET:
continue
selected.append(doc)
used += nget_num_tokens menghitung token offline memakai tokenizer model — tanpa panggilan jaringan — jadi cocok untuk menyaring sebelum request. Mulai dari budget 40-60 persen dari context window, sisakan ruang untuk pertanyaan dan jawaban. Ingat urutannya: deduplikasi dulu, baru budget.
Episode ini menaikkan RAG kalian dari "berfungsi" menjadi "tahan banting": multi-query retrieval menutup celah kosakata, query rewriting membuat kueri mandiri, hybrid retrieval menggabungkan makna dan kecocokan eksak, reranking memilih kandidat terbaik, contextual retrieval menjaga konteks dokumen, serta prompt caching, deduplikasi, dan budget token menekan biaya. Semakin banyak lapisan, semakin baik kualitas jawaban dengan harga retrieval yang masih masuk akal.
Inti yang harus dibawa pulang:
top_n kecil.RAG yang optimal hanyalah alat untuk memberi model "ingatan". Di episode 13 kita memberinya kemampuan untuk bertindak: agents dengan LangGraph — agen yang memutuskan sendiri tool apa yang dipakai, dengan state yang bisa dijeda dan dilanjutkan (human-in-the-loop). Sampai jumpa di sana!