Skip to content

1.3 generate() dan stream()

Kenapa bab ini penting

Pilihan antara kedua method ini tidak mengubah apa yang dijawab agent, tapi sangat mengubah bagaimana rasanya dipakai. Untuk jawaban panjang, selisihnya adalah antara pengguna menatap layar kosong beberapa detik, atau melihat teks mengalir hampir seketika.

Kalau kamu melewatinya: antarmuka chat-mu akan terasa menggantung, dan kamu tidak punya cara mendiagnosis jawaban yang terpotong di tengah.

Kalau kamu tidak butuh: agent-mu dipanggil dari job latar belakang atau workflow, di mana tidak ada manusia yang menunggu di depan layar.

Tujuan bab

Setelah bab ini kamu bisa:

  • Memanggil agent dengan generate() dan stream(), dan memilih yang tepat untuk sebuah kebutuhan
  • Membaca textStream secara bertahap dengan for await
  • Mengambil properti hasil selain teks: usage, steps, dan finishReason
  • Mengirim pesan dalam tiga bentuk — string, array string, dan array objek pesan — serta tahu kapan tiap bentuk berguna
  • Membatasi loop agent dengan maxSteps dan stopWhen

Prasyarat

Satu perbedaan, dua pengalaman

Kedua method memanggil agent yang sama dengan cara kerja internal yang sama. Yang berbeda hanya kapan kamu menerima hasilnya.

generate()   ──────────────────────────────────▶ [jawaban utuh]
                     (menunggu)

stream()     ─▶ Un ─▶ tuk ─▶ me ─▶ re ─▶ set ─▶ ... ─▶ [selesai]
                (potongan demi potongan)

Untuk jawaban satu kalimat, perbedaannya tidak terasa. Untuk jawaban tiga paragraf, generate() berarti pengguna menatap layar kosong selama beberapa detik, sementara stream() mulai menampilkan kata pertama hampir seketika.

Waktu total sampai selesai kurang lebih sama. Yang berubah adalah waktu sampai tanda kehidupan pertama, dan bagi pengguna itu perbedaan antara "cepat" dan "menggantung".

Diukur sungguhan

Pertanyaan yang sama — "Jelaskan cara kerja cache pada aplikasi web" dalam 3 paragraf — dijalankan dengan kedua method memakai openai/gpt-5-mini:

text
generate() : karakter pertama muncul setelah 6674 ms
             (yaitu saat SELURUH jawaban selesai)
             total 1797 karakter

stream()   : karakter pertama muncul setelah 3069 ms
             selesai seluruhnya setelah 8670 ms, total 2174 karakter

Dua hal yang perlu dibaca dari angka ini, dan yang kedua sering mengejutkan:

  1. Pengguna menunggu 6674 ms vs 3069 ms sebelum melihat apa pun. Lebih dari dua kali lipat.
  2. stream() justru selesai lebih lambat — 8670 ms vs 6674 ms.

Jadi streaming tidak membuat agent lebih cepat. Ia memindahkan penantian: dari "diam total lalu semuanya muncul" menjadi "mulai cepat lalu mengalir". Untuk manusia yang menunggu, yang kedua terasa jauh lebih baik meski totalnya lebih lama.

Dari pemanggilan yang sama, usage mengungkap sesuatu yang tidak terlihat:

text
finishReason : stop
usage        : { inputTokens: 35, outputTokens: 879, totalTokens: 914,
                 reasoningTokens: 384 }

384 dari 879 token keluaran adalah reasoning — penalaran internal model yang tidak pernah muncul di layar, tapi tetap kamu bayar. Ini alasan konkret kenapa usage layak dicatat, bukan diabaikan.

generate()

ts
// run.mjs
import { mastra } from './src/mastra/index.ts'

const agent = mastra.getAgentById('support-agent')
const result = await agent.generate('Bagaimana cara reset password saya?')

console.log(result.text)

Method ini menunggu sampai seluruh proses selesai, lalu mengembalikan hasil lengkapnya sekaligus. Pakai ini saat:

  • Hasilnya diproses program, bukan ditampilkan ke manusia
  • Kamu berada di dalam job latar belakang atau workflow
  • Kamu butuh keluaran terstruktur yang harus utuh dulu sebelum berguna (lihat 1.4)

stream()

ts
// run.mjs
import { mastra } from './src/mastra/index.ts'

const agent = mastra.getAgentById('support-agent')
const stream = await agent.stream([
  { role: 'user', content: 'Bantu saya menyusun rencana hari ini' },
])

for await (const chunk of stream.textStream) {
  process.stdout.write(chunk)
}

stream() mengembalikan objek MastraModelOutput. Properti textStream adalah readable stream yang memancarkan potongan teks; for await mengonsumsinya sepotong demi sepotong.

