Skip to main content
BlogAI

Membangun Chatbot Streaming dengan Groq dan Next.js

Bangun chatbot streaming yang cepat dengan Groq API dan route handler Next.js, mencakup system prompt, rate limiting, dan kontrol biaya.

Matthews Wong

Software Engineer

Juli 20269 menit baca
Membangun Chatbot Streaming dengan Groq dan Next.js

Foto oleh Ioan Sameli on flickr

Ringkasan Utama

Groq menjalankan model di atas chip LPU sehingga token pertama muncul dalam hitungan milidetik; dipadukan dengan Server-Sent Events dan route handler Next.js berbasis ReadableStream, chatbot bisa menampilkan balasan sedikit demi sedikit alih-alih menunggu respons utuh. Agar siap produksi, API key harus disimpan di server, terapkan rate limiting per pengunjung, batasi max_completion_tokens, dan perkuat system prompt terhadap prompt injection.

Sebagian besar demo chatbot terasa lambat karena menunggu seluruh respons selesai sebelum menampilkan apa pun. Chatbot produksi seharusnya terasa instan, dan dua bahan yang membuat itu mungkin adalah penyedia inferensi yang benar-benar cepat serta pipeline streaming yang mengirim token ke browser begitu token itu tersedia, bukan menyimpan seluruh balasan di server terlebih dahulu.

Tulisan ini membahas pembuatan pipeline tersebut secara menyeluruh: route handler Next.js App Router yang memanggil Groq API dengan mode streaming aktif, klien yang membaca respons secara bertahap, system prompt yang menjaga asisten tetap pada topiknya, serta rate limiting dan kontrol biaya yang dibutuhkan sebelum ini digunakan untuk trafik sungguhan.

Mengapa Groq Mengubah Pengalaman Streaming

Groq menjalankan model open source seperti Llama dan GPT OSS di atas perangkat keras LPU (Language Processing Unit) khusus, bukan GPU serba guna, sehingga chat completions API-nya mampu mempertahankan throughput token keluaran yang sangat tinggi. Untuk chatbot streaming, throughput ini lebih penting daripada sekadar kualitas model, karena kecepatan yang dirasakan pengguna pada antarmuka chat lebih ditentukan oleh seberapa cepat token berikutnya muncul setelah token pertama, bukan oleh total waktu permintaan.

  • Waktu hingga token pertama biasanya di bawah beberapa ratus milidetik, sehingga UI bisa mulai merender hampir seketika setelah pengguna menekan kirim.
  • Throughput token tetap tinggi bahkan pada respons yang panjang, sehingga jawaban lima ratus kata tidak terasa melambat di tengah jalan.
  • Bentuk API-nya kompatibel dengan OpenAI, jadi paket resmi groq-sdk dan pola streaming di bawah ini akan terasa familiar jika kamu pernah membangun fitur chat dengan OpenAI sebelumnya.

Pilih model berdasarkan throughput dan context window yang dibutuhkan use case-mu, bukan hanya skor benchmark. Model yang lebih kecil dan cepat yang streaming secara instan sering memberi pengalaman yang terasa lebih baik dibanding model besar yang sedikit lebih akurat tapi terlihat lambat saat mulai merespons.

Membangun Route Handler Streaming

Di App Router, route handler hanyalah fungsi yang menerima Web Request dan mengembalikan Web Response, artinya kamu bisa mengembalikan ReadableStream mentah alih-alih body JSON. Itulah persis yang dibutuhkan chat streaming: handler membuka koneksi ke Groq dengan stream diset true, lalu meneruskan setiap token sebagai server-sent event begitu token itu tiba.

Handler di bawah ini menyimpan sumber kebenaran percakapan di server, menyusun array messages dengan system prompt ditambah pesan pengguna yang masuk, lalu meneruskan setiap delta dari stream Groq ke klien menggunakan format standar Server-Sent Events, yaitu baris berawalan data titik dua diikuti payload JSON dan baris kosong. Membungkus seluruh pemanggilan dalam blok try dan finally penting di sini: jika permintaan ke Groq gagal di tengah jalan, blok finally tetap menutup controller sehingga koneksi klien tidak menggantung tanpa batas waktu.

// app/api/chat/route.ts
import { NextRequest } from "next/server"
import Groq from "groq-sdk"

const groq = new Groq({ apiKey: process.env.GROQ_API_KEY })

