Constrained Decoding: Ubah LLM Jadi Model Keputusan Cepat
Constrained decoding mengubah LLM jadi classifier cepat. Pelajari cara logit masking, kalibrasi, dan temperature scaling agar model keputusan bekerja andal.

Ada trik murah yang diam-diam sangat berguna: jika kamu hanya peduli token pertama yang akan dikeluarkan LLM, kamu bisa mengajukan pertanyaan pilihan ganda dan mendapat jawaban dalam satu forward pass. Constrained decoding — menutup setiap token di vocabulary kecuali segelintir yang bersedia kamu terima — mengubah model generatif menjadi sesuatu yang sangat mirip classifier. Tanpa parsing JSON, tanpa loop retry, tanpa sebelas langkah autoregressive untuk menghasilkan sebelas karakter. Satu pass, satu softmax, satu jawaban dengan probabilitas untuk setiap opsi.
Ide ini sudah lama beredar dengan nama seperti "decision models" atau inferensi "system one", dan baru-baru ini viral di Hacker News lewat tutorial yang menunjukkan cara membangunnya dari model Qwen 1,7B parameter dalam sekitar empat puluh baris Python. Para veteran machine learning di kolom komentar, seperti yang sudah bisa ditebak, langsung protes: itu kan classifier, kita sudah punya itu sejak perceptron. Mereka benar, dan sedikit melewatkan poinnya. Yang baru bukan konsepnya — melainkan kenyataan bahwa kamu mendapatkan classifier zero-shot dari language model serbaguna tanpa melatih apa pun. Pertanyaan teknis yang menarik adalah kapan pendekatan terbatas ini mengungguli membiarkan model bicara bebas, dan kapan ia diam-diam membohongimu dengan probabilitas yang overconfident.
Dua Cara Mendapatkan Jawaban dari LLM
Mode default saat berbicara dengan language model adalah generasi. Kamu mengajukan pertanyaan, model mengeluarkan token satu per satu, lalu di tahap berikutnya kamu mem-parsing apa pun yang keluar. Jika butuh output terstruktur, kamu menambahkan skema: JSON mode, grammar-constrained sampling, mesin finite-state gaya outlines. Cara ini berhasil, tetapi model tetap berjalan token demi token melewati seluruh respons. Jawaban pilihan ganda yang sederhana bisa memakan sebelas langkah decoding, dan setiap langkah membutuhkan forward pass penuh atas miliaran parameter.
Alternatifnya adalah tidak pernah membiarkannya berjalan sama sekali. Setelah prompt diproses, kamu melihat logits atas vocabulary pada posisi terakhir, membuang semuanya kecuali token ID yang sesuai dengan opsimu — misalnya "A", "B", "C", "D", "E" — lalu melakukan softmax hanya pada opsi-opsi itu. Argmax adalah prediksimu; nilai softmax adalah skornya. Total biayanya: satu forward pass, sama dengan prefill yang memang sudah kamu bayar. Ini intinya, diadaptasi dari pendekatan berbasis Qwen yang sedang ramai dibahas:
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
model_name = "Qwen/Qwen3-1.7B"
options = ["A", "B", "C", "D", "E"]
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForCausalLM.from_pretrained(
model_name, torch_dtype="auto", device_map="auto"
)
# First token the model would emit for each option
option_token_ids = [
tokenizer.encode(opt, add_special_tokens=False)[0] for opt in options
]
prompt = (
"What color is the sky?\n"
"A. Red\nB. Blue\nC. Green\nD. Purple\nE. I don't know\nAnswer:"
)
messages = [{"role": "user", "content": prompt}]
text = tokenizer.apply_chat_template(
messages, tokenize=False, add_generation_prompt=True,
enable_thinking=False
)
inputs = tokenizer(text, return_tensors="pt").to(model.device)
with torch.no_grad():
logits = model(**inputs).logits[0, -1]
# Constrained decoding: softmax over only the option tokens
probs = torch.softmax(logits[option_token_ids].float(), dim=-1)
print(options[probs.argmax().item()]) # -> B
Itu saja mesinnya. Vocabulary memiliki lebih dari 150.000 token dan kita telah mereduksi keputusan menjadi lima angka. Tidak ada permainan temperature sampling saat generasi, tidak ada parser yang bisa gagal, dan tidak ada cara bagi model untuk berhalusinasi memilih opsi yang tidak ada dalam daftar. Ruang output tertutup secara konstruksi.
Ini Classifier, dan Itu Tidak Masalah
Mari beri para skeptis haknya. Yang kita bangun adalah classifier diskriminatif atas label set tetap — keturunan ide yang kembali ke perceptron Rosenblatt di tahun 1950-an, melalui regresi logistik, hingga setiap neural net berkepala softmax di era deep learning. Jika dipikir-pikir, constrained decoding hanyalah linear readout atas hidden state terakhir, yang memang selalu menjadi bentuk classification head. MLE yang bertahun-tahun memohon timnya untuk sekadar melatih classifier wajar saja merasa sedikit gemas melihat ini dikemas ulang sebagai "decision model".
Tapi sejarah juga menunjukkan mengapa versi baru ini penting. Classifier lama itu sempit: kamu mengumpulkan data berlabel, melatih, dan mendapat model yang hanya tahu satu tugas dan tidak ada yang lain. Alasan sistem berbasis LLM terus memakan tugas yang "seharusnya" memakai classifier khusus adalah sifat zero-shot-nya — base model sudah menyerap cukup banyak dunia sehingga sebuah prompt adalah data latihannya. Ketika saya menguji setup seperti ini pada holdout CommonsenseQA, model 1,7B mencapai sekitar 59% akurasi tanpa fine-tuning sama sekali, naik ke sekitar 62% setelah tuning singkat pada split training. Memang bukan state of the art, tetapi biayanya hanya satu sore dan tanpa data berlabel milik sendiri. Classifier khusus mungkin akan lebih baik; tapi ia membutuhkan pipeline, dataset, dan rencana retraining setiap kali label berubah.
Ada kerangka berguna di sini yang meminjam dari perdebatan lama generatif versus diskriminatif. Classifier generatif memodelkan seluruh distribusi, boros tapi fleksibel; yang diskriminatif memodelkan batas keputusan, efisien tapi kaku. Constrained decoding adalah hibrida yang aneh: model generatif yang dipaksa melayani peran diskriminatif saat inferensi. Kamu mendapat efisiensi readout diskriminatif dengan keluasan pretraining generatif. Kombinasi ini sebenarnya belum tersedia sebelumnya, meskipun komponennya sudah sangat tua.
Di Mana Constrained Decoding Unggul
- Latensi. Satu forward pass dibanding N langkah autoregressive. Pada model kecil, ini adalah perbedaan antara 'cukup cepat untuk jalur request' dan 'perlu antrean'. Praktisi yang menjalankan decision model murni di browser melaporkan respons di bawah 200ms — coba lakukan itu dengan JSON generatif.
- Kebenaran struktural. Model benar-benar tidak bisa menghasilkan output di luar set yang diizinkan. Tidak ada JSON rusak, tidak ada ocehan 'Jawabannya mungkin B karena...', dan tidak perlu lapisan guardrail untuk menangkap pelanggaran format.
- Throughput. Karena setiap request adalah satu pass dengan bentuk yang identik, batching menjadi sepele dan dapat diprediksi. Generasi jawaban dengan panjang variabel merusak efisiensi batching.
- Skor per opsi. Kamu mendapat seluruh distribusi, bukan hanya pemenangnya. Ini membuka pintu bagi logika abstensi: jika probabilitas teratas di bawah ambang, arahkan ke manusia atau model yang lebih besar.
Poin abstensi ini layak ditekankan. Jawaban generatif adalah satu artefak yang kamu percaya atau tidak. Distribusi probabilitas atas opsi memungkinkanmu membangun routing: kasus dengan keyakinan tinggi lolos otomatis, yang keyakinannya rendah dieskalasi. Ini adalah pola di balik banyak sistem triage produksi — routing tiket support, pra-screening moderasi konten, deteksi intent — dan di situlah teknik ini membuktikan nilainya. Jika tugasmu secara alami bisa diuraikan menjadi "pilih salah satu dari K label, dan beri tahu seberapa yakin kamu", constrained decoding hampir pasti alat yang tepat.
Di Mana Output Generatif Unggul
Sekarang sisi sebaliknya. Begitu tugasmu tidak cocok dengan label set tetap, constrained decoding mulai runtuh. Jika jawabannya berupa entitas bebas, angka, potongan kode, atau apa pun yang bersifat komposisional, kamu butuh generasi — mungkin dengan batasan output terstruktur, tetapi tetap generasi. Ada juga kerugian yang lebih halus: penalaran. Ketika model menghasilkan chain of thought sebelum menjawab, performanya sering jauh lebih baik pada pertanyaan sulit. Single-pass decision head tidak memberimu scratchpad. Kamu meminta pemikiran sistem satu, cepat dan intuitif, dan itulah yang kamu dapatkan — termasuk kegagalan khasnya.
Ada juga masalah sensitivitas terhadap redaksi. Classifier terbatas atas "A/B/C/D/E" sebenarnya mengukur preferensi model terhadap teks setiap opsi di posisi tersebut. Ubah sedikit redaksi opsi C, urutkan ulang daftarnya, atau ganti "Answer:" menjadi "The best answer is", dan skornya bisa bergeser. Jawaban generatif dengan penalaran cenderung lebih tahan terhadap perturbasi permukaan karena model harus berkomitmen pada konten, bukan hanya pada sebuah token. Jika kamu mengevaluasi salah satu pendekatan, coba perturbasi presentasinya dan lihat apa yang rusak — ini tes robustness yang murah dan tidak nyaman.

