LLM Evals di CI: Menguji Prompt Seperti Kode

Foto oleh Euphy on flickr
Golden dataset adalah kumpulan pasangan input dan keluaran yang diharapkan, bersifat tetap dan berversi, yang mewakili perilaku yang harus dipertahankan oleh sebuah prompt atau model. Biasanya dimulai dari jumlah kecil, sekitar lima puluh sampai seratus kasus yang diambil dari kegagalan produksi nyata dan kasus edge yang sudah diketahui, dan disimpan di repository agar perubahannya muncul di diff seperti fixture pengujian lainnya.
Prompt adalah logika yang dieksekusi meskipun tidak memiliki compiler untuk menangkap kesalahan, dan perubahan kata kecil bisa mengubah perilaku keluaran di seluruh distribusi input dengan cara yang tidak akan terdeteksi oleh pengecekan manual sekilas. Menjalankan golden set tetap pada setiap pull request yang menyentuh prompt mengubah regresi kualitas dari perdebatan subjektif menjadi angka pass rate objektif yang otomatis memblokir merge.
Gunakan assertion berbasis kode seperti exact match, pemeriksaan substring, atau validasi skema JSON kapan pun tugasnya memiliki jawaban yang bisa diperiksa secara objektif, karena caranya cepat dan deterministik. Simpan LLM judge untuk kualitas subjektif seperti nada bicara, kegunaan, atau kesetiaan pada sumber, dan nilai dengan model berbeda dari yang sedang diuji untuk mengurangi bias memihak diri sendiri.
Bisa, itu salah satu alasan terkuat untuk menjalankan golden set di CI secara terjadwal selain pada pull request. Jika penyedia memperbarui model di balik alias API yang stabil, jalannya eval terjadwal berikutnya akan menunjukkan penurunan pass rate meskipun tidak ada yang berubah di repository Anda, memberi sinyal dini sebelum pengguna menyadarinya.
Jalankan prompt produksi saat ini terhadap seluruh golden set satu kali untuk menetapkan pass rate baseline, lalu tetapkan ambang batas CI sedikit di bawah baseline itu agar bisa menyerap noise wajar dari panggilan model alih-alih menuntut seratus persen yang rapuh. Tetapkan ulang baseline secara sengaja lewat pull request yang direview setiap kali Anda dengan sadar menerima kompromi baru.