export async function POST(request: NextRequest) {
  const { message } = await request.json()

  const encoder = new TextEncoder()

  const stream = new ReadableStream({
    async start(controller) {
      try {
        const completion = await groq.chat.completions.create({
          model: "openai/gpt-oss-120b",
          messages: [
            { role: "system", content: "You are a helpful assistant." },
            { role: "user", content: message },
          ],
          max_completion_tokens: 1024,
          temperature: 0.7,
          stream: true,
        })

        for await (const chunk of completion) {
          const delta = chunk.choices[0]?.delta?.content
          if (delta) {
            controller.enqueue(
              encoder.encode(`data: ${JSON.stringify({ delta })}\n\n`)
            )
          }
        }
        controller.enqueue(encoder.encode('data: {"done":true}\n\n'))
      } catch (err) {
        controller.enqueue(
          encoder.encode('data: {"error":"stream_failed"}\n\n')
        )
      } finally {
        controller.close()
      }
    },
  })

  return new Response(stream, {
    headers: {
      "Content-Type": "text/event-stream; charset=utf-8",
      "Cache-Control": "no-cache, no-transform",
      Connection: "keep-alive",
    },
  })
}

Membaca Stream di Sisi Klien

Di sisi klien, kamu tidak memerlukan objek EventSource karena permintaannya adalah POST dengan body, bukan GET biasa. Sebagai gantinya, baca body respons langsung menggunakan reader streaming dari Fetch API, decode setiap potongan byte menjadi teks, lalu pisahkan berdasarkan baris kosong ganda yang memisahkan setiap server-sent event.

Simpan sementara (buffer) setiap event yang datang terpotong di antara dua chunk, karena TCP tidak menjamin satu event utuh selalu tiba dalam satu kali pembacaan. Setelah kamu melihat flag done pada payload, hentikan pembacaan dan finalisasi pesan di state UI-mu. Ada baiknya juga merender teks yang terkumpul setiap kali delta tiba alih-alih menunggu seluruh pesan selesai, karena repaint bertahap itulah inti visual dari streaming itu sendiri; klien yang menyimpan semuanya dulu sebelum memperbarui layar membuang manfaat latensi yang seharusnya diberikan throughput Groq.

// client component
async function sendMessage(message: string, onDelta: (text: string) => void) {
  const res = await fetch("/api/chat", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ message }),
  })

  const reader = res.body!.getReader()
  const decoder = new TextDecoder()
  let buffer = ""

  while (true) {
    const { value, done } = await reader.read()
    if (done) break

    buffer += decoder.decode(value, { stream: true })
    const lines = buffer.split("\n\n")
    buffer = lines.pop() ?? ""

    for (const line of lines) {
      if (!line.startsWith("data: ")) continue
      const payload = JSON.parse(line.slice(6))
      if (payload.delta) onDelta(payload.delta)
      if (payload.done) return
    }
  }
}

Jangan memanggil Groq API langsung dari browser dengan API key tertanam di kode klien. Setiap permintaan harus melewati route handler milikmu sendiri agar key tetap berada di server; key yang bocor di halaman chatbot publik bisa diambil dan dipakai untuk menjalankan trafik orang lain dengan biaya ditanggung akunmu dalam hitungan menit.

System Prompt yang Menjaga Asisten Tetap pada Topik

Chatbot yang menjawab apa saja adalah risiko dukungan dan penyalahgunaan. System prompt harus menyatakan tujuan tunggal asisten secara jelas, mendaftarkan hal yang harus ditolaknya, dan menyertakan pengingat singkat menjelang akhir prompt untuk menegaskan kembali batasan tersebut, karena percakapan panjang bisa melemahkan instruksi di awal. Menyisipkan string canary unik sebagai komentar HTML di dalam system prompt juga membantu; jika string itu muncul kembali terbawa dalam pesan pengguna, itu sinyal kuat bahwa seseorang berhasil mengekstrak prompt-mu dan sedang mengujinya kembali ke endpoint milikmu sendiri.

  • Nyatakan persona dan cakupan pada kalimat pertama: topik apa yang dijawab asisten, dan ke mana ia harus mengalihkan permintaan di luar itu.
  • Tambahkan instruksi eksplisit untuk mengabaikan permintaan apa pun yang disisipkan dalam input pengguna yang mencoba mengganti, menimpa, atau memperluas peran asisten, karena upaya prompt injection cukup umum bahkan pada chatbot portofolio atau dukungan bertrafik rendah.
  • Validasi dan batasi input pengguna di sisi server sebelum sampai ke model: terapkan panjang pesan maksimum, hapus karakter kontrol, dan tolak frasa injection yang dikenal lewat pemeriksaan regex sebelum menghabiskan satu permintaan Groq untuk itu.

Rate Limiting dan Kontrol Biaya

Groq menerapkan batas per API key di beberapa dimensi: permintaan per menit, token per menit, dan batas harian untuk keduanya. Melampaui batas mana pun akan mengembalikan status 429 disertai header retry-after yang memberitahu berapa detik harus menunggu. Aplikasimu sendiri sebaiknya menerapkan batas yang lebih ketat per pengunjung jauh sebelum menyentuh batas Groq, baik untuk mengendalikan biaya maupun mencegah satu klien yang menyalahgunakan sistem membuat pengguna lain kelaparan sumber daya.

