Tampilan
8.4 Custom Scorer & CI
Tujuan bab
Setelah bab ini kamu bisa:
- Menulis scorer sendiri dengan
createScorer()dan merangkai tahapannya - Membedakan function step dan prompt object step, serta tahu mana yang memanggil judge
- Menjalankan evaluasi terhadap banyak kasus uji dengan
runEvals() - Memakai gates dan threshold untuk membuat gerbang mutu di CI
- Membaca
verdictdan menerjemahkannya menjadi keputusan lolos-tidaknya build
Prasyarat
Kapan butuh scorer sendiri
Scorer bawaan menilai hal-hal yang berlaku umum: relevansi, halusinasi, toksisitas. Yang tidak mereka ketahui adalah aturan domainmu.
- "Balasan support harus menyebutkan nomor tiket"
- "Ringkasan medis tidak boleh menyarankan dosis obat"
- "Laporan penjualan harus memuat perbandingan periode sebelumnya"
Kriteria semacam itu hanya kamu yang tahu, dan createScorer() adalah cara menuliskannya.
Bentuk dasarnya
ts
// src/mastra/scorers/custom-scorer.ts
import { createScorer } from '@mastra/core/evals'
export const scorer = createScorer({
id: 'my-custom-scorer',
name: 'My Custom Scorer',
description: 'Menilai respons berdasarkan kriteria kustom',
type: 'agent',
judge: {
model: 'openai/gpt-5-mini',
instructions: 'Kamu penilai ahli...',
},
})
.preprocess({ /* konfigurasi tahap */ })
.analyze({ /* konfigurasi tahap */ })
.generateScore(({ run, results }) => {
// Kembalikan sebuah angka
return 1
})
.generateReason({ /* konfigurasi tahap */ })Polanya: createScorer() mengembalikan builder, dan kamu merangkai tahapan di atasnya. Satu-satunya tahap yang wajib adalah generateScore.
| Opsi | Kegunaan |
|---|---|
id | Pengenal unik. Dipakai sebagai nama bila name tidak diisi |
name | Nama scorer |
description | Penjelasan apa yang dinilai |
type | 'agent' untuk tipe agent otomatis |
judge.model | Model yang menilai |
judge.instructions | System prompt untuk penilai |
prepareRun | Mengubah data run sebelum pipeline berjalan |
Empat tahap, dua bentuk
Ini konsep inti bab ini, dan yang paling menentukan biaya scorer-mu.
Tiap tahap bisa ditulis dalam dua bentuk:
| Bentuk | Isinya | Memanggil judge? |
|---|---|---|
| Function step | Fungsi biasa | Tidak pernah |
| Prompt object step | Objek dengan description + createPrompt (dan outputSchema untuk preprocess/analyze) | Ya |
Dokumentasi menyatakannya dengan tegas: judge hanya berjalan untuk tahap yang berbentuk prompt object. Kalau kamu hanya memakai function step, judge tidak pernah dipanggil sama sekali — dan skor serta alasannya harus dihasilkan fungsimu sendiri.
Keempat tahap dan urutannya:
| Tahap | Perannya |
|---|---|
preprocess | Menyiapkan atau mengekstrak data sebelum dianalisis |
analyze | Menganalisis keluaran terhadap kriteria |
generateScore | Menghasilkan angka — satu-satunya yang wajib |
generateReason | Menjelaskan kenapa skornya begitu |
Saat sebuah prompt-object step berjalan, keluaran terstrukturnya disimpan ke field hasil yang bersangkutan — preprocessStepResult, analyzeStepResult, atau nilai yang dikonsumsi saat menghitung skor.
Scorer deterministik itu sah dan murah
Scorer yang seluruh tahapnya berupa function step tidak memanggil model sama sekali. Untuk aturan seperti "balasan harus menyebutkan nomor tiket", itu justru yang kamu inginkan — hasilnya pasti, instan, dan gratis.
Pakai judge hanya untuk kriteria yang benar-benar butuh penilaian bahasa.
prepareRun — membatasi data yang dinilai
Satu opsi yang layak diperhatikan: prepareRun mengubah data run sebelum pipeline berjalan. Kegunaannya menurut dokumentasi: menyaring pesan, membatasi ukuran konteks, atau membuang field yang tidak dibutuhkan scorer.
Ini penting untuk scorer berbasis judge — makin sedikit data yang dikirim ke penilai, makin murah dan makin fokus penilaiannya. Tersedia juga utilitas filterRun() yang membuat fungsi ini dari opsi deklaratif.
Menjalankan evaluasi di CI
Sampai sini scorer berjalan sebagai live evaluation. Untuk CI, kamu butuh menjalankan banyak kasus uji sekaligus — dan itu tugas runEvals().
ts
// src/mastra/scorers/weather-agent.test.ts
import { describe, it, expect } from 'vitest'
import { runEvals } from '@mastra/core/evals'
import { weatherAgent } from './weather-agent.ts'
import { locationScorer } from '../scorers/location-scorer.ts'
describe('Weather Agent Tests', () => {
it('mengekstrak lokasi dari pertanyaan dengan benar', async () => {
const result = await runEvals({
data: [
{
input: 'cuaca di Berlin',
groundTruth: { expectedLocation: 'Berlin', expectedCountry: 'DE' },
},
{
input: 'cuaca di Berlin, Maryland',
groundTruth: { expectedLocation: 'Berlin', expectedCountry: 'US' },
},
{
input: 'cuaca di Berlin, Rusia',
groundTruth: { expectedLocation: 'Berlin', expectedCountry: 'RU' },
},
],
target: weatherAgent,
scorers: [locationScorer],
})
expect(result.scores['location-accuracy']).toBe(1)
expect(result.summary.totalItems).toBe(3)
})
})Beberapa hal yang perlu dicatat:
Framework tesnya bebas — apa pun yang mendukung modul ESM: Vitest, Jest, atau Mocha.
data berisi kasus uji, masing-masing dengan input dan groundTruth opsional untuk validasi scorer.
target adalah agent atau workflow yang diuji.
Hasilnya agregat — result.scores per scorer, dan result.summary untuk ringkasannya.
Perhatikan ketiga kasus uji di atas: semuanya menyebut "Berlin", tapi dengan maksud berbeda. Ini pola kasus uji yang baik — menguji perbatasan yang mudah salah, bukan kasus yang jelas benar.
Gates dan threshold — gerbang mutu
runEvals() sendiri hanya memberi angka. Untuk CI, kamu butuh keputusan: build ini lolos atau tidak?
Gates dan threshold menambahkan semantik keparahan.
ts
// src/mastra/scorers/weather-agent.test.ts
import { runEvals } from '@mastra/core/evals'
import { checks } from '@mastra/evals/checks'
import { weatherAgent } from '../agents/weather-agent.ts'
import { faithfulnessScorer } from '../scorers/faithfulness.ts'
const result = await runEvals({
data: [{ input: 'Bagaimana cuaca di Brooklyn?' }],
target: weatherAgent,
// Gates: semua harus bernilai 1.0, atau run dianggap gagal
gates: [checks.calledTool('get_weather'), checks.noToolErrors()],
// Scorers: dilacak, dengan ambang opsional
scorers: [
{ scorer: faithfulnessScorer, threshold: 0.7 },
checks.includes('Brooklyn'), // tanpa ambang = hanya dilacak
],
})
console.log(result.verdict) // 'passed' | 'scored' | 'failed'Perbedaan keduanya:
| Gates | Threshold | |
|---|---|---|
| Sifatnya | Syarat keras | Metrik yang dilacak |
| Harus bernilai | Rata-rata 1.0 | Di atas ambang yang ditetapkan |
| Kalau gagal | Run gagal | Verdict menjadi scored |
| Berjalan | Sebelum scorer biasa | Setelah gates |
Membaca verdict
Verdict dihitung setelah semua item data diproses:
| Verdict | Artinya |
|---|---|
failed | Minimal satu gate rata-ratanya di bawah 1.0 |
scored | Semua gate lolos, tapi minimal satu threshold tidak tercapai |
passed | Semua gate bernilai 1.0 dan semua threshold tercapai |
Tiga tingkat ini memetakan dengan rapi ke keputusan CI:
passed → build lolos
scored → build lolos dengan peringatan (mutu turun, tapi tidak melanggar aturan keras)
failed → build ditolakKalau tidak ada gate maupun scorer berambang yang diberikan, field verdict dihilangkan dan runEvals berperilaku seperti sebelumnya.
Pilih gate dengan hemat
Gate harus bernilai 1.0 rata-rata di semua item data — itu standar yang sangat ketat, dan memang seharusnya. Isi gate dengan hal-hal yang benar-benar tidak boleh dilanggar: tool yang benar dipanggil, tidak ada error tool, tidak ada konten berbahaya.
Segala yang bersifat "sebaiknya lebih baik" masuk ke threshold, bukan gate. Gate yang terlalu banyak menghasilkan build yang gagal terus dan tim yang berhenti memperhatikannya.
Alur lengkap dari pengembangan ke CI
Perhatikan langkah D: kasus uji terbaik datang dari kegagalan nyata. Setiap kali agent-mu salah di produksi, itu kandidat kasus uji baru. Suite evaluasi yang tumbuh dari kegagalan nyata jauh lebih berguna daripada yang disusun dari bayangan di awal.
Kesalahan umum
Gejala: Scorer kustom berjalan lambat dan mahal, padahal kriterianya sederhana dan sebenarnya pasti. Penyebab: Tahapnya ditulis sebagai prompt object, sehingga tiap penilaian memanggil judge — padahal kriteria seperti "memuat nomor tiket" bisa dinilai fungsi biasa. Perbaikan: Tulis tahapnya sebagai function step. Judge hanya berjalan untuk prompt object step; scorer yang seluruhnya function step tidak memanggil model sama sekali.
Gejala: Pipeline CI gagal terus-menerus sampai tim mulai mengabaikannya. Penyebab: Terlalu banyak scorer dijadikan gate. Gate menuntut rata-rata 1.0 di semua item — standar yang hanya pantas untuk syarat keras. Perbaikan: Sisakan di gates hanya hal yang benar-benar tidak boleh dilanggar. Pindahkan sisanya ke scorers dengan threshold, sehingga penurunan mutu menghasilkan verdict scored — terlihat, tapi tidak menolak build.
Coba sendiri
Tantangan: Bangun gerbang mutu lengkap untuk pustaka-agent dari Bagian 2, dari scorer kustom sampai CI yang menolak build.
Ketentuan:
- Tulis scorer deterministik dengan
createScorer()yang seluruh tahapnya berupa function step. Kriterianya: jawaban tentang ketersediaan buku harus menyebutkan judul buku secara persis - Tulis scorer berbasis judge untuk kriteria yang benar-benar butuh penilaian: apakah nada jawabannya sesuai untuk pelayanan perpustakaan
- Susun minimal 12 kasus uji, termasuk minimal 4 yang berada di perbatasan — judul yang mirip, buku yang tidak ada, anggota yang tidak terdaftar
- Jalankan
runEvals()dengan:gates: tool yang benar dipanggil, dan tidak ada error toolscorers: kedua scorer-mu, satu denganthreshold, satu tanpa
- Pastikan
verdictbernilaipassed - Rusak sengaja: hapus satu tool dari agent. Jalankan lagi dan pastikan
verdictmenjadifailed - Rusak lebih halus: kembalikan tool-nya, tapi longgarkan instruksi agent sehingga nadanya jadi kurang sesuai. Pastikan
verdictmenjadiscored, bukanfailed - Jalankan suite ini lewat Vitest
Checklist penerimaan:
- [ ] Scorer pertama tidak memanggil model sama sekali — buktikan dengan memeriksa bahwa tidak ada panggilan judge di trace
- [ ] Ketiga verdict berhasil dipicu:
passed,failed, danscored - [ ] Kamu bisa menjelaskan kenapa langkah 6 menghasilkan
failedsementara langkah 7 menghasilkanscored - [ ] Minimal 4 kasus ujimu menguji perbatasan, bukan kasus yang jelas benar
- [ ] Suite berjalan lewat
vitestdan bisa dijalankan di CI - [ ] Kamu bisa menyebutkan satu kriteria yang sengaja tidak kamu jadikan gate, beserta alasannya
Petunjuk: Langkah 7 adalah bagian tersulit dan paling berharga. Perbedaan antara failed dan scored adalah perbedaan antara "ada aturan yang dilanggar" dan "mutunya menurun" — dan sistem CI yang tidak bisa membedakan keduanya akan memaksa timmu memilih antara mengabaikan peringatan atau memblokir rilis untuk hal sepele. Rasakan sendiri kedua verdict itu muncul dari sebab yang berbeda.
Ikhtisar
createScorer()mengembalikan builder; kamu merangkai tahapan di atasnya. HanyagenerateScoreyang wajib.- Empat tahap:
preprocess→analyze→generateScore→generateReason. - Tiap tahap bisa berupa function step (tidak pernah memanggil judge) atau prompt object step (memanggil judge). Scorer yang seluruhnya function step tidak memanggil model sama sekali.
prepareRunmembatasi data yang masuk ke pipeline — menyaring pesan, membatasi konteks, membuang field yang tak perlu.runEvals()menjalankan banyak kasus uji terhadap agent atau workflow, dan bekerja dengan framework tes ESM apa pun.- Gates adalah syarat keras yang harus rata-rata 1.0; threshold adalah metrik yang dilacak dengan skor minimum.
verdict:failed(gate gagal) →scored(threshold meleset) →passed(semuanya terpenuhi). Ini memetakan langsung ke keputusan CI.- Kasus uji terbaik datang dari kegagalan nyata di produksi, bukan dari bayangan saat merancang.
Lanjut ke mana
Bagian 8 selesai. Kamu bisa melihat apa yang terjadi dan mengukur seberapa baik hasilnya. Yang tersisa adalah membuat semuanya bertahan di dunia nyata: Bagian 9 — Produksi.