Structured Output dari LLM: Bukan Sekadar Minta JSON
Begitu LLM dipakai sebagai bagian dari pipeline otomatis — bukan sekadar chatbot yang dibaca manusia — masalah baru muncul: outputnya harus bisa diparsing oleh kode, bukan cuma "kelihatan benar" secara visual. Saya cukup sering menemui asumsi bahwa cukup menambahkan kalimat "tolong jawab dalam format JSON" di prompt, lalu di sisi aplikasi tinggal json_decode(). Di demo itu berhasil. Begitu volume permintaan naik dan variasi input makin liar, parsing mulai gagal secara acak, dan itu jauh lebih menyebalkan daripada error yang konsisten karena susah direproduksi.
Kenapa permintaan JSON polos gampang bocor
Model bahasa pada dasarnya adalah prediktor teks, bukan mesin yang "mengerti" skema data. Kalau instruksinya cuma kalimat biasa, ada beberapa kegagalan yang lumrah terjadi:
- Model menambahkan narasi sebelum/sesudah JSON, misalnya "Berikut hasilnya:" lalu baru objeknya.
- JSON dibungkus code fence markdown (
```json ... ```) yang lupa dibersihkan sebelum parsing. - Tipe data meleset — angka jadi string, field opsional kadang muncul kadang tidak, key ditulis dengan casing berbeda-beda.
- Model "berimprovisasi" menambah field yang tidak diminta karena merasa itu informatif.
Semua ini bukan bug aneh, tapi konsekuensi wajar dari sifat generatif model. Solusinya bukan menulis prompt yang lebih "sopan", tapi mengurangi ruang gerak model untuk berimprovisasi.
Teknik yang benar-benar berpengaruh
Beberapa API LLM modern sudah menyediakan fitur untuk membatasi keluaran ke skema tertentu, biasanya disebut structured output, tool use, atau function calling. Prinsipnya sama: kita mendefinisikan skema (nama field, tipe, mana yang wajib) di level API, bukan cuma di kalimat prompt, sehingga proses decoding model ikut dibatasi mengikuti skema itu. Ini jauh lebih tahan banding daripada berharap model "nurut" dari instruksi teks saja.
Kalau provider yang dipakai belum punya fitur itu, atau kita menjalankan model open-source sendiri, ada beberapa kombinasi yang membantu:
- Contoh few-shot yang konkret — tunjukkan satu-dua contoh input-output persis seperti yang diharapkan, lengkap dengan struktur field-nya. Model jauh lebih patuh meniru contoh daripada menafsirkan deskripsi abstrak.
- Skema eksplisit di prompt, misalnya definisi ala JSON Schema atau tipe data (mirip interface TypeScript), diletakkan sebagai instruksi sistem yang terpisah dari instruksi konten.
- Instruksi negatif yang jelas — "jangan tambahkan teks lain di luar objek JSON", "jangan bungkus dengan markdown" — sederhana tapi ternyata cukup efektif mengurangi kebocoran.
Jangan percaya output mentah
Sebagus apa pun tekniknya, saya tetap memperlakukan output LLM sebagai input yang tidak tepercaya, sama seperti input dari pengguna. Ada dua lapisan yang saya anggap wajib:
Pertama, validasi skema setelah parsing — cek tipe data, field wajib, rentang nilai yang masuk akal — sebelum data itu dipakai lebih jauh di sistem. Kedua, mekanisme retry dengan umpan balik: kalau validasi gagal, kirim ulang ke model beserta pesan error yang spesifik ("field tanggal harus format YYYY-MM-DD, kamu mengirim..."), bukan sekadar mengulang prompt yang sama. Model cenderung memperbaiki diri kalau diberi tahu persis di mana letak kesalahannya.
Untuk kasus yang benar-benar kritis — misalnya hasil ekstraksi yang langsung memengaruhi keputusan otomatis — saya juga menambahkan batas percobaan (misalnya maksimal dua-tiga kali retry) lalu fallback ke jalur manual atau nilai default yang aman, alih-alih membiarkan sistem menunggu tanpa batas atau memaksakan data yang belum tentu valid.
Takeaway
Structured output yang andal bukan soal kalimat prompt yang lebih persuasif, tapi soal mengurangi ruang ambiguitas: pakai fitur constrained output kalau tersedia, perkuat dengan contoh dan skema eksplisit, lalu tetap validasi dan siapkan jalur retry di sisi aplikasi. LLM boleh generatif, tapi pipeline di sekelilingnya tetap harus berperilaku deterministik.