TingkatanBatas permintaan umumBatas token umum
Tingkat gratis30 permintaan per menit6.000 token per menit untuk sebagian besar model
Tingkat developerHingga 1.000 permintaan per menitHingga kurang lebih 250.000 sampai 300.000 token per menit
Melebihi batas mana punRespons 429 Too Many RequestsHeader retry-after menyatakan waktu tunggu dalam detik

Sebelum memanggil Groq, tambahkan rate limiter per pengunjung berdasarkan alamat IP, dibatasi jauh di bawah batas paketmu. Batasi juga max_completion_tokens pada setiap permintaan; panjang completion yang tidak dibatasi adalah cara termudah membuat model yang murah per token menjadi tagihan mahal ketika pengguna (atau scraper) mengirim prompt yang sama berulang-ulang.

Checklist Produksi Sebelum Rilis

Chat streaming terlihat sederhana dalam demo tetapi bisa gagal dengan cara yang spesifik dan bisa diprediksi di produksi. Periksa daftar ini sebelum mengarahkan trafik sungguhan ke route handler, dan tinjau ulang setelah rilis begitu pola pemakaian nyata mulai terlihat di log-mu.

  1. Simpan API key hanya di sisi server, baca dari environment variable, dan jangan pernah dikembalikan dalam respons atau log yang terlihat klien.
  2. Terapkan rate limiting per pengunjung di servermu sendiri selain mengandalkan batas paket Groq, dan kembalikan nilai retry-after yang jelas ke klien.
  3. Batasi max_completion_tokens dan validasi panjang pesan masuk agar satu permintaan tidak bisa membengkakkan pemakaian token atau biaya.
  4. Tangani error stream dengan baik: jika pemanggilan Groq gagal di tengah stream, kirim event error yang bisa ditampilkan klien alih-alih membiarkan koneksi menggantung.
  5. Catat pemakaian token per permintaan (Groq mengembalikan data usage) sehingga kamu bisa menangkap lonjakan biaya akibat bug atau penyalahgunaan sebelum tagihan bulanan yang menyadarkanmu.
FAQ

Pertanyaan Umum

Jawaban singkat untuk pertanyaan yang paling sering diajukan pembaca tentang topik ini.

Mengapa memakai Groq alih-alih penyedia LLM lain untuk chatbot streaming?

Groq menjalankan model open source di atas perangkat keras LPU khusus, bukan GPU serba guna, sehingga menghasilkan throughput token keluaran yang sangat tinggi dan waktu ke token pertama yang rendah. Untuk UI chat streaming, hal ini langsung berarti pengalaman yang terasa lebih cepat, karena kecepatan yang dirasakan bergantung pada seberapa cepat token terus muncul setelah token pertama, bukan total waktu permintaan.

Bagaimana cara melakukan streaming chat completion Groq di route handler Next.js?

Set stream menjadi true saat memanggil groq.chat.completions.create, lalu iterasi generator async yang dikembalikan dengan loop for-await di dalam callback start milik ReadableStream. Konten delta setiap chunk di-encode sebagai server-sent event dan dimasukkan ke controller, dan Response dibuat dengan stream tersebut serta content type text/event-stream.

Bisakah saya membaca respons streaming Next.js API tanpa library di sisi klien?

Bisa. response.body pada Fetch API menyediakan ReadableStream yang bisa dibaca langsung dengan getReader, mendekode setiap chunk dengan TextDecoder lalu memisahkannya berdasarkan baris kosong ganda yang memisahkan setiap server-sent event. Tidak perlu EventSource atau dependency tambahan, yang penting di sini karena EventSource hanya mendukung permintaan GET, bukan POST.

Guardrail system prompt apa yang dibutuhkan chatbot produksi?

Nyatakan tujuan tunggal dan cakupan topik asisten pada kalimat pertama, tambahkan instruksi eksplisit untuk mengabaikan permintaan tersisip dalam input pengguna yang mencoba mengganti peran asisten, dan ulangi batasan cakupan itu menjelang akhir prompt agar tetap bertahan pada percakapan panjang. Padukan ini dengan validasi sisi server yang membatasi panjang pesan dan menolak frasa prompt injection yang dikenal sebelum permintaan sampai ke model.

Bagaimana mengendalikan biaya dan menghindari batas rate limit Groq?

Terapkan rate limit per pengunjung milikmu sendiri, berdasarkan alamat IP, diset jauh di bawah batas paket Groq untuk permintaan dan token per menit, serta batasi max_completion_tokens pada setiap permintaan agar satu balasan tidak bisa menghabiskan token tanpa batas. Groq juga mengembalikan data usage per respons, sehingga mencatatnya membantu menangkap lonjakan biaya akibat bug atau klien yang menyalahgunakan sebelum tagihan bulanan yang menyadarkanmu.