Skip to content

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 verdict dan 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.

OpsiKegunaan
idPengenal unik. Dipakai sebagai nama bila name tidak diisi
nameNama scorer
descriptionPenjelasan apa yang dinilai
type'agent' untuk tipe agent otomatis
judge.modelModel yang menilai
judge.instructionsSystem prompt untuk penilai
prepareRunMengubah 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:

BentukIsinyaMemanggil judge?
Function stepFungsi biasaTidak pernah
Prompt object stepObjek 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:

TahapPerannya
preprocessMenyiapkan atau mengekstrak data sebelum dianalisis
analyzeMenganalisis keluaran terhadap kriteria
generateScoreMenghasilkan angka — satu-satunya yang wajib
generateReasonMenjelaskan 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 agregatresult.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:

GatesThreshold
SifatnyaSyarat kerasMetrik yang dilacak
Harus bernilaiRata-rata 1.0Di atas ambang yang ditetapkan
Kalau gagalRun gagalVerdict menjadi scored
BerjalanSebelum scorer biasaSetelah gates

Membaca verdict

Verdict dihitung setelah semua item data diproses:

VerdictArtinya
failedMinimal satu gate rata-ratanya di bawah 1.0
scoredSemua gate lolos, tapi minimal satu threshold tidak tercapai
passedSemua 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 ditolak

Kalau 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:

  1. Tulis scorer deterministik dengan createScorer() yang seluruh tahapnya berupa function step. Kriterianya: jawaban tentang ketersediaan buku harus menyebutkan judul buku secara persis
  2. Tulis scorer berbasis judge untuk kriteria yang benar-benar butuh penilaian: apakah nada jawabannya sesuai untuk pelayanan perpustakaan
  3. Susun minimal 12 kasus uji, termasuk minimal 4 yang berada di perbatasan — judul yang mirip, buku yang tidak ada, anggota yang tidak terdaftar
  4. Jalankan runEvals() dengan:
    • gates: tool yang benar dipanggil, dan tidak ada error tool
    • scorers: kedua scorer-mu, satu dengan threshold, satu tanpa
  5. Pastikan verdict bernilai passed
  6. Rusak sengaja: hapus satu tool dari agent. Jalankan lagi dan pastikan verdict menjadi failed
  7. Rusak lebih halus: kembalikan tool-nya, tapi longgarkan instruksi agent sehingga nadanya jadi kurang sesuai. Pastikan verdict menjadi scored, bukan failed
  8. 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, dan scored
  • [ ] Kamu bisa menjelaskan kenapa langkah 6 menghasilkan failed sementara langkah 7 menghasilkan scored
  • [ ] Minimal 4 kasus ujimu menguji perbatasan, bukan kasus yang jelas benar
  • [ ] Suite berjalan lewat vitest dan 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. Hanya generateScore yang wajib.
  • Empat tahap: preprocessanalyzegenerateScoregenerateReason.
  • 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.
  • prepareRun membatasi 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.

Materi belajar mandiri. Bukan dokumentasi resmi Mastra.