Skip to content

10.15 Platform Siap Produksi

Topologi: C — Supervisor + Subagent Materi yang dipakai: 8.1, 8.2, 8.3, 8.4, 9.4Prasyarat: 10.10, 10.11

Masalahnya

Empat belas kasus sebelumnya membangun sistem yang bekerja. Kasus ini menjawab pertanyaan yang berbeda: bagaimana kamu tahu ia masih bekerja besok?

Tiga situasi nyata yang tidak bisa dijawab sistem mana pun sejauh ini:

  • Kamu memperbaiki instruksi agent hari Senin. Apakah itu memperbaiki mutu atau justru merusak sesuatu yang lain?
  • Pelanggan melapor jawaban salah tadi pagi. Trace yang mana?
  • Biaya bulan ini naik 40%. Dari agent mana?

Ketiganya sama-sama pertanyaan pengukuran, dan tidak ada satu pun yang terjawab dengan mencoba sendiri lima pertanyaan.

Keputusan arsitektur

Keputusan 1 — Apa yang direkam, dan berapa banyak?

Merekam semuanya bagus saat pengembangan dan mahal di produksi.

DataVolumeStrategi
TraceSangat tinggiSemua yang gagal, sebagian yang sukses
LogTinggiLevel info ke atas, format JSON
MetrikRendah (agregat)Semua
SkorSedangSampling per scorer

Pilihan: sampling asimetris — 100% trace yang error, sebagian kecil yang sukses.

Alasannya dari 8.1: trace sukses sebagian besar seragam dan bisa diwakili sampel; trace gagal selalu berbeda, dan justru itu yang kamu butuhkan saat mendiagnosis.

Keputusan 2 — Storage untuk observability

Ini penerapan langsung composite storage dari 9.1.

DomainPola aksesBackend
memory, workflowsBaca-tulis baris, transaksionalTurso / Postgres
observabilityTulis banyak, baca agregatDuckDB / ClickHouse

Pilihan: rutekan observability ke backend analitik, sisanya ke transaksional.

Menyimpan telemetri bervolume tinggi di basis data transaksional bisa saja — tapi kueri agregat di atasnya lambat, dan tabelnya membengkak jauh lebih cepat dari tabel bisnis.

Keputusan 3 — Scorer mana yang dijalankan live, mana di CI?

Live evaluationCI (runEvals)
KapanSaat agent melayaniSebelum merge
DataLalu lintas nyataKasus uji tetap
BiayaPer permintaanPer build
Menjawab"Apakah mutu turun?""Apakah perubahan ini merusak?"

Pilihan: keduanya, dengan pembagian tegas.

  • Live: scorer keamanan pada rate: 1, scorer mutu pada rate rendah
  • CI: gate keras + threshold pada kasus uji yang dikurasi

Keduanya menjawab pertanyaan berbeda dan tidak saling menggantikan.

Keputusan 4 — Quick Check dulu, LLM belakangan

Quick ChecksScorer LLM
BiayaNolPer pemanggilan
KecepatanInstanDetik
Bisa menilaiFakta deterministikMutu bahasa

Pilihan: apa pun yang deterministik pakai Quick Check.

"Apakah tool dipanggil sebelum menjawab" tidak butuh model untuk menilainya. Simpan scorer LLM untuk pertanyaan yang benar-benar butuh penilaian bahasa.

Struktur direktori

text
platform-cs/
├── src/
│   ├── domain/
│   ├── mastra/
│   │   ├── agents/
│   │   ├── scorers/
│   │   │   ├── tanpa-janji.ts       ← scorer kustom, DETERMINISTIK
│   │   │   └── nada-sesuai.ts       ← scorer kustom, pakai judge
│   │   └── index.ts                 ← observability + composite storage
│   ├── server/
│   └── worker/
├── evals/
│   ├── kasus-uji.ts                 ← dikurasi dari kegagalan nyata
│   └── cs-agent.test.ts             ← dijalankan di CI
├── .github/workflows/eval.yml
└── package.json

evals/ sejajar dengan src/, bukan di dalamnya — ia bukan bagian dari aplikasi yang di-deploy.

Alur

Panah putus-putus di bawah adalah lingkaran yang membuat sistem ini membaik seiring waktu: setiap kegagalan di produksi menjadi kasus uji baru.

Implementasi

src/mastra/index.ts