Foto oleh Euphy on flickr
Pertama kali perubahan prompt diam-diam merusak produksi bagi saya, tidak ada yang menyadarinya selama empat hari. Sebuah asisten triase dukungan pelanggan ditulis ulang agar terdengar lebih ramah, dan dalam prosesnya berhenti menandai keluhan penagihan sebagai keluhan penagihan secara konsisten. Perubahan itu lolos code review karena tidak ada kode untuk direview, hanya satu paragraf bahasa Inggris. Itulah masalah inti prompt: mereka adalah logika yang dieksekusi dan dirilis tanpa satu pun jaring pengaman yang kita wajibkan untuk fungsi biasa yang melakukan pekerjaan serupa.
Solusinya tidak eksotis. Ini adalah disiplin yang sama yang sudah kita terapkan pada kode aplikasi, diadaptasi untuk sistem yang keluarannya probabilistik bukan deterministik. Tulisan ini membahas cara membangun harness evaluasi yang berjalan di continuous integration: golden dataset berisi kasus representatif, kombinasi assertion berbasis kode dan penilaian LLM-as-judge, gate pass rate yang memblokir merge, serta alur kerja untuk menangkap drift sebelum pelanggan yang menemukannya lebih dulu.
Prompt adalah spesifikasi dengan batas yang kabur. Dua prompt yang terlihat hampir identik dari segi kata-kata bisa menghasilkan distribusi keluaran yang berbeda secara signifikan begitu dijalankan pada ratusan input nyata. Pengecekan manual sekilas di playground hanya menyampel sedikit kasus, dan justru kasus yang tidak sempat dicek itulah yang akhirnya rusak di produksi.
Bangun golden dataset dari kegagalan produksi nyata, bukan dari kasus edge hipotetis yang Anda ciptakan di papan tulis. Kegagalan yang sudah pernah terjadi sekali adalah kegagalan yang akan terjadi lagi.
Golden dataset adalah kumpulan pasangan input dan keluaran yang diharapkan, bersifat tetap, yang mewakili perilaku yang berkomitmen untuk Anda pertahankan. Jaga agar versi pertama tetap kecil dan fokus, sekitar lima puluh sampai seratus kasus, mencakup kategori yang benar-benar ditangani fitur Anda ditambah kasus edge yang sudah pernah menggigit: frasa ambigu, input yang tidak biasa panjangnya, input yang bersifat adversarial atau bermusuhan, dan input yang memang tidak punya jawaban bersih. Simpan sebagai JSONL berversi di repository sehingga setiap perubahan pada dataset muncul di diff, sama seperti perubahan pada fixture pengujian biasa.
# golden/support-triage.jsonl
{"input": "My invoice shows double billing for March", "expected_category": "billing", "expected_contains": ["refund", "billing team"]}
{"input": "The API returns 500 on every /v2/export call", "expected_category": "bug", "expected_contains": ["engineering", "ticket"]}
{"input": "How do I add a teammate to my workspace?", "expected_category": "how_to", "expected_contains": ["settings", "invite"]}
# eval_runner.py (simplified)
import json, subprocess
def run_case(case, model_fn):
output = model_fn(case["input"])
checks = {
"category_match": case["expected_category"] in output.lower(),
"contains_terms": all(term in output.lower() for term in case["expected_contains"]),
}
return all(checks.values()), checks
def run_suite(path, model_fn):
passed, total = 0, 0
for line in open(path):
case = json.loads(line)
ok, _ = run_case(case, model_fn)
passed += ok
total += 1
return passed / total
Setiap kasus butuh strategi assertion, bukan sekadar string yang diharapkan. Untuk tugas bergaya klasifikasi, Anda bisa memeriksa kecocokan kategori secara eksak. Untuk generasi teks terbuka, periksa substring yang wajib ada, substring yang terlarang, atau properti struktural seperti JSON yang valid. Runner di bawah memuat dataset, memanggil fungsi model yang diuji, lalu melaporkan pass rate yang dapat dibandingkan dengan ambang batas di CI.
Tidak semua keluaran bisa dinilai dengan perbandingan string, dan tidak semua keluaran memerlukan biaya panggilan model kedua untuk menilainya. Sebagian besar suite eval yang matang mencampur kedua pendekatan, dipilih per kasus bukan per suite.
| Metode penilaian | Paling cocok untuk | Kompromi |
|---|---|---|
| Assertion berbasis kode | Klasifikasi, field wajib, JSON valid, kata yang dilarang | Cepat dan deterministik, tapi rapuh terhadap penulisan ulang yang tetap valid |
| LLM-as-judge | Nada bicara, kegunaan, kesetiaan pada sumber, kualitas subjektif | Fleksibel dan bernuansa, tapi non-deterministik dan menambah biaya panggilan |
| Kesamaan embedding | Konsistensi paraphrase, drift semantik antar versi | Murah dihitung, tapi proksi yang kasar untuk kebenaran |
Saat Anda memang menggunakan LLM judge, pakai model yang berbeda dari model yang menghasilkan keluaran yang diuji, dan berikan judge rubrik dengan patokan konkret alih-alih prompt terbuka seperti "nilai respons ini". Rubrik yang menjelaskan seperti apa nilai satu dan seperti apa nilai lima menghasilkan skor yang jauh lebih konsisten antar-run dibanding skala kosong.
LLM judge yang dinilai oleh model dari keluarga yang sama dengan yang menghasilkan keluaran cenderung menilai jawaban saudaranya sendiri lebih baik. Jika anggaran memungkinkan, gunakan judge dari penyedia berbeda atau setidaknya ukuran model yang berbeda untuk mengurangi bias memihak diri sendiri itu.
Pass rate mentah baru berguna setelah Anda memutuskan apa yang dianggap dapat diterima, dan keputusan itu harus berasal dari baseline Anda saat ini, bukan dari angka bulat sembarangan. Proses yang selama ini berhasil bagi saya:
Kasus yang konsisten lolos di banyak iterasi prompt adalah kandidat untuk naik ke suite regresi yang lebih ketat dan harus tetap mendekati seratus persen, sementara kasus yang lebih baru atau lebih sulit bisa tinggal di suite terpisah yang masih aktif Anda tingkatkan.
Workflow di bawah hanya terpicu saat prompt, eval runner, atau golden dataset itu sendiri berubah, sehingga perubahan kode aplikasi rutin tidak ikut membayar job yang berat panggilan model di setiap push. Ia menjalankan suite yang dinilai secara kode dan suite yang dinilai oleh judge sebagai langkah terpisah agar kegagalan di satu suite tidak mengaburkan hasil suite lainnya.
# .github/workflows/llm-eval.yml
name: LLM Eval Gate
on:
pull_request:
paths:
- "prompts/**"
- "eval_runner.py"
- "golden/**"
jobs:
eval:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pip install -r requirements.txt
- name: Run golden set eval
run: python eval_runner.py --suite golden/support-triage.jsonl --min-pass-rate 0.92
- name: Run judge-scored eval
run: python judge_eval.py --suite golden/tone.jsonl --min-avg-score 4.0
Jadikan exit code dari job eval sebagai sumber kebenaran untuk keputusan merge, dan publikasikan hasil per kasus sebagai artifact build atau komentar PR agar reviewer bisa melihat tepat kasus mana yang mengalami regresi, bukan hanya bahwa skor agregat turun. Satu angka tanpa rincian kasus di baliknya jauh lebih sulit didiagnosis pada jam lima sore hari Jumat.
Setelah gate ini ada, perubahan prompt menjadi pull request biasa: buka, lihat job eval berjalan terhadap golden set, dan merge begitu pass rate tetap terjaga. Tidak perlu lagi menguji ulang selusin skenario secara manual di jendela chat sebelum setiap rilis.
Nilai dari suite eval otomatis bukan karena ia lebih pintar daripada engineer yang hati-hati menguji perubahan secara manual. Nilainya karena ia menjalankan kumpulan kasus tetap yang sama persis setiap kali, tanpa kelelahan, tanpa lupa kasus edge aneh dari tiga bulan lalu, dan tanpa melewatkan pengecekan karena perubahannya terlihat kecil.
Mulai dari yang kecil. Golden set berisi lima puluh kasus dengan gate pass rate sederhana sudah menangkap jauh lebih banyak regresi dibanding tidak punya suite eval sama sekali, dan itu adalah fondasi yang bisa Anda kembangkan menjadi rubrik bernilai judge dan ambang batas per kategori seiring fitur semakin matang.