Perhatikan process.stdout.write() alih-alih console.log() — yang kedua menambahkan baris baru di tiap potongan, sehingga jawaban akan tercetak menurun satu kata per baris.

Yang bisa diambil dari sebuah stream

Ini bagian yang sering terlewat: stream() bukan hanya soal teks bertahap. Objek yang dikembalikan menyediakan sejumlah properti, sebagian berupa stream dan sebagian berupa promise yang selesai saat proses berakhir.

PropertiJenisIsi
textStreamstreamPotongan teks saat dihasilkan
textpromiseTeks lengkap setelah selesai
stepspromiseLangkah-langkah model yang telah dijalankan
finishReasonpromiseAlasan agent berhenti
usagepromiseInformasi pemakaian token
objectStream, objectstream, promiseKeluaran terstruktur, parsial dan final

Artinya kamu bisa melakukan keduanya sekaligus — menampilkan teks secara bertahap ke pengguna, lalu setelah selesai mengambil data pemakaian token untuk dicatat:

ts
// run.mjs
import { mastra } from './src/mastra/index.ts'

const agent = mastra.getAgentById('support-agent')
const stream = await agent.stream('Jelaskan cara kerja cache dalam 3 paragraf')

for await (const chunk of stream.textStream) {
  process.stdout.write(chunk)
}

const usage = await stream.usage
const finishReason = await stream.finishReason

console.log('\n---')
console.log('Alasan berhenti:', finishReason)
console.log('Pemakaian token:', usage)

finishReason layak diperhatikan sejak awal. Nilainya memberitahu kenapa agent berhenti — misalnya karena memang selesai, karena menabrak batas panjang, atau karena masih menunggu hasil tool. Saat jawaban terpotong di tengah kalimat, ini properti pertama yang harus kamu periksa.

Tiga bentuk pesan

Kedua method menerima pesan dalam beberapa bentuk:

ts
// 1. String tunggal — paling ringkas
await agent.generate('Bantu saya menyusun rencana hari ini')

// 2. Array string — beberapa potong konteks
await agent.generate([
  'Saya punya rapat jam 10 dan jam 14.',
  'Saya perlu menyiapkan presentasi.',
  'Bagaimana sebaiknya saya susun hari ini?',
])

// 3. Array objek pesan — kontrol penuh atas peran
await agent.generate([
  { role: 'user', content: 'Saya perlu bantuan menyusun jadwal' },
  { role: 'assistant', content: 'Tentu. Apa saja agenda Anda hari ini?' },
  { role: 'user', content: 'Dua rapat dan satu presentasi' },
])

Bentuk ketiga memberi kendali penuh atas peran dan alur percakapan. Ini berguna saat kamu mengelola riwayat sendiri — misalnya memuat percakapan dari database milikmu.

Perlu dicatat: kalau yang kamu butuhkan adalah agent yang mengingat percakapan sebelumnya secara otomatis, jangan menyusun riwayat manual seperti ini. Itu tugas memory, dan dibahas di Bagian 3.

Membatasi loop

Ingat dari 1.1 bahwa agent adalah loop. Loop tanpa batas bisa berputar lebih lama dari yang kamu mau. Dua opsi mengendalikannya:

ts
const result = await agent.generate('Riset topik ini dan rangkum', {
  maxSteps: 5,
})

maxSteps membatasi jumlah langkah maksimum selama eksekusi. Sederhana dan biasanya cukup.

Untuk kondisi berhenti yang lebih spesifik — misalnya berdasarkan batas token, bukan hanya jumlah langkah — tersedia stopWhen.

Ada juga onIterationComplete, sebuah callback yang dipanggil setiap kali satu iterasi selesai. Ia menerima konteks berisi teks iterasi saat ini, tool apa yang dipanggil, dan alasan iterasi itu berakhir — lalu boleh mengembalikan continue: false untuk menghentikan lebih awal, atau feedback berupa pesan untuk mengarahkan iterasi berikutnya. Ini alat yang berguna saat kamu ingin mengawasi agent yang berjalan panjang, dan akan lebih masuk akal setelah kamu melihat trace di Bagian 8.

Memilih di antara keduanya

Kesalahan umum

Gejala: Jawaban tercetak menurun, satu kata atau satu potongan per baris, padahal seharusnya menyambung jadi paragraf. Penyebab: Memakai console.log() di dalam loop for await. Setiap pemanggilan menambahkan baris baru. Perbaikan: Pakai process.stdout.write(chunk) untuk keluaran terminal. Di antarmuka web, tambahkan potongan ke state teks yang sudah ada, bukan merender tiap potongan sebagai elemen terpisah.