Masalah Kalibrasi: Skor Kepercayaanmu Berbohong
Inilah jebakan yang menjerat siapa pun yang membangun sistem seperti ini. Kamu mendapat probabilitas dari softmax, jadi itu pasti probabilitas, kan? Bukan. Itu adalah tingkat keyakinan model bahwa sebuah token akan muncul berikutnya, yang merupakan pernyataan tentang bahasa, bukan tentang kebenaran. Tanyakan pada model "Di mana kamu paling mungkin menemukan kelelawar?" dengan opsi termasuk "Gua" dan "Pertandingan bisbol", dan ia akan memberi sekitar 0,998 untuk "Gua" — pertanyaan ambigu tanpa jawaban yang bisa dibenarkan secara pasti, dijawab dengan kepastian hampir penuh.
Kalau prediksi dikelompokkan berdasarkan confidence pada eval nyata, gambarannya makin buruk. Dalam satu run CommonsenseQA, bucket confidence 0,9–1,0 hanya benar sekitar 70% kali, dan bucket 0,8–0,9 nyaris tidak mencapai 40%. Model yang terkalibrasi dengan baik seharusnya benar sekitar 90% saat mengatakan 0,9. Model ini overconfident secara sistematis — yang, jika dipikir-pikir, mencerminkan pengamatan lama bahwa deep net modern umumnya overconfident. Guo et al. menunjukkan pada 2017 bahwa output softmax ResNet biasa sangat tidak terkalibrasi dibanding jaringan dangkal era 1990-an. Semua yang lama kembali lagi; kita hanya menemukan ulang masalahnya pada skala 1,7 miliar parameter.
Kabar baiknya, solusinya juga lama dan hampir memalukan karena sangat sederhana: temperature scaling. Bagi logits dengan satu skalar T yang dipelajari sebelum softmax:
def scaled_probs(logits, option_ids, temperature):
selected = logits[option_ids].float() / temperature
return torch.softmax(selected, dim=-1)
# Fit T on a validation set by minimizing negative log-likelihood
# of the correct option. For the CommonsenseQA run above, T ~= 3.8
temperature = 3.7973
probs = scaled_probs(logits, option_token_ids, temperature)
T lebih besar dari 1 meratakan distribusi; T lebih kecil dari 1 mempertajamnya. Menyesuaikan satu angka terhadap held-out set mengubah tabel kalibrasi yang menyedihkan itu menjadi sesuatu yang jujur: bucket 0,9–1,0 kini berada di sekitar 95% akurasi, bucket 0,5–0,6 sekitar 55%. Akurasinya sama sekali tidak berubah — argmax invarian terhadap scaling monotonik — tetapi skornya kini bermakna seperti yang kamu harapkan. Jika kamu berniat melakukan routing berdasarkan probabilitas ini, kalibrasi dulu atau jangan repot mengumpulkannya.
Apakah Menyetel Temperature pada Benchmark Itu Curang?
Pertanyaan wajar yang muncul dalam diskusi: bukankah menyesuaikan T agar model tampak terkalibrasi pada benchmark itu agak p-hacking? Memang akan begitu, jika kamu menyesuaikannya pada test set. Dilakukan dengan benar — sesuaikan T pada validation split, laporkan kalibrasi pada held-out test split — ini hanyalah regresi satu parameter, dan merupakan standar praktik dalam literatur justru karena alasan itu. Disiplin yang penting sama dengan di mana pun dalam ML: jaga split-mu tetap jujur, dan curigai klaim kalibrasi apa pun yang diukur pada data yang pernah tersentuh proses fitting. Jika domain deployment-mu bergeser dari domain eval, T-mu juga ikut bergeser, jadi recalibration perlu masuk ke loop maintenance bersama hal-hal lainnya.
Ini sebenarnya contoh dari penyakit yang lebih luas: jurang antara apa yang secara spesifikasi dimaksudkan oleh sistem dan apa yang sebenarnya dihitungnya, jurang yang menurut saya pernah saya bahas sebagai tempat bug bersarang. "Confidence" dispesifikasikan sebagai probabilitas kebenaran; implementasinya memberimu plausibilitas token berikutnya. Temperature scaling adalah tambalan atas jurang itu, bukan penyelesaiannya. Ingat perbedaan ini setiap kali kamu tergoda menghubungkan output softmax ke ambang alerting.
Prosedur Keputusan yang Praktis
Ketika saya memilih antara dua mode sekarang, saya menjalankan checklist singkat:
- Apakah output-nya set label yang tetap? Jika ya, constrained decoding bisa dipertimbangkan. Jika tidak, generate.
- Apakah saya butuh penalaran untuk akurasi yang layak? Prototipe keduanya. Jika single-pass head tertinggal dari chain-of-thought lebih dari yang bisa kamu toleransi, generasi menang meski latensinya lebih tinggi.
- Apakah saya butuh skor per opsi untuk routing atau abstensi? Jika ya, constrained decoding ditambah temperature scaling hampir tanpa biaya, dan generasi tidak memberi sesuatu yang sebanding.
- Seberapa besar label set-nya? Lima opsi itu sepele; lima ribu sudah masuk wilayah retrieval. Dengan ruang label yang besar, embed label-labelmu, buat shortlist dengan vector search, dan baru kemudian gunakan model untuk memilih di antara finalis — trik scaling classifier yang sudah jadi standar sejak era sistem rekomendasi.
- Apakah label akan sering berubah? Sifat zero-shot adalah inti dari semuanya. Jika label berganti setiap minggu, classifier yang dilatih ulang adalah beban maintenance; mengedit prompt bukanlah beban.
Softmax model adalah klaim tentang token apa yang akan datang berikutnya, bukan tentang apa yang benar. Kalibrasi dulu, atau jangan percaya.
Satu pertimbangan lagi: ukuran model berinteraksi dengan pilihan ini. Single-pass head model kecil cukup murah untuk dijalankan di setiap request, bahkan di sisi klien; orang sudah mengirimkan decision model yang berjalan di browser dengan respons di bawah 200ms. Desain kaskade — model terbatas kecil untuk 80% kasus mudah, model generatif besar untuk ekor yang sulit — sering kali mengungguli kedua ekstrem dalam hal biaya dan akurasi. Ini insting yang sama di balik speculative decoding, diterapkan di level sistem alih-alih level token.
Rekomendasi
Posisi saya: jika tugasmu benar-benar keputusan pilihan tetap — routing, triage, intent, evaluasi pilihan ganda — bangun constrained decision head dan jangan menoleh lagi. Keuntungan latensinya saja sudah cukup membenarkannya, jaminan struktural menghilangkan satu kelas kegagalan parsing, dan skor per opsi memberimu logika routing yang tidak bisa disamai generasi. Tapi perlakukan softmax mentah sebagai instrumen yang belum terkalibrasi. Lakukan fine-tune pada tugas aktualmu jika kamu bisa mengumpulkan dataset meski kecil, sesuaikan temperature pada validation split yang bersih, dan verifikasi kalibrasi pada data yang tidak pernah dilihat proses fitting.
Simpan output generatif — dengan batasan output terstruktur jika memang diperlukan — untuk tugas yang benar-benar komposisional atau yang diuntungkan oleh penalaran yang terlihat. Dan jangan biarkan siapa pun menjualmu "decision model" sebagai kategori baru: ia adalah classifier yang mengenakan jas LLM, turunan dari enam dekade pemodelan diskriminatif, dan paling berguna justru ketika kamu cukup menghormati garis keturunannya untuk melakukan pekerjaan kalibrasi yang selalu ditekankan para senior. Alatnya baru. Disiplinnya tidak.


