Seorang pengguna melaporkan bahwa plugin terjemahan kami mengembalikan hasil cache yang sama untuk setiap permintaan, terlepas dari inputnya. Kami menyelidiki dan menemukan sesuatu yang lebih buruk: 95% dari semua hit Semantic Cache di seluruh platform kami adalah false positive. 199 permintaan terjemahan yang berbeda, 198 body permintaan yang unik, satu respons yang di-cache disajikan untuk semuanya.
Jika Anda peduli dengan state agent yang berumur panjang dan penanganan permintaan produksi, postingan ini sangat cocok dipadukan dengan Mengapa AI Agent Anda Terus Kehilangan Memorinya, panduan chatbot satu API key, dan panduan rate limiting AI API.
Laporan Bug
Laporannya sederhana: "Saya menonaktifkan Semantic Cache, tetapi setiap terjemahan mengembalikan hasil yang sama."
Tiga ID permintaan, tiga segmen terjemahan yang berbeda, respons cache yang identik. Body permintaan berkisar antara 1.564 hingga 8.676 byte. ID respons yang di-cache sama di semuanya: chatcmpl-DG6J03nhdvcF7Ek0C8rJkjh7lN9pF.
Kecurigaan pertama: pengaturan cache pengguna tidak diterapkan. Itu ternyata merupakan bug sinkronisasi data-source yang terpisah (panel admin menulis ke satu tabel, API gateway membaca dari tabel lain). Namun, memperbaiki hal itu hanya menyelesaikan setengah masalah. Bahkan dengan cache yang diaktifkan dan berfungsi dengan benar, Semantic Cache mencocokkan permintaan yang seharusnya tidak pernah cocok.
Data Produksi
Kami mengambil data cache hit selama 24 jam dari ClickHouse. Angkanya buruk.
| Model | Total Permintaan | Cache Hit | Permintaan Unik | Respons Unik | Hit Rate |
|---|---|---|---|---|---|
| gpt-4.1-nano | 200 | 199 | 198 | 1 | 99.5% |
| glm-4.6-thinking | 100 | 38 | 13 | 1 | 38% |
| gpt-5-nano | 31 | 29 | 28 | 2 | 93.5% |
| gpt-oss-120b | 18 | 17 | 17 | 1 | 94.4% |
| qwen3-vl-flash | 17 | 16 | 16 | 1 | 94.1% |
198 permintaan terjemahan unik, semuanya mengembalikan satu respons cache yang sama. Itu bukan cache. Itu adalah fungsi rusak yang mengembalikan nilai konstan.
Setiap model yang terdampak memiliki dua kesamaan: semua permintaan berasal dari satu pengguna, dan semuanya menggunakan template system prompt yang tetap dengan konten pengguna yang bervariasi.
Mengapa Embedding Gagal pada Input Terstruktur
Plugin terjemahan mengirimkan permintaan seperti ini:
System: "Act as a translation API. Output a single raw JSON object only.
Input: {"targetLanguage":"<lang>","title":"...","segments":[...]}"
User: {"targetLanguage":"zh","title":"Product Page",
"description":"Translate product descriptions",
"tone":"formal",
"segments":[{"text":"actual varying content here"}]}
System prompt identik di semua permintaan. User message adalah objek JSON di mana targetLanguage, title, description, dan tone bersifat tetap. Hanya segments[].text yang berubah.
Ketika Semantic Cache kami mengekstrak teks untuk embedding, ia menggabungkan system prompt dan user message. Template yang tetap menyumbang sekitar 80% dari teks tersebut. Model embedding (all-mpnet-base-v2, 768 dimensi) mengompresi ini menjadi vektor di mana struktur template mendominasi. Konten terjemahan yang sebenarnya hampir tidak memberikan pengaruh signifikan.
Hasilnya: cosine similarity antara "terjemahkan 'Hello world'" dan "terjemahkan 'Laporan keuangan kuartalan menunjukkan kenaikan pendapatan sebesar 15%'" melebihi 0.95. Threshold kami adalah 0.95. Setiap permintaan terjemahan cocok dengan entri cache pertama.
Menelusuri log, kami menemukan tiga cara masalah ini terjadi:
Plugin terjemahan adalah pelanggar terburuk. Key dan value JSON yang tetap menenggelamkan segmen terjemahan yang sebenarnya. gpt-4.1-nano dan gpt-5-nano keduanya mengalami hal ini.
Asisten peringkasan konteks memiliki variasi masalah yang sama. System prompt-nya sangat panjang sehingga konten pengguna (berkisar antara 5KB hingga 47KB) hampir tidak terdaftar dalam embedding. Itulah sebabnya glm-4.6-thinking akhirnya mengembalikan ringkasan yang sama untuk setiap percakapan.
Pola ketiga lebih halus. Untuk gpt-oss-120b dan qwen3-vl-flash, 500 karakter pertama dari setiap permintaan identik byte-demi-byte. Konten yang bervariasi muncul setelahnya, tetapi embedding sudah didominasi oleh awalan yang sama.
Apa yang Dikatakan oleh Riset
Ini bukan masalah baru. Makalah terbaru telah mengukurnya.
Proyek vCache dari UC Berkeley menemukan bahwa cache hit yang benar dan salah memiliki "distribusi kemiripan yang sangat tumpang tindih." Threshold optimal bervariasi dari 0.71 hingga 1.0 di berbagai entri cache yang berbeda. Tidak ada satu angka yang berhasil untuk semuanya. Solusi mereka: mempelajari threshold terpisah per entri cache, yang memangkas tingkat kesalahan sebesar 6x lipat sambil menggandakan hit rate. (vCache, 2025)
Masalah menjadi lebih buruk ketika Anda mencampur jenis kueri. Sebuah studi caching yang sadar kategori menunjukkan bahwa threshold 0.80 menghasilkan 15% kecocokan palsu pada kueri kode (sort_ascending vs sort_descending), sementara threshold yang sama melewatkan parafrase yang valid dalam kueri percakapan. Satu threshold, dua mode kegagalan. (Category-Aware Semantic Caching, 2025)
Bank juga mengalami hal ini. Sebuah studi kasus InfoQ mendokumentasikan sistem RAG di mana "Bisakah saya menunda pembayaran pinjaman bulan ini" cocok dengan "Apa yang terjadi jika saya melewatkan pembayaran pinjaman" pada kemiripan 88.7%. Niat berbeda, jawaban cache sama. Mereka mulai dengan tingkat false positive 99% dan membutuhkan empat putaran optimasi untuk turun menjadi 3.8%. (InfoQ Banking Case Study, 2025)
Masalah yang lebih dalam: embedding mengukur apakah dua prompt secara semantik mirip, bukan apakah respons yang sama dapat menjawab keduanya. Celah itulah tempat false cache hit berada. (Efficient Prompt Caching via Embedding Similarity, 2024)
Setiap makalah yang kami temukan setuju pada satu hal: kemiripan embedding saja tidak cukup. Anda memerlukan lapisan verifikasi.
Solusi Dua Lapis
Kami membangun dua pertahanan. Yang pertama menghilangkan noise template sebelum embedding. Yang kedua memverifikasi hit setelah pencocokan.
Lapis 2: Ekstraksi Konten untuk Embedding
Sebelum menghasilkan embedding, kami sekarang mendeteksi input terstruktur (JSON) dan hanya mengekstrak konten variabel yang bermakna.
Logikanya:
- Periksa apakah konten pesan dimulai dengan
{atau[ - Jika dapat di-parse sebagai JSON, kumpulkan semua nilai string leaf secara rekursif
- Filter nilai yang pendek (20 karakter atau kurang) karena biasanya itu adalah field konfigurasi seperti
"zh","formal", atau"Product Page" - Jika teks yang diekstrak terlalu pendek atau kosong, kembali ke teks asli
function extractContentForEmbedding(text: string): string {
const extracted = tryExtractJsonContent(text);
return extracted && extracted.length > 20 ? extracted : text;
}
Ini berlaku untuk system prompt dan user message. Untuk plugin terjemahan, embedding sekarang mewakili "Hello world" alih-alih blob JSON sebesar 2KB. Untuk asisten peringkasan, ia menarik percakapan aktual keluar dari pembungkus template.
Threshold 20 karakter dipilih secara empiris:
"zh"(2 karakter): difilter. Nilai konfigurasi."formal"(6 karakter): difilter. Nilai konfigurasi."Product Page"(12 karakter): difilter. Field template."Translate product descriptions"(31 karakter): dipertahankan. Konten bermakna."The quarterly financial report..."(40+ karakter): dipertahankan. Konten terjemahan aktual.
Lapis 3: Verifikasi Fingerprint
Setelah Semantic Cache hit, kami membandingkan hash dari teks yang diekstrak dari permintaan saat ini dengan hash yang disimpan dalam entri cache. Jika tidak cocok, hit ditolak.
// Saat penulisan cache
entry.metadata.textHash = fnv1aHash(extractedText);
// Saat pembacaan cache, setelah menemukan kecocokan kemiripan
if (entry.metadata.textHash !== undefined) {
if (entry.metadata.textHash !== fnv1aHash(currentExtractedText)) {
// False positive: mirip secara semantik tetapi konten berbeda
metrics.recordFingerprintRejection();
return null;
}
}
Hash menggunakan teks yang diekstrak (setelah Lapis 2), bukan input mentah. Dua permintaan dengan pembungkus template berbeda tetapi konten aktual identik tetap akan cocok. Konten berbeda, hash berbeda, ditolak.
Entri cache lama tanpa textHash akan melewati verifikasi (kompatibel ke belakang). Mereka akan kedaluwarsa secara alami melalui TTL.
Kami menggunakan FNV-1a (32-bit) untuk hash. Cepat, deterministik, dan tingkat tabrakan ~1 dalam 4 miliar sudah cukup baik untuk memeriksa satu cache hit.
Mengapa Tidak Menaikkan Threshold Saja?
Threshold kami sudah 0.95. Menaikkannya tidak membantu. Masalahnya adalah input yang secara struktur mirip menghasilkan skor kemiripan di atas 0.95 tidak peduli apa pun isi konten sebenarnya.
Data vCache mendukung hal ini: distribusi kemiripan dari hit yang benar dan salah sangat tumpang tindih sehingga tidak ada satu titik potong yang dapat memisahkan keduanya. Dorong threshold ke 0.99 dan Anda akan mematikan cache hit yang sah untuk parafrase tanpa menghilangkan false positive dari permintaan yang berat di template.
Perbaiki input, verifikasi output. Jangan mengutak-atik threshold.
Hasil
Dengan kedua lapisan diterapkan:
| Metrik | Sebelum | Sesudah |
|---|---|---|
| False positive gpt-4.1-nano | 198/199 | 0 |
| Pangsa false positive dari semua cache hit | ~95% | <5% |
| Rate cache hit yang sah | Tidak berubah | Tidak berubah |
| Latensi tambahan per permintaan | 0 | <1ms (JSON parse + FNV hash) |
Lapis 2 saja sudah cukup untuk memperbaiki plugin terjemahan. Lapis 3 adalah jaring pengaman untuk kasus di mana ekstraksi JSON tidak sepenuhnya memisahkan konten, atau untuk input terstruktur yang bukan JSON.
Kesimpulan Utama
Jika Anda menjalankan Semantic Cache di produksi:
Pantau keragaman respons. Jika sebuah model memiliki 100% cache hit rate dan hanya 1 respons unik, Anda memiliki masalah false positive. Kueri:
SELECT model, uniqExact(substring(response_body, 1, 200)) as unique_responses, count() as total FROM request_logs WHERE cache_hit = true GROUP BY model.Input terstruktur merusak embedding naif. Setiap permintaan dengan template tetap (JSON API, pembungkus system prompt, tugas pengisian formulir) akan menghasilkan skor kemiripan yang tinggi secara artifisial. Lakukan prapemrosesan sebelum embedding.
Lapisan verifikasi bukanlah pilihan. Setiap Semantic Cache produksi dalam literatur riset memilikinya. Pertanyaannya adalah apakah Anda menggunakan pemeriksaan hash yang ringan, cross-encoder reranker, atau panggilan verifikasi LLM penuh. Pilih berdasarkan anggaran latensi Anda.
Threshold global adalah kompromi, bukan solusi. Jenis kueri yang berbeda membutuhkan threshold yang berbeda. Jika Anda tidak dapat melakukan threshold per kategori atau per entri, setidaknya tambahkan prapemrosesan input untuk menormalkan kualitas embedding di seluruh kategori.
Semantic caching dapat memangkas 30-70% biaya LLM API. Namun tanpa prapemrosesan input dan verifikasi hit, Anda hanya menyajikan jawaban basi dan menyebutnya sebagai kemenangan performa.
LemonData menyediakan akses terpadu ke 300+ model AI dengan caching bawaan, routing, dan optimasi biaya. Coba gratis dengan kredit $1.