Gejala: Jawaban berhenti di tengah kalimat, dan tidak ada error apa pun yang muncul. Penyebab: Agent berhenti karena alasan selain "selesai" — biasanya menabrak batas panjang keluaran, atau menyentuh batas langkah yang kamu tetapkan lewat maxSteps. Karena ini bukan error, tidak ada yang dilempar. Perbaikan: Periksa finishReason. Kalau nilainya menunjukkan batas panjang, perpendek permintaan atau naikkan batasnya; kalau menunjukkan batas langkah, naikkan maxSteps — tapi periksa dulu lewat trace apakah agent memang butuh langkah sebanyak itu, atau sedang berputar-putar karena instruksi yang ambigu.

Coba sendiri

Tujuannya: mengukur sendiri selisih waktu tunggu, bukan mempercayai angka di bab ini.

Kode awal

Salin ke demo/uji-stream.mjs, lalu jalankan node demo/uji-stream.mjs.

js
// demo/uji-stream.mjs
import { Agent } from '@mastra/core/agent'

const agent = new Agent({
  id: 's', name: 'S',
  instructions: 'Kamu penjelas teknis. Jawab dalam 3 paragraf.',
  model: 'openai/gpt-5-mini',
})
const PESAN = 'Jelaskan cara kerja cache pada aplikasi web.'

// --- generate() ---
let t0 = Date.now()
const g = await agent.generate(PESAN)
const tGenerate = Date.now() - t0
console.log(`generate() : ${tGenerate} ms sampai karakter pertama (= sampai selesai)`)

// --- stream() ---
t0 = Date.now()
const s = await agent.stream(PESAN)
let tFirst = null

for await (const chunk of s.textStream) {
  // TODO 1: catat waktu HANYA pada potongan pertama.
  //   Petunjuk: `if (tFirst === null) tFirst = Date.now() - t0`
}
const tTotal = Date.now() - t0

console.log(`stream()   : ${tFirst} ms sampai karakter pertama`)
console.log(`             ${tTotal} ms sampai selesai`)

// TODO 2: cetak finishReason dan usage. Keduanya PROMISE — pakai await.
// TODO 3: dari usage, cetak berapa persen token keluaran yang berupa reasoning.

Bentuk hasil yang diharapkan

Angka di mesinmu akan berbeda — yang harus muncul adalah polanya:

text
generate() : 6674 ms sampai karakter pertama (= sampai selesai)
stream()   : 3069 ms sampai karakter pertama
             8670 ms sampai selesai
finishReason: stop
reasoning   : 384 dari 879 token keluaran (44%)

Selesai kalau

  • [ ] Waktu karakter pertama stream() jauh lebih kecil dari generate()
  • [ ] Waktu total stream() tidak lebih cepat — biasanya justru sedikit lebih lama
  • [ ] Kamu bisa menjelaskan kenapa keduanya benar sekaligus
  • [ ] finishReason bernilai stop
  • [ ] Kamu tahu berapa persen token keluaranmu terpakai untuk reasoning

Tantangan lanjutan

Ubah instruksi menjadi 'Jawab dalam SATU kalimat.' lalu jalankan ulang. Selisih waktu karakter pertama akan menyusut drastis — dan itu menjelaskan kenapa stream() tidak selalu sepadan.

Kalau macet

tFirst bernilai null di akhir? Blok for await-mu kosong, atau kondisinya salah. Ia harus mengisi tFirst pada iterasi pertama saja.

finishReason mencetak Promise { <pending> }? Ia promise. Tulis await s.finishReason, bukan s.finishReason.

Teks tercetak menurun satu kata per baris? Kamu memakai console.log di dalam loop. Pakai process.stdout.write(chunk).

Ikhtisar

  • generate() dan stream() menjalankan proses yang sama; yang berbeda adalah kapan hasil sampai ke tanganmu.
  • Pakai stream() saat hasilnya dibaca manusia secara langsung dan berpotensi panjang; pakai generate() saat hasilnya diproses program, di dalam workflow, atau berupa keluaran terstruktur.
  • stream() mengembalikan MastraModelOutput — bukan hanya textStream, tapi juga promise text, steps, finishReason, usage, dan object/objectStream.
  • finishReason adalah properti diagnostik pertama saat jawaban terpotong atau berhenti tak terduga.
  • Pesan bisa dikirim sebagai string, array string, atau array objek pesan. Bentuk terakhir untuk kendali penuh — tapi riwayat otomatis adalah tugas memory, bukan tugasmu.
  • Loop dibatasi dengan maxSteps atau stopWhen, dan diawasi dengan onIterationComplete.

Lanjut ke mana

Sejauh ini keluaran agent selalu berupa teks. Untuk dipakai program, teks harus diurai dan diverifikasi — kecuali kalau kamu memintanya berbentuk objek sejak awal: 1.4 Structured Output.

Materi belajar mandiri. Bukan dokumentasi resmi Mastra.