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.
Mengapa prompt butuh gate yang sama seperti kode
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.
- Perubahan prompt, kenaikan versi model, serta perubahan konfigurasi retrieval atau tool semuanya tetap tergolong deploy, meskipun tidak ada kode aplikasi yang berubah.
- Tanpa dataset dan ambang batas yang tetap, regresi kualitas dinilai berdasarkan perasaan, dan perasaan tidak muncul di diff pull request.
- Penyedia model yang diam-diam memperbarui model di balik alias API yang sama bisa mengubah perilaku tanpa satu pun perubahan kode di sisi Anda.
- Memperlakukan evals sebagai gate CI mengubah pertanyaan "apakah ini masih berfungsi" dari debat menjadi angka yang bisa disepakati semua orang.
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.
Membangun golden dataset
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.
Dua strategi penilaian, dan kapan memakai masing-masing
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.
Menetapkan ambang pass rate yang benar-benar berarti
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:
- Jalankan seluruh golden set terhadap prompt produksi saat ini dan catat pass rate-nya sebagai baseline.
- Tetapkan ambang batas CI sedikit di bawah baseline itu, menyisakan ruang untuk noise yang melekat pada setiap panggilan model, alih-alih menuntut seratus persen sempurna yang bisa gagal karena hal yang tidak berkaitan.
- Tetapkan ulang baseline secara sengaja, lewat pull request yang direview, setiap kali Anda dengan sadar menerima kompromi baru, sehingga ambang batas selalu mencerminkan keputusan yang benar-benar diambil seseorang.
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.
Menghubungkan gate ke CI
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.
Apa yang tertangkap oleh ini yang terlewat oleh pengujian manual
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.
- Pembaruan diam-diam dari penyedia model di balik alias API yang stabil, tertangkap saat CI berikutnya berjalan meskipun tidak ada perubahan kode di sisi Anda.
- Perubahan prompt yang memperbaiki kasus umum sambil diam-diam merusak kategori yang kurang umum namun tetap penting, langsung terlihat sebagai penurunan pass rate per kategori.
- Drift yang muncul dari perubahan hulu pada konteks yang diambil atau keluaran tool, karena golden set menguji seluruh pipeline, bukan teks prompt saja secara terisolasi.
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.