ts
import { Mastra } from '@mastra/core'
import { MastraCompositeStore } from '@mastra/core/storage'
import { LibSQLStore } from '@mastra/libsql'
import { DuckDBStore } from '@mastra/duckdb'
import { PinoLogger } from '@mastra/loggers'
import {
  Observability,
  MastraStorageExporter,
  SensitiveDataFilter,
} from '@mastra/observability'
import { csAgent } from './agents/cs-agent.ts'

export const mastra = new Mastra({
  agents: { csAgent },

  // Tiap domain ke backend yang cocok pola aksesnya.
  storage: new MastraCompositeStore({
    id: 'composite-storage',
    default: new LibSQLStore({
      id: 'mastra-storage',
      url: process.env.TURSO_DATABASE_URL || 'file:./mastra.db',
      authToken: process.env.TURSO_AUTH_TOKEN || undefined,
    }),
    domains: {
      // Telemetri: tulis banyak, baca agregat.
      observability: await new DuckDBStore().getStore('observability'),
    },
  }),

  logger: new PinoLogger({
    name: 'platform-cs',
    level: 'info',
    prettyPrint: false,     // JSON di produksi
  }),

  observability: new Observability({
    configs: {
      produksi: {
        serviceName: 'platform-cs',
        exporters: [new MastraStorageExporter()],
        spanOutputProcessors: [new SensitiveDataFilter()],

        // Inilah yang membuat trace bisa DICARI saat pelanggan melapor.
        requestContextKeys: ['tenantId', 'userId'],

        includeInternalSpans: false,
      },
    },
  }),

  server: {
    port: Number(process.env.PORT) || 4111,
    host: '0.0.0.0',
  },
})

requestContextKeys adalah baris yang paling sering dilupakan dan paling sering disesali. Tanpa itu, kamu punya ribuan trace tanpa cara menemukan milik pelanggan tertentu.

observability menerima instance, bukan objek konfigurasi

Ingat dari 8.1: properti ini bertipe ObservabilityEntrypoint. Bentuk observability: { configs: {...} } akan ditolak compiler.

Bungkus dengan new Observability({ ... }) dari @mastra/observability.

src/mastra/scorers/tanpa-janji.ts

ts
import { createScorer } from '@mastra/core/evals'

// Scorer DETERMINISTIK — seluruh tahapnya function step,
// jadi judge TIDAK PERNAH dipanggil. Nol biaya.
export const tanpaJanji = createScorer({
  id: 'tanpa-janji',
  name: 'Tanpa Janji Waktu',
  description:
    'Memastikan jawaban tidak menjanjikan waktu tiba yang tidak berdasar data',
}).generateScore(({ run }) => {
  const teks = JSON.stringify(run.output ?? '').toLowerCase()
  const janji = /(besok|hari ini|2 hari|3 hari|minggu depan) (sampai|tiba|diterima)/
  return janji.test(teks) ? 0 : 1
})

Aturan domain seperti ini tidak dicakup scorer bawaan — tidak ada createNoDeliveryPromiseScorer(). Inilah kasus di mana scorer kustom memang diperlukan.

Dan karena kriterianya bisa diperiksa regex, ia ditulis sebagai function step: judge tidak pernah dipanggil, biayanya nol.

src/mastra/agents/cs-agent.ts — live evaluation

ts
import { Agent } from '@mastra/core/agent'
import { createToxicityScorer, createAnswerRelevancyScorer } from '@mastra/evals/scorers/prebuilt'
import { tanpaJanji } from '../scorers/tanpa-janji.ts'
import { cekPesanan } from '../tools/cek-pesanan.ts'

export const csAgent = new Agent({
  id: 'cs-agent',
  name: 'CS Agent',
  instructions: '...',
  model: 'openai/gpt-5-mini',
  tools: { cekPesanan },

  scorers: {
    // Keamanan: SEMUA keluaran diperiksa.
    keamanan: {
      scorer: createToxicityScorer({ model: 'openai/gpt-5-mini' }),
      sampling: { type: 'ratio', rate: 1 },
    },

    // Aturan domain: gratis, jadi jalankan semuanya.
    janji: {
      scorer: tanpaJanji,
      sampling: { type: 'ratio', rate: 1 },
    },

    // Mutu: cukup sampel — ini pemantauan tren, bukan gerbang.
    relevansi: {
      scorer: createAnswerRelevancyScorer({ model: 'openai/gpt-5-mini' }),
      sampling: { type: 'ratio', rate: 0.2 },
    },
  },
})

Perhatikan pembagian rate-nya: 1.0 untuk keamanan dan aturan domain, 0.2 untuk mutu. Pelanggaran keamanan tidak boleh lolos; penurunan mutu cukup terdeteksi secara statistik.

