ColBERT & RAGatouille: Retrieval Late-Interaction untuk RAG yang Lebih Baik
Sebagian besar sistem RAG mengandalkan dense embedding satu-vektor, di mana seluruh paragraf dipadatkan menjadi satu vektor. Pemadatan itu praktis dan cepat, tetapi membuang detail level-token yang sering menentukan apakah hasil retrieval benar-benar relevan. Tutorial ini menjelaskan bagaimana pendekatan late-interaction ColBERT mempertahankan detail tersebut, dan bagaimana RAGatouille membuatnya bisa Anda adopsi tanpa menulis ulang seluruh stack.
Masalah pada Dense Retrieval Satu-Vektor
Bi-encoder standar (bayangkan Sentence-Transformers yang memberi makan FAISS atau pgvector) mengubah query menjadi satu vektor dan tiap dokumen menjadi satu vektor, lalu mengurutkan berdasarkan cosine similarity. Inilah yang dibahas pada tutorial semantic-search, FAISS, dan Sentence-Transformers, dan pendekatan ini bekerja baik untuk banyak kasus.
Kelemahannya adalah information bottleneck. Sebuah paragraf 512 token tentang, misalnya, "klaim garansi untuk mesin diesel pada truk tambang" dirata-ratakan menjadi satu vektor 768 dimensi. Istilah spesifik dan keterkaitannya tercampur menjadi satu. Ketika query menanyakan satu detail presisi ("berapa periode garansi turbocharger?"), satu vektor dokumen itu mungkin tidak memunculkannya, karena detail turbocharger sudah terlarut oleh segala hal lain di paragraf tersebut.
Tiga mode kegagalan sering muncul:
- Presisi leksikal hilang. Istilah langka tapi penting (nomor part, nama obat, rujukan pasal) terhapus oleh rata-rata.
- Paragraf panjang menurun kualitasnya. Semakin panjang chunk, semakin satu vektor menjadi ringkasan yang kabur.
- Query di luar domain menderita. Bi-encoder generik tidak punya sinyal level-token sebagai cadangan.
Tiga Arsitektur Retrieval
Akan membantu menempatkan ColBERT di antara dua arsitektur yang sudah Anda kenal.
Bi-encoder (dense satu-vektor)
Query dan dokumen di-encode secara terpisah menjadi masing-masing satu vektor. Similarity adalah satu dot product.
- Kualitas: baik, dengan plafon yang dibatasi bottleneck.
- Latensi: sangat rendah saat query (satu vektor, pencarian ANN).
- Penyimpanan: kecil (satu vektor per dokumen).
Cross-encoder
Query dan dokumen digabung dan dimasukkan ke model bersamaan, menghasilkan satu skor relevansi. Model dapat memperhatikan setiap pasangan token query-dokumen.
- Kualitas: tertinggi.
- Latensi: sangat tinggi. Model harus dijalankan sekali per dokumen kandidat, sehingga tidak bisa memindai korpus; hanya bisa me-rerank daftar pendek.
- Penyimpanan: tidak ada yang dihitung di muka (dan inilah masalahnya pada skala besar).
Late interaction (ColBERT)
ColBERT mempertahankan satu embedding per token baik untuk query maupun dokumen. Saat scoring, setiap embedding token-query dicocokkan dengan semua embedding token-dokumen, kecocokan terbaik per token query disimpan (MaxSim), lalu dijumlahkan.
- Kualitas: mendekati cross-encoder, jauh di atas bi-encoder.
- Latensi: sedang. Embedding token dokumen dihitung di muka dan diindeks; hanya agregasi MaxSim yang terjadi saat query.
- Penyimpanan: lebih besar (banyak vektor per dokumen), yang menjadi biaya utamanya.
Inti idenya: ColBERT menunda interaksi query-dokumen sampai keduanya selesai di-encode ("late"), sehingga dokumen tetap bisa dihitung di muka dan diindeks, namun perbandingannya tetap di level token.
Cara Kerja Scoring MaxSim
Diberikan sebuah query dengan embedding token q1 ... qn dan dokumen dengan embedding token d1 ... dm, skor ColBERT adalah:
score(Q, D) = jumlah atas i dari ( maks atas j dari ( qi . dj ) )
Untuk setiap token query, cari satu token dokumen yang paling cocok dengannya (operasi maks), lalu jumlahkan kecocokan terbaik itu untuk semua token query. Token query untuk "turbocharger" dapat berpaut pada satu token dokumen yang menyebut turbocharger, meskipun sisa paragrafnya membahas hal lain. Itulah sinyal yang hilang pada satu vektor rata-rata.
Ilustrasi kecil operasi ini dengan NumPy (konseptual, bukan engine sebenarnya):
import numpy as np
def maxsim(querytokenemb, doctokenemb):
# querytokenemb: (nq, dim), doctokenemb: (nd, dim)
# asumsikan embedding ter-normalisasi L2, jadi dot product == cosine
sim = querytokenemb @ doctokenemb.T # (nq, nd)
perquerybest = sim.max(axis=1) # token dokumen terbaik per token query
return perquerybest.sum() # jumlah atas token query
q = np.random.randn(8, 128); q /= np.linalg.norm(q, axis=1, keepdims=True)
d = np.random.randn(180, 128); d /= np.linalg.norm(d, axis=1, keepdims=True)
print("Skor MaxSim:", maxsim(q, d))
ColBERT sebenarnya memakai vektor token 128 dimensi (jauh lebih kecil dari vektor kalimat 768 dimensi yang umum), yang sebagian mengimbangi biaya penyimpanan karena banyaknya vektor yang disimpan.
Mengapa ColBERTv2 + PLAID Membuatnya Praktis
ColBERT versi awal menyimpan satu vektor float penuh per token, sehingga indeksnya sangat besar. Dua perkembangan membuat late interaction layak dideploy:
- Kompresi residual (ColBERTv2). Alih-alih menyimpan setiap vektor token secara penuh, ColBERTv2 mengelompokkan embedding token dengan centroid dan menyimpan tiap token sebagai id centroid plus residual terkuantisasi yang kecil. Ini memangkas ukuran indeks secara drastis sambil mempertahankan sebagian besar kualitas.
- Engine PLAID. PLAID adalah engine retrieval yang dioptimalkan untuk ColBERTv2. PLAID memangkas kandidat secara agresif memakai scoring level-centroid sebelum menjalankan MaxSim penuh yang mahal, sehingga latensi query turun ke kisaran yang bisa dipakai bahkan pada koleksi besar.
Keduanya bersama-sama mengubah late interaction dari sekadar keingintahuan riset menjadi sesuatu yang bisa Anda jalankan pada satu mesin untuk korpus berukuran sedang.
Masuk ke RAGatouille
RAGatouille adalah pembungkus tipis dan praktis di atas library ColBERT. Ia menyembunyikan konfigurasi indexing dan PLAID di balik beberapa metode, sehingga Anda bisa indexing, search, rerank, bahkan fine-tune tanpa mempelajari internalnya.
Instalasi
pip install ragatouille
RAGatouille membawa torch dan engine colbert.
GPU CUDA sangat disarankan untuk indexing koleksi yang lebih besar.
python -c "from ragatouille import RAGPretrainedModel; print('RAGatouille siap')"
Memuat Model Pretrained
from ragatouille import RAGPretrainedModel
colbert-ir/colbertv2.0 adalah checkpoint bahasa Inggris standar
RAG = RAGPretrainedModel.frompretrained("colbert-ir/colbertv2.0")
Ini mengunduh bobot ColBERTv2 dari Hugging Face Hub dan menyiapkan model untuk indexing serta query.
Contoh End-to-End: Sebuah Retriever Domain
Kita akan membangun retriever kecil di atas basis pengetahuan perawatan armada fiktif, lalu melakukan query. Biarkan dokumen tetap berupa paragraf alami; RAGatouille akan memecahnya (chunking) untuk Anda.
Menyiapkan Koleksi
documents = [
"Turbocharger pada truk tambang DX-900 memiliki periode garansi "
"24 bulan atau 4000 jam operasi, mana yang tercapai lebih dulu. Klaim "
"garansi memerlukan buku catatan servis asli.",
"Penggantian oli rutin untuk mesin diesel DX-900 dijadwalkan setiap "
"500 jam operasi. Gunakan oli grade 15W-40 yang disetujui pabrikan.",
"Katup pelepas tekanan sistem hidraulik harus diperiksa setiap "
"triwulan. Katup pengganti tercakup dalam garansi part 12 bulan.",
"Interval penggantian kampas rem bergantung pada medan. Pada operasi "
"tambang terbuka curam, periksa kampas rem setiap 250 jam operasi.",
"Coolant untuk DX-900 harus diganti setiap 2000 jam operasi. "
"Mencampur jenis coolant membatalkan garansi sistem pendingin.",
]
Id yang stabil memungkinkan Anda memetakan hasil kembali ke record sumber.
docids = ["garansi-turbo", "ganti-oli", "katup-hidraulik",
"kampas-rem", "coolant"]
Metadata opsional ikut bersama tiap dokumen.
docmetadatas = [{"bagian": "garansi"}, {"bagian": "perawatan"},
{"bagian": "garansi"}, {"bagian": "perawatan"},
{"bagian": "garansi"}]
Membangun Indeks
RAG.index(
collection=documents,
documentids=docids,
documentmetadatas=docmetadatas,
indexname="perawatanarmada",
maxdocumentlength=256, # token per chunk; dokumen panjang dipecah
splitdocuments=True, # biarkan RAGatouille memecah paragraf panjang
)
Catatan tentang chunking dan panjang:
maxdocumentlengthadalah ukuran chunk dalam token. Penyimpanan per-token ColBERT berarti chunk yang sangat panjang membengkakkan indeks, jadi 180-300 token adalah titik yang umum optimal.- Dengan
splitdocuments=True, satu dokumen panjang menjadi beberapa paragraf terindeks yang tetap tertaut kedocumentidyang sama, sehingga Anda bisa mengelompokkan hasil per sumber. - Indeks ditulis ke disk (di bawah
.ragatouille/) dan dapat dimuat ulang nanti tanpa meng-encode ulang.
Melakukan Pencarian
results = RAG.search(
query="Berapa periode garansi turbocharger?",
k=3,
)
for r in results:
print(round(r["score"], 2), r["documentid"], "->", r["content"][:80])
Output umumnya menempatkan paragraf garansi turbocharger di urutan pertama dengan margin skor yang jelas, karena token query "turbocharger" cocok dengan token persis itu di paragraf melalui MaxSim, alih-alih bergantung pada rata-rata paragraf yang kabur.
Memuat Ulang Indeks yang Ada
RAG = RAGPretrainedModel.fromindex(".ragatouille/colbert/indexes/perawatanarmada")
results = RAG.search(query="seberapa sering mengganti coolant", k=2)
Menggunakan RAGatouille sebagai Reranker
Anda tidak harus menjadikan ColBERT sebagai retriever tahap pertama. Pola yang umum dan hemat biaya adalah mempertahankan bi-encoder murah atau BM25 sebagai tahap pertama, mengambil daftar kandidat, lalu membiarkan ColBERT me-rerank-nya. Ini memberi Anda kualitas mendekati late-interaction sambil membayar biaya late-interaction hanya pada daftar pendek.
from ragatouille import RAGPretrainedModel
RAG = RAGPretrainedModel.frompretrained("colbert-ir/colbertv2.0")
Misalkan retriever Anda (FAISS, pgvector, Elasticsearch...) mengembalikan ini:
candidatepassages = [
"Coolant untuk DX-900 harus diganti setiap 2000 jam operasi.",
"Penggantian oli rutin DX-900 dijadwalkan setiap 500 jam.",
"Periode garansi turbocharger adalah 24 bulan atau 4000 jam operasi.",
"Kampas rem harus diperiksa setiap 250 jam operasi pada tambang terbuka.",
]
reranked = RAG.rerank(
query="lama garansi turbocharger",
documents=candidatepassages,
k=3,
)
for r in reranked:
print(round(r["score"], 2), "->", r["content"][:70])
rerank tidak memerlukan indeks yang dibangun sebelumnya. Ia meng-encode kandidat secara langsung dan menilainya dengan MaxSim, sehingga ideal sebagai tahap kedua siap-pakai. Jaga jumlah kandidat tetap moderat (misalnya 50-100) untuk mengendalikan latensi.
Integrasi dengan LangChain dan LlamaIndex
RAGatouille menyediakan adapter sehingga retriever-nya menyatu ke pipeline yang sudah ada.
LangChain:
# Indeks RAGatouille dapat dibungkus sebagai retriever LangChain.
retriever = RAG.aslangchainretriever(k=5)
Gunakan di mana pun retriever LangChain diharapkan, misalnya dalam sebuah chain.
docs = retriever.invoke("periode garansi turbocharger")
LlamaIndex (pakai sebagai node postprocessor / reranker atas node yang diambil):
# RAGatouille juga bisa menjadi langkah reranking atas node yang dikembalikan
oleh vector retriever LlamaIndex, menggabungkan recall murah dengan presisi ColBERT.
rerankednodes = RAG.rerank(
query="periode garansi turbocharger",
documents=[node.getcontent() for node in retrievednodes],
k=5,
)
Inti praktisnya: ColBERT bisa masuk baik sebagai retriever maupun sebagai reranker di atas tahap recall apa pun yang sudah Anda jalankan.
Fine-Tuning ColBERT pada Data Anda Sendiri
Checkpoint pretrained colbertv2.0 bersifat umum untuk bahasa Inggris. Untuk domain khusus (hukum, medis, jargon internal, teks non-Inggris), fine-tuning sering kali sepadan. RAGatouille membungkus pelatihan melalui RAGTrainer.
Menyiapkan Data Pelatihan
ColBERT dilatih pada triple (query, passagepositif, passagenegatif). Anda juga bisa memulai dari pasangan (query, positif) dan membiarkan RAGatouille menambang hard negative untuk Anda.
from ragatouille import RAGTrainer
trainer = RAGTrainer(
modelname="colbertarmada",
pretrainedmodelname="colbert-ir/colbertv2.0", # warm start
languagecode="id",
)
Pasangan (query, passagerelevan) dari data berlabel atau log klik Anda.
pairs = [
("periode garansi turbocharger",
"Periode garansi turbocharger adalah 24 bulan atau 4000 jam operasi."),
("seberapa sering mengganti coolant",
"Coolant untuk DX-900 harus diganti setiap 2000 jam operasi."),
("interval pemeriksaan kampas rem",
"Periksa kampas rem setiap 250 jam operasi pada operasi tambang terbuka."),
]
Semua passage di koleksi Anda, dipakai sebagai pool untuk menambang negatif.
fullcollection = [p for , p in pairs] + [
"Penggantian oli rutin dijadwalkan setiap 500 jam operasi.",
"Katup pelepas hidraulik diperiksa setiap triwulan.",
]
Menambang Hard Negative dan Melatih
trainer.preparetrainingdata(
raw
data=pairs,
dataoutpath="./datapelatihanarmada/",
alldocuments=fullcollection,
numnewnegatives=10, # hard negative yang ditambang per positif
minehardnegatives=True, # memakai retriever untuk menemukan distraktor sulit
)
trainer.train(
batchsize=16,
nbits=2, # bit kompresi residual untuk indeks hasil latih
maxsteps=2000,
userelu=False,
learningrate=5e-6,
)
Mengapa hard negative penting: negatif acak mudah ditolak dan mengajari model sedikit. Hard negative adalah passage yang tampak relevan padahal tidak (misalnya passage ganti oli ketika query tentang coolant). Melatih dengannya mempertajam batas keputusan tepat di tempat yang benar-benar penting. Jaga set berlabel tetap realistis; beberapa ribu triple yang baik biasanya mengalahkan set besar yang penuh noise.
Pertimbangan Penyimpanan dan Skala
Biaya late interaction ada pada penyimpanan dan waktu indexing, bukan pada logika query. Anggarkan untuk itu.
- Ukuran indeks. Bi-encoder menyimpan satu vektor per chunk. ColBERTv2 menyimpan banyak (satu per token), bahkan setelah kompresi residual. Harapkan indeks beberapa kali lebih besar dari indeks dense setara. Setting
nbits(1, 2, atau 4) menukar ukuran indeks dengan kualitas;nbits=2adalah default yang wajar. - Waktu indexing. Meng-encode setiap token lebih berat daripada meng-encode satu vektor terpool per chunk. Gunakan GPU untuk koleksi yang tidak sepele.
- Latensi query. PLAID menjaga ini tetap wajar untuk korpus sedang (ratusan ribu hingga beberapa juta passage pada satu mesin). Di atas itu, pola reranking biasanya lebih cocok.
- Titik optimal. Late interaction sepadan dengan biayanya ketika kualitas retrieval menjadi penghambat dan korpus berukuran sedang: dokumentasi teknis, pencarian hukum dan medis, basis pengetahuan support, serta domain yang penuh terminologi presisi.
Library ColBERT yang Mendasari dan Serving
RAGatouille berdiri di atas library ColBERT dari Stanford. Saat Anda butuh kendali lebih (parameter PLAID kustom, indexing terdistribusi, akses langsung ke searcher), Anda bisa turun ke ColBERT langsung; indeks RAGatouille adalah indeks ColBERT standar di disk.
Untuk serving, pola umumnya adalah memuat indeks sekali saat proses dimulai dan mengekspos search/rerank di balik API kecil. Indeks di-memory-map, sehingga query hangat tetap cepat, tetapi pemuatan pertama membayar biaya startup. Simpan satu instance model per worker dan hindari memuat ulang per request.
# Sketsa serving minimal (handler gaya FastAPI).
from ragatouille import RAGPretrainedModel
RAG = RAGPretrainedModel.fromindex(
".ragatouille/colbert/indexes/perawatanarmada"
) # muat sekali saat startup
def handlesearch(query: str, k: int = 5):
return RAG.search(query=query, k=k)
Praktik Terbaik
- Mulai dengan reranking. Sebelum berkomitmen pada indeks ColBERT penuh, coba ColBERT sebagai reranker atas retriever Anda yang ada. Ini cara termurah mengukur peningkatan kualitas pada data Anda.
- Tuning panjang chunk, bukan hanya
k. Penyimpanan level-token membuat ukuran chunk menjadi pengungkit nyata bagi kualitas maupun ukuran indeks. Uji chunk 180, 256, dan 300 token. - Jaga
documentidtetap stabil. Petakan tiap chunk kembali ke record sumber agar bisa deduplikasi dan mengutip. - Ukur pada query Anda sendiri. Gunakan set query nyata yang disisihkan dengan jawaban yang diketahui, dan bandingkan ColBERT dengan dense retriever Anda saat ini sebelum migrasi.
- Pilih
nbitssecara sengaja. Pakainbits=2sebagai baseline; turun kenbits=1hanya jika penyimpanan sangat ketat dan Anda sudah memverifikasi penurunan kualitasnya dapat diterima. - Fine-tune untuk domain padat jargon atau non-Inggris. Checkpoint umum adalah awal yang kuat, tetapi triple domain dengan hard negative yang ditambang menutup sisa kesenjangan.
Kapan TIDAK Menggunakan Late Interaction
ColBERT bukan default yang tepat untuk setiap sistem.
- Korpus sangat kecil. Dengan beberapa ratus passage, bi-encoder plus reranker cross-encoder lebih sederhana dan selisih kualitasnya marginal.
- Anggaran penyimpanan ketat. Jika ukuran indeks adalah kendala utama, penyimpanan yang berlipat dari late interaction mungkin tidak terjustifikasi.
- Retrieval skala sangat besar dengan latensi ultra-rendah. Bi-encoder yang ter-tuning baik dengan pencarian ANN masih unggul pada throughput mentah di skala miliaran vektor.
- Anda sudah memenuhi standar kualitas. Jika dense retriever Anda sudah memenuhi metrik evaluasi, kompleksitas tambahan tidak sepadan.
Kesimpulan dan Poin Utama
Retrieval late-interaction mengatasi kelemahan konkret dense embedding satu-vektor: information bottleneck yang mengubur sinyal presisi level-token. ColBERT mempertahankan embedding per token dan menilai dengan MaxSim, mencapai kualitas mendekati cross-encoder sambil tetap bisa diindeks, dan ColBERTv2 plus PLAID membuatnya praktis melalui kompresi residual serta pemangkasan kandidat yang agresif.
Poin utama:
- Bi-encoder cepat dan murah tapi lossy; cross-encoder akurat tapi tidak bisa memindai korpus; late interaction berada di antaranya, mempertahankan detail token sambil tetap bisa diindeks.
- RAGatouille adalah jalur masuk yang mudah:
frompretrained,index,search, danrerankmencakup sebagian besar kebutuhan tanpa menyentuh internal ColBERT. - Gunakan ColBERT sebagai reranker terlebih dahulu untuk mengukur peningkatan secara murah, lalu naik ke indeks penuh jika kualitasnya menjustifikasi penyimpanan.
- Fine-tuning dengan
RAGTrainerdan hard negative yang ditambang menutup kesenjangan untuk domain khusus atau non-Inggris. - Rencanakan untuk indeks yang lebih besar dan waktu indexing lebih lama; imbalannya adalah kualitas retrieval pada korpus berukuran sedang yang kaya terminologi.