โ† Semua pembelajaran / Python untuk AI Engineer
Fase 7 ยท Capstone: RAG Chatbot

Introducing Contextual Retrieval

Chunk yang terpotong kehilangan konteksnya. Contextual retrieval memperbaikinya dengan menambahkan konteks sebelum embedding.

Sumber asli anthropic.com Artikel Rangkuman ~6 menit baca

Intisari

  • Masalah inti RAG naif: satu chunk kehilangan konteks dokumen induknya, sehingga tidak bisa ditemukan.
  • Solusinya: tambahkan satu-dua kalimat konteks ke tiap chunk sebelum di-embed.
  • Konteksnya dibuat oleh LLM, sekali saat ingest โ€” dan prompt caching membuat biayanya kecil.
  • Gabungkan embedding (semantik) dengan BM25 (kata kunci) untuk hasil terbaik.
  • Tambahkan tahap reranking kalau masih kurang akurat.

Masalahnya

Bayangkan dokumen laporan keuangan dipotong jadi chunk. Salah satu chunk berbunyi:

"Pendapatan perusahaan tumbuh 3% dibanding kuartal sebelumnya."

Kalau pengguna bertanya "berapa pertumbuhan pendapatan ACME di Q2 2023?", chunk itu kemungkinan besar tidak ditemukan. Kenapa? Karena chunk itu tidak menyebut:

Semua informasi itu ada di dokumen induknya โ€” tapi hilang saat dipotong.

Inilah kegagalan paling umum di sistem RAG. Bukan modelnya yang salah menjawab, tapi retrieval-nya yang tidak pernah menemukan chunk yang benar. Kalau konteks yang tepat tidak masuk ke prompt, model tidak punya kesempatan menjawab dengan benar.

Contextual retrieval

Idenya sederhana: sebelum meng-embed, tambahkan konteks singkat di depan tiap chunk.

SEBELUM (chunk mentah):
"Pendapatan perusahaan tumbuh 3% dibanding kuartal sebelumnya."

SESUDAH (dengan konteks):
"Potongan ini berasal dari laporan keuangan ACME Corp untuk Q2 2023;
bagian sebelumnya membahas pendapatan Q1 sebesar $314 juta.

Pendapatan perusahaan tumbuh 3% dibanding kuartal sebelumnya."

Sekarang chunk itu mengandung "ACME", "Q2 2023", dan "pendapatan" โ€” dan bisa ditemukan oleh pertanyaan tadi.

Cara membuat konteksnya

PROMPT_KONTEKS = """<dokumen>
{dokumen}
</dokumen>

Berikut satu potongan dari dokumen di atas:
<potongan>
{chunk}
</potongan>

Tulis 1-2 kalimat singkat yang menempatkan potongan ini dalam konteks
keseluruhan dokumen, untuk memperbaiki hasil pencarian.
Jawab hanya dengan kalimat konteksnya, tanpa pembuka apa pun."""


async def buat_konteks(dokumen: str, chunk: str) -> str:
    r = await client.messages.create(
        model="claude-haiku-4-5",          # tugas sederhana, model murah sudah cukup
        max_tokens=200,
        system=[{
            "type": "text",
            "text": f"<dokumen>\n{dokumen}\n</dokumen>",
            "cache_control": {"type": "ephemeral"},     # โ† kunci penghematan biaya
        }],
        messages=[{"role": "user", "content": PROMPT_KONTEKS.format(
            dokumen="", chunk=chunk,
        )}],
    )
    return next(b.text for b in r.content if b.type == "text")

Prompt caching adalah yang membuat teknik ini terjangkau. Kamu memanggil LLM sekali per chunk, dan tiap panggilan menyertakan seluruh dokumen. Tanpa cache, dokumen 50 halaman dengan 100 chunk berarti membayar 100 kali untuk dokumen yang sama. Dengan cache, dokumennya ditulis sekali dan dibaca 99 kali dengan harga ~0,1ร—.

Pipeline ingest lengkap