Ingat juga dari 8.3: sampling deterministik berdasarkan trace ID, bukan acak. Itu yang membuat skor antar scorer bisa dibandingkan pada lalu lintas yang sama.

evals/cs-agent.test.ts — gerbang CI

ts
import { describe, it, expect } from 'vitest'
import { runEvals } from '@mastra/core/evals'
import { checks } from '@mastra/evals/checks'
import { csAgent } from '../src/mastra/agents/cs-agent.ts'
import { tanpaJanji } from '../src/mastra/scorers/tanpa-janji.ts'
import { KASUS_UJI } from './kasus-uji.ts'

describe('CS Agent', () => {
  it('memenuhi gerbang mutu', async () => {
    const hasil = await runEvals({
      data: KASUS_UJI,
      target: csAgent,

      // Gate: syarat KERAS, harus rata-rata 1.0.
      gates: [
        checks.calledTool('cekPesanan'),   // ← registration key, bukan id
        checks.noToolErrors(),
      ],

      // Threshold: dilacak, kegagalan -> verdict 'scored' bukan 'failed'.
      scorers: [{ scorer: tanpaJanji, threshold: 1 }],
    })

    expect(hasil.verdict).not.toBe('failed')
  })
})

checks.calledTool() memakai registration key, bukan id tool

Ini jebakan yang ditemukan saat menjalankan contoh ini sungguhan, dan kegagalannya menyesatkan.

Tool didefinisikan dengan id: 'cek-pesanan' tapi didaftarkan sebagai tools: { cekPesanan }. Yang dilihat checks.calledTool() adalah key pendaftarancekPesanan.

Memakai 'cek-pesanan' menghasilkan verdict: 'failed' tanpa pesan yang menjelaskan kenapa — gate-nya sekadar tidak pernah terpenuhi. Buktinya ada di bagian berikut.

.github/workflows/eval.yml

yaml
name: Eval
on: [pull_request]

jobs:
  eval:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '22'
      - run: npm ci
      - run: npm run eval
        env:
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}

Hasil nyata

Suite evaluasi dijalankan sungguhan dengan gate, threshold, dan scorer kustom.

Percobaan pertama — gate memakai id tool ('cek-pesanan'):

text
VERDICT : failed
SCORES  : { "tanpa-janji": 1 }
SUMMARY : { "totalItems": 2 }

Perhatikan betapa menyesatkannya keluaran ini: scorer kustom lulus sempurna (skor 1), tapi verdict failed. Tidak ada pesan yang menyebutkan gate mana yang gagal atau kenapa.

Percobaan kedua — satu-satunya yang berubah adalah nama tool di gate, dari 'cek-pesanan' menjadi 'cekPesanan':

text
VERDICT : passed
SCORES  : { "tanpa-janji": 1 }
SUMMARY : { "totalItems": 2 }

Dua pelajaran dari perbandingan ini.

1. checks.calledTool() memakai registration key. Kalau gate-mu gagal tanpa penjelasan, ini hal pertama yang harus diperiksa.

2. Verdict failed tidak memberitahu penyebabnya. Karena itu jalankan suite-mu sekali dalam keadaan yang kamu tahu pasti lulus sebelum mengandalkannya di CI. Kalau tidak, kamu akan mengira agent-mu bermasalah padahal gate-nya yang salah tulis.

Perhatikan juga scores hanya memuat scorer bernama, bukan gate. Gate mempengaruhi verdict tapi tidak muncul sebagai skor — itu perbedaan yang disebut di 8.4.

Tiga verdict, tiga keputusan CI

VerdictArtinyaKeputusan
passedSemua gate 1.0, semua threshold tercapaiMerge
scoredGate lolos, ada threshold melesetMerge + peringatan
failedAda gate di bawah 1.0Tolak

Pemisahan ini yang membuat CI berguna. Tanpa scored, tim harus memilih antara memblokir rilis untuk penurunan mutu kecil, atau mengabaikan peringatan sama sekali.

evals/kasus-uji.ts — dari mana kasus uji datang

ts
export const KASUS_UJI = [
  // --- kasus normal ---
  { input: 'status pesanan P-1?' },

  // --- perbatasan ---
  { input: 'status pesanan P-999?' },        // tidak ada
  { input: 'status pesanan?' },              // nomor tidak disebut
  { input: 'pesanan saya kapan sampai?' },   // memancing janji waktu

  // --- dari kegagalan produksi nyata ---
  // 2026-09-03: agent menjanjikan "besok sampai" tanpa data
  { input: 'udah dikirim belum? buru-buru nih' },
]

