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