Konteks percakapan di chat AI, tanpa session di server
Chat AI yang nyambung tanpa session server: frontend pegang riwayat di localStorage, backend susun ulang prompt dengan char-budget.
User nanya: "terus gimana cara implementasinya?" di kolom chat blog ini. Yang balas bukan jawaban, malah permintaan untuk "spesifikasikan pertanyaannya". Sok nyambung tapi sebenernya nggak pegang konteks apa-apa dari pertanyaan sebelumnya. Momen itu yang bikin saya sadar: fitur chat AI blog ini masih amnesia total tiap giliran tanya baru.
Dugaan salah: perlu session server
Angan pertama saya klise: bikin session di server, kasih ID ke tiap visitor, simpan riwayat percakapan di Redis. Begitu dipikir lagi, itu overkill buat fitur pencarian blog. Tapi ternyata dugaan itu salah bukan cuma soal effort. Dokumentasi OpenAI soal conversation state bilang eksplisit: setiap request text generation itu independen dan stateless, dan percakapan multi-turn malah diwujudkan lewat mengirim ulang pesan-pesan sebelumnya sebagai parameter request [1]. Artinya model server-side session itu nggak pernah jadi cara kerja API semacam ini. Konteks memang didesain untuk dititipkan ke klien.
Tempat penyimpanannya udah tersedia gratis: localStorage. MDN menjelaskan data di sana persisten antar sesi browser dan terikat per origin [4]. Persis yang dibutuhkan: riwayat tanya-jawab hidup di browser user, nggak pernah menyentuh server.
Frontend simpan, backend susun ulang
Alurnya jadi sederhana. Setiap ask baru, frontend mengumpulkan tiga giliran tanya-jawab terakhir yang udah selesai, lalu menyisipkannya di body request. Backend menerima array history dengan validasi Pydantic di trust boundary: maksimal 5 pasangan dan pertanyaan maksimal 500 karakter, jawaban maksimal 2000. Yang lolos validasi disusun jadi blok "Percakapan sebelumnya" di dalam prompt, dengan budget total 900 karakter dan jawaban per giliran dipangkas 300 karakter. Giliran paling lama yang nggak muat di-drop begitu saja.
# use_cases.py — potongan
history_block = _build_history_block(history or [], locale)
prompt = (
f"{system.get(locale, system['id'])}\n\n"
+ (f"{history_block}\n\n" if history_block else "")
+ "\n\n".join(blocks)
+ f"\n\n{_ASK_QUESTION_LABELS.get(locale, 'Pertanyaan')}: {query}"
)
Pola ini juga cocok dengan model request-scoped FastAPI: satu scope per request, dependensi cuma membagikan handle, bukan menyimpan state [3]. Karena API-nya stateless, nggak ada cleanup sesi, nggak ada data percakapan pribadi yang numpuk di server, dan skala dibebani ke perangkat masing-masing user. Cache jawaban yang sudah ada tetap jalan dengan pola cache-aside biasa: biarkan entry expire alami [2].
Trade-off yang saya terima
Angka tiga giliran dan budget 900 karakter itu heuristik saya sendiri, bukan rekomendasi resmi. Cukup untuk pertanyaan lanjutan yang wajar, kecil kalau user benar-benar menggali jauh ke satu topik. Trade-off lain: konteksnya hilang pas user ganti browser atau bersih-bersih data. Saya milih nerima dua-duanya, soalnya yang ditawarkan balik lebih berharga, backend yang tetap bodoh dan stateless.
Hasil akhirnya: pertanyaan lanjutan sekarang nyambung. Dan yang paling saya suka, nggak ada satu baris pun infrastruktur baru yang dibuat.
Sumber
[1] OpenAI API Docs: Conversation state
[2] Redis Cache-aside Guide
[3] FastAPI Best Practices for Dependency Injection
[4] MDN Window.localStorage