async def ingest_dengan_konteks(path: Path, collection) -> None:
    dokumen = path.read_text(encoding="utf-8")
    chunks = potong(dokumen, ukuran=1000, tumpang=200)

    sem = asyncio.Semaphore(10)

    async def olah(i: int, chunk: str) -> dict:
        async with sem:
            konteks = await buat_konteks(dokumen, chunk)
        teks_lengkap = f"{konteks}\n\n{chunk}"
        return {
            "id": f"{path.stem}-{i}",
            "teks_embed": teks_lengkap,     # yang di-EMBED
            "teks_asli": chunk,             # yang DIKIRIM ke LLM saat menjawab
            "konteks": konteks,
            "sumber": str(path),
        }

    hasil = await asyncio.gather(*(olah(i, c) for i, c in enumerate(chunks)))

    embeddings = await buat_embedding([h["teks_embed"] for h in hasil])
    collection.upsert(
        ids=[h["id"] for h in hasil],
        embeddings=embeddings,
        documents=[h["teks_asli"] for h in hasil],
        metadatas=[{"sumber": h["sumber"], "konteks": h["konteks"]} for h in hasil],
    )

Perhatikan pemisahan teks_embed dan teks_asli. Konteks ditambahkan untuk memperbaiki pencarian. Saat menyusun prompt jawaban, kamu bisa memilih mengirim chunk aslinya saja (lebih hemat token) atau versi lengkapnya (lebih jelas bagi model). Simpan keduanya supaya kamu punya pilihan.

Hybrid search: embedding + BM25

Embedding menangkap makna; BM25 menangkap kata kunci persis. Keduanya gagal di tempat berbeda โ€” dan itulah kenapa menggabungkannya bekerja.

MetodeKuat untukLemah untuk
EmbeddingParafrase, sinonim, konsepKode error, ID, nama produk, istilah langka
BM25Istilah persis, kode, namaPertanyaan yang tidak memakai kata yang sama
from rank_bm25 import BM25Okapi

bm25 = BM25Okapi([c.split() for c in semua_chunk])

def cari_hybrid(query: str, emb_query: list[float], k: int = 20) -> list[str]:
    # Ambil kandidat dari kedua sisi
    id_vektor = cari_vektor(emb_query, k=k)
    id_bm25 = [semua_id[i] for i in bm25.get_top_n(query.split(), range(len(semua_id)), n=k)]

    # Gabungkan dengan Reciprocal Rank Fusion
    skor: dict[str, float] = {}
    for peringkat, id_ in enumerate(id_vektor):
        skor[id_] = skor.get(id_, 0) + 1 / (60 + peringkat)
    for peringkat, id_ in enumerate(id_bm25):
        skor[id_] = skor.get(id_, 0) + 1 / (60 + peringkat)

    return sorted(skor, key=skor.get, reverse=True)[:k]

Reranking

Ambil 20 kandidat teratas dari hybrid search, lalu urutkan ulang dengan model yang lebih teliti, dan ambil 5 terbaik untuk dikirim ke LLM.

Query
  โ”‚
  โ”œโ”€ Vector search  โ”€โ”€โ”
  โ”‚                   โ”œโ”€โ”€> 20 kandidat โ”€โ”€> Rerank โ”€โ”€> 5 terbaik โ”€โ”€> LLM
  โ””โ”€ BM25 search   โ”€โ”€โ”˜

Reranking lebih mahal per dokumen, jadi hanya dijalankan pada kandidat yang sudah tersaring. Ini pola dua tahap yang standar: recall dulu (ambil banyak), lalu precision (saring yang terbaik).

Urutan perbaikan yang disarankan

  1. RAG dasar โ€” chunking + embedding + cosine similarity. Ukur akurasinya.
  2. Tumpang tindih chunk โ€” 10โ€“20% dari ukuran chunk, supaya kalimat tidak terpotong di batas
  3. Contextual retrieval โ€” biasanya lompatan terbesar
  4. Hybrid search โ€” tambahkan BM25
  5. Reranking โ€” kalau masih kurang

Ukur di tiap langkah. Buat dataset 20โ€“30 pertanyaan dengan chunk jawaban yang seharusnya ditemukan, lalu hitung berapa persen yang benar-benar masuk ke top-5. Tanpa angka itu, kamu hanya menebak apakah perubahanmu memperbaiki atau justru merusak.

Ukuran chunk

UkuranKelebihanKekurangan
Kecil (200โ€“400 token)Pencarian presisiKehilangan konteks, butuh lebih banyak chunk
Sedang (500โ€“1000)Keseimbangan yang baikโ€”
Besar (1500+)Konteks utuhBanyak isi tidak relevan ikut terkirim

Mulai dari 800 token dengan tumpang tindih 150. Potong di batas yang alami (paragraf, judul bagian), bukan di tengah kalimat.

Yang layak dicatat untuk eval

Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.