Komentar tanggal di kasus terakhir bukan hiasan. Kasus uji terbaik datang dari kegagalan nyata, bukan dari bayangan saat merancang — dan mencatat asalnya membuat orang berikutnya tidak menghapusnya karena mengira tidak penting.

Menjawab tiga pertanyaan pembuka

Sekarang ketiga masalah di awal kasus ini bisa dijawab:

PertanyaanDijawab oleh
"Apakah perubahan Senin merusak sesuatu?"runEvals di CI, bandingkan verdict sebelum dan sesudah
"Trace pelanggan yang mana?"Filter berdasarkan requestContextKeys: ['tenantId', 'userId']
"Biaya naik dari agent mana?"Metrik agregat di storage analitik

Best practice yang dipakai di sini

1. Composite storage: telemetri ke backend analitik. Pola aksesnya memang berbeda dari data bisnis.

2. requestContextKeys diisi sejak awal. Tanpa itu trace tidak bisa dicari.

3. Rate scorer asimetris. Keamanan 1.0, mutu 0.2.

4. Scorer deterministik untuk aturan domain. Nol biaya, dan bisa dijalankan pada semua keluaran.

5. Gate untuk syarat keras, threshold untuk mutu. Gate yang terlalu banyak membuat CI gagal terus dan diabaikan.

6. Jalankan suite dalam keadaan pasti lulus dulu. Membuktikan gerbangnya sendiri benar, sebelum mempercayainya.

7. Kasus uji diberi tanggal dan asal-usul. Supaya tidak dihapus orang berikutnya.

8. prettyPrint: false di produksi. JSON bisa diurai pengumpul log.

Kapan pola ini salah

SituasiKenapa gagalKe mana
Prototipe yang belum punya penggunaMengukur sebelum ada yang diukurTunda sampai ada lalu lintas
Biaya eval mendekati biaya agentTerlalu banyak scorer LLM pada rate tinggiPindahkan ke Quick Checks
Tim mengabaikan hasil CITerlalu banyak gateKurangi gate, perbanyak threshold
Butuh tahu tren lintas mingguTrace per permintaan tidak cukupMetrik agregat

Coba sendiri

Ambil salah satu sistem dari 14 kasus sebelumnya dan pasangi gerbang mutu lengkap.

Selesai kalau

  • [ ] Observability aktif dengan requestContextKeys yang bisa dipakai mencari trace per pengguna
  • [ ] Minimal satu scorer kustom deterministik untuk aturan domain sistem itu
  • [ ] Live evaluation dengan rate berbeda: 1.0 untuk keamanan, lebih rendah untuk mutu
  • [ ] Suite runEvals dengan minimal 8 kasus uji, minimal 3 di perbatasan
  • [ ] Kamu bisa memicu ketiga verdict: passed, scored, dan failed
  • [ ] Kamu menjalankan suite dalam keadaan pasti lulus lebih dulu untuk memvalidasi gate-nya
  • [ ] Satu baris log error bisa dilacak ke trace-nya lewat trace_id
  • [ ] Berjalan di CI dan menolak PR yang merusak gate

Kalau macet

verdict: failed tapi semua skor bagus? Periksa nama tool di checks.calledTool() — ia memakai registration key, bukan properti id.

Biaya eval membengkak? Hitung scorer LLM mana yang berjalan pada rate 1.0. Pindahkan yang deterministik ke Quick Checks atau scorer function-step.

Trace tidak bisa dicari? requestContextKeys belum diisi, atau nilainya tidak pernah di-set ke RequestContext di middleware.

Penutup Bagian 10

Lima belas kasus, dari FAQ bot 40 baris sampai platform yang mengukur dirinya sendiri. Kalau ada satu hal yang layak dibawa dari seluruh bagian ini:

Keputusan arsitektur yang paling menentukan hampir selalu dibuat sebelum baris kode pertama — dan hampir selalu bisa diringkas jadi pertanyaan sederhana: apakah ini butuh penilaian, atau butuh kepastian?

Agent untuk yang pertama. Workflow, kode biasa, dan gerbang mutu untuk yang kedua.

Lanjut ke mana

Sekarang giliranmu membangun sesuatu yang memakai semuanya — Bagian 11 — Proyek Akhir.

Materi belajar mandiri. Bukan dokumentasi resmi Mastra.