Dua Eksperimen, Hasil Identik: Index Vektor yang Basi
Ketika Angka yang Sama Persis Justru Jadi Tanda Bahaya
Ada satu insting yang saya pelajari ulang dengan cara yang menyakitkan minggu ini: kalau saya mengubah kode, lalu menjalankan benchmark dua kali, dan hasilnya identik sampai desimal terakhir — itu bukan berita bagus. Itu alarm. Dalam satu sesi kerja di proyek medcoder, pola ini muncul dua kali dengan penyebab yang berbeda. Salah satunya cukup instruktif untuk saya tulis ulang di sini, karena dua ronde pengukuran penuh terbuang percuma sebelum saya sadar apa yang sebenarnya terjadi.
Data Dihapus, Tapi Indeksnya Tidak Ikut Dihapus
Sistem medcoder memakai retrieval few-shot: saat model diminta mengusulkan kode ICD-10/ICD-9-CM dari sebuah catatan SOAP, ia mencari contoh-contoh kasus lama yang mirip lewat pencarian vektor, lalu menyelipkan contoh itu ke prompt sebagai referensi. Supaya pencarian ini cepat, contoh-contoh kasus disimpan dua kali: sekali di database utama (sumber kebenaran), sekali lagi di index vektor terpisah yang sudah di-embed sebelumnya.
Ceritanya begini. Saya perlu membuang sejumlah contoh kasus yang sudah tidak relevan — saya jalankan lewat DELETE SQL langsung ke database utama. Rapi, cepat, selesai. Yang saya lupa: index vektornya adalah struktur terpisah, dan tidak ada mekanisme apa pun yang membuatnya ikut terhapus saat baris di database hilang. Jadi contoh-contoh kasus yang saya kira sudah "dicabut" itu sebenarnya masih hidup penuh di index, dan sistem terus memakainya sebagai referensi retrieval — tanpa siapa pun, termasuk saya, menyadarinya.
Yang membuat ini licin untuk terdeteksi: tidak ada error, tidak ada anomali yang mencolok. Hasil retrieval tetap tampak wajar, kode yang diusulkan tetap masuk akal secara klinis. Saya baru curiga justru karena saya membandingkan dua ronde benchmark yang seharusnya berbeda — ada perubahan kode nyata di antara keduanya — tapi angka yang keluar sama persis. Saat itulah saya mulai menelusuri ke belakang, dan menemukan bahwa perubahan yang saya buat sama sekali tidak pernah tersentuh oleh jalur eksekusi yang diukur, karena retrieval-nya masih menunjuk ke index lama yang isinya sudah tidak sinkron dengan database.
Pemeriksa Otomatis yang Langsung Menemukan Kasus Kedua
Setelah tahu akar masalahnya, saya bikin skrip pemeriksa sinkronisasi yang cukup sederhana: bandingkan set ID yang ada di index vektor dengan set ID yang ada di database. Dua arah — apakah semua ID di index masih benar-benar ada di database, dan apakah semua ID di database sudah punya representasi di index. Kalau ada selisih di salah satu arah, itu tandanya kedua penyimpanan sudah drift.
Yang menarik: begitu pemeriksa ini saya pasang dan jalankan pertama kali, ia langsung menemukan satu kasus serupa lain — sisa index yang sudah "hidup" di lingkungan produksi selama berjam-jam tanpa siapa pun sadar. Tidak butuh menunggu insiden besar untuk membuktikan alat ini berguna; hari pertama dipasang, ia sudah menangkap sesuatu yang nyata.
Pelajaran yang Saya Bawa Pulang
Poin metodologisnya lebih luas dari sekadar bug index-database ini. Kapan pun sebuah sistem punya lebih dari satu tempat penyimpanan yang harus selalu sinkron — sumber data dan turunannya, entah itu index pencarian, cache, atau salinan hasil pra-proses — jangan pernah asumsikan keduanya otomatis konsisten. Setiap operasi hapus atau ubah di satu sisi wajib punya jaminan eksplisit bahwa sisi turunannya ikut berubah, bukan sekadar diharapkan begitu.
Dan yang paling saya pegang sekarang: kalau saya mengukur dua kondisi yang seharusnya berbeda, lalu hasilnya identik sampai titik desimal terakhir, saya tidak lagi menganggap itu bukti bahwa perubahan saya "netral" terhadap hasil. Saya anggap itu bukti bahwa perubahan saya kemungkinan besar tidak pernah benar-benar dieksekusi oleh jalur yang saya ukur — dan saya cari dulu di mana jalurnya "bocor" sebelum percaya pada angkanya.