SEJATIDIMEDIA Logo
Tersedia Sesi Konsultasi Gratis
SejatiDimedia Logo

Streaming Respons AI di Laravel (SSE)

Sekarang, chatbot kita nunggu AI-nya selesai generate seluruh respons dulu baru ditampilkan di layar. Untuk jawaban pendek ini nggak masalah, tapi untuk respons yang panjang, user cuma bengong lihat loading kosong selama beberapa detik. Itu bukan pengalaman yang enak, dan ini salah satu alasan kenapa tool seperti ChatGPT nampilin teks muncul kata demi kata, bukan langsung semuanya sekaligus. Efek kata-demi-kata itu disebut streaming. Di artikel ini, kita akan implementasikan itu di Laravel pakai Server-Sent Events (SSE), yang setup-nya lebih sederhana dibanding WebSocket dan sudah cukup untuk kebanyakan use case chatbot.

Timur Dian Radha Sejati

Timur Dian Radha Sejati

Founder SejatiDimedia•22 September 2026•5 menit baca
Silabus Seri Rekayasa

Laravel & AI Integration

Menghubungkan Laravel dengan model AI sekarang makin gampang, tapi bikin fiturnya stabil dan hemat resource itu cerita lain. Seri ini fokus ngebahas arsitektur praktis integrasi AI di Laravel: mulai dari manajemen queue untuk task berat, implementasi RAG sederhana, parsing structured output, sampai tips ngamanin API cost biar nggak boncos

Lihat Silabus Lengkap
Streaming Respons AI di Laravel (SSE)

SSE vs WebSocket, Mana yang Kamu Butuhkan

Keduanya memungkinkan server mengirim data ke browser tanpa browser harus terus-menerus minta update. Bedanya:

  • SSE (Server-Sent Events) itu satu arah, dari server ke browser saja, dan jalan di atas HTTP biasa. Setup-nya lebih sederhana dan pas banget untuk kebutuhan kita menampilkan teks AI yang mengalir.
  • WebSocket itu dua arah dan butuh koneksi yang terus terbuka, setup-nya lebih ribet (seperti Laravel Reverb atau Pusher), dan lebih cocok untuk hal seperti chat room multi-user atau kolaborasi real-time.

Karena chatbot kita cuma perlu kirim teks hasil AI ke satu user dalam satu waktu, SSE adalah pilihan yang tepat di sini. Kita baru butuh WebSocket nanti kalau bikin fitur multi-user atau broadcast.

Cara Kerja Streaming, dengan Bahasa Sederhana

Daripada provider AI-nya kirim satu respons besar di akhir, dia kirim potongan-potongan teks kecil (chunk) terus-menerus seiring proses generate-nya jalan. Aplikasi Laravel-mu menerima tiap chunk dan langsung meneruskannya ke browser, bukannya nunggu kumpulin dulu seluruh respons.

Langkah 1: Streaming Pakai HTTP Client Laravel

Kalau kamu manggil API OpenAI langsung, kamu perlu bilang ke API-nya supaya stream responsnya, lalu baca chunk-chunk yang datang:

ChatStreamController.phpphp
1
<?php
2
3
namespace App\Http\Controllers;
4
5
use Illuminate\Http\Request;
6
use Illuminate\Support\Facades\Http;
7
8
class ChatStreamController extends Controller
9
{
10
public function stream(Request $request)
11
{
12
$message = $request->query('message');
13
14
return response()->stream(function () use ($message) {
15
$response = Http::withToken(config('services.openai.key'))
16
->withOptions(['stream' => true])
17
->post('https://api.openai.com/v1/chat/completions', [
18
'model' => config('services.openai.model'),
19
'stream' => true,
20
'messages' => [
21
['role' => 'user', 'content' => $message],
22
],
23
]);
24
25
$body = $response->toPsrResponse()->getBody();
26
27
while (!$body->eof()) {
28
$line = $this->readLine($body);
29
30
if (str_starts_with($line, 'data: ')) {
31
$data = substr($line, 6);
32
33
if (trim($data) === '[DONE]') {
34
break;
35
}
36
37
$decoded = json_decode($data, true);
38
$text = $decoded['choices'][0]['delta']['content'] ?? '';
39
40
if ($text !== '') {
41
echo "data: " . json_encode(['text' => $text]) . "\n\n";
42
ob_flush();
43
flush();
44
}
45
}
46
}
47
}, 200, [
48
'Cache-Control' => 'no-cache',
49
'Content-Type' => 'text/event-stream',
50
'X-Accel-Buffering' => 'no',
51
]);
52
}
53
54
private function readLine($body): string
55
{
56
$line = '';
57
58
while (!$body->eof()) {
59
$char = $body->read(1);
60
$line .= $char;
61
62
if ($char === "\n") {
63
break;
64
}
65
}
66
67
return trim($line);
68
}
69
}
69 baris•1950 chars
UTF-8•Spaces: 4

Detail penting di sini adalah

PLAINTEXT Snippetplaintext
1
'X-Accel-Buffering' => 'no'
1 baris•27 chars
UTF-8•Spaces: 4
. Tanpa ini, Nginx (dan beberapa setup hosting) akan buffer seluruh respons dulu sebelum dikirim, yang malah menghilangkan tujuan dari streaming. Perhatikan juga
PLAINTEXT Snippetplaintext
1
ob_flush()
1 baris•10 chars
UTF-8•Spaces: 4
dan
PLAINTEXT Snippetplaintext
1
flush()
1 baris•7 chars
UTF-8•Spaces: 4
setelah tiap chunk, ini memaksa PHP untuk benar-benar kirim data secara langsung, bukan menahannya di buffer.

Langkah 2: Streaming Pakai Prism PHP

Kalau kamu pakai Prism dari Part 4, streaming-nya jauh lebih sederhana karena sudah built-in:

script.phpphp
1
use Prism\Prism\Facades\Prism;
2
use Prism\Prism\Enums\Provider;
3
 
4
Route::get('/chat/stream', function (Request $request) {
5
return response()->stream(function () use ($request) {
6
$stream = Prism::text()
7
->using(Provider::OpenAI, 'gpt-4o-mini')
8
->withPrompt($request->query('message'))
9
->asStream();
10
 
11
foreach ($stream as $chunk) {
12
echo "data: " . json_encode(['text' => $chunk->text]) . "\n\n";
13
ob_flush();
14
flush();
15
}
16
}, 200, [
17
'Cache-Control' => 'no-cache',
18
'Content-Type' => 'text/event-stream',
19
'X-Accel-Buffering' => 'no',
20
]);
21
});
21 baris•662 chars
UTF-8•Spaces: 4

Prism juga punya

PLAINTEXT Snippetplaintext
1
asEventStreamResponse()
1 baris•23 chars
UTF-8•Spaces: 4
, yang otomatis mengurus format SSE-nya untuk kamu:

script.phpphp
1
Route::get('/chat/stream', function (Request $request) {
2
return Prism::text()
3
->using(Provider::OpenAI, 'gpt-4o-mini')
4
->withPrompt($request->query('message'))
5
->asEventStreamResponse();
6
});
6 baris•218 chars
UTF-8•Spaces: 4

Ini versi paling sederhana dan sudah cukup untuk kebanyakan kasus. Pakai versi manual di atas kalau kamu butuh format custom atau logika tambahan di antara chunk-chunk-nya.

Langkah 3: Menerima Stream di Browser

Di frontend, pakai API

PLAINTEXT Snippetplaintext
1
EventSource
1 baris•11 chars
UTF-8•Spaces: 4
bawaan browser untuk mendengarkan chunk yang masuk:

HTML Snippethtml
1
<div id="chat-output"></div>
2
<input type="text" id="message-input" placeholder="Ketik pesan...">
3
<button onclick="sendMessage()">Kirim</button>
4
 
5
<script>
6
function sendMessage() {
7
const message = document.getElementById('message-input').value;
8
const output = document.getElementById('chat-output');
9
output.innerText = '';
10
11
const eventSource = new EventSource(`/chat/stream?message=${encodeURIComponent(message)}`);
12
13
eventSource.onmessage = (event) => {
14
const data = JSON.parse(event.data);
15
output.innerText += data.text;
16
};
17
18
eventSource.onerror = () => {
19
eventSource.close();
20
};
21
}
22
</script>
22 baris•645 chars
UTF-8•Spaces: 4

Setiap kali chunk datang, kita tambahkan ke elemen output, sehingga terbentuk efek kata-demi-kata.

Jebakan yang Sering Terjadi

1. Buffering di server memblokir streaming Meski kode PHP-nya sudah benar, beberapa setup server tetap buffer output secara default. Kalau streaming-nya kelihatan nggak jalan, cek config Nginx-mu untuk

PLAINTEXT Snippetplaintext
1
proxy_buffering
1 baris•15 chars
UTF-8•Spaces: 4
dan matikan itu untuk route streaming-nya, dan pastikan
PLAINTEXT Snippetplaintext
1
zlib.output_compression
1 baris•23 chars
UTF-8•Spaces: 4
di
PLAINTEXT Snippetplaintext
1
php.ini
1 baris•7 chars
UTF-8•Spaces: 4
juga mati.

2. Pakai

PLAINTEXT Snippetplaintext
1
php artisan serve
1 baris•17 chars
UTF-8•Spaces: 4
di lokal sudah cukup buat testing Ini menangani streaming tanpa config tambahan, jadi cara yang bagus untuk memastikan kodemu benar sebelum debug masalah buffering di level server.

3. Laravel Telescope bisa mengganggu Kalau kamu pakai Telescope, dia bisa mencegat event dari HTTP client dan "makan" stream-nya sebelum kodemu sempat baca. Matikan HTTP watcher milik Telescope untuk route yang butuh streaming.

4. Menutup koneksi Selalu panggil

PLAINTEXT Snippetplaintext
1
eventSource.close()
1 baris•19 chars
UTF-8•Spaces: 4
begitu kamu deteksi responsnya sudah selesai (atau kalau error), kalau nggak koneksi browsernya tetap terbuka tanpa perlu.

Selanjutnya

Streaming bikin pengalaman chat terasa lebih cepat, tapi masih ada masalah yang belum kita tangani: gimana kalau respons AI-nya butuh waktu lama buat mulai, atau kalau banyak user pakai fitur AI-mu bersamaan? Di artikel selanjutnya, kita akan pindahkan proses AI yang berat ke queue dan background job, supaya aplikasimu tetap responsif meski lagi ramai.


Ini adalah Part 6 dari series "Laravel + AI Integration".

Topik Terkait:LaravelArchitecture
Timur Dian Radha Sejati

Timur Dian Radha Sejati

Penulis & Lead Engineer

Software engineer dan konsultan sistem di SejatiDimedia. Berfokus pada perancangan arsitektur berkinerja tinggi, refactoring backend skala enterprise, hingga pengembangan aplikasi mobile, web modern dan integrasi AI.

Punya Masalah Arsitektur atau Ingin Membangun Sistem yang Benar?

Kami siap membantu mengaudit kode, me-refactor arsitektur yang lemot, atau membangun aplikasi bisnis Anda dengan standar enterprise sejak awal.

EKSPLORASI LANJUTAN

Artikel Rekayasa Terkait

Lihat Semua
Membangun Sistem RAG Sederhana dengan Laravel + Vector Database
Backend, AI
6 menit baca

Membangun Sistem RAG Sederhana dengan Laravel + Vector Database

Sejauh ini chatbot kita cuma bisa jawab berdasarkan apa yang sudah diketahui model AI-nya dari training. Coba tanya soal dokumentasi internal perusahaanmu, harga spesifik produkmu, atau catatan rapat minggu lalu, AI-nya nggak akan bisa jawab, karena memang nggak pernah dilatih dengan data itu. Di sinilah RAG berperan, singkatan dari Retrieval Augmented Generation. Daripada cuma mengandalkan data training model, kita ambil dulu potongan-potongan relevan dari dokumen kita sendiri, lalu kasih itu ke AI sebagai konteks sebelum kita ajukan pertanyaan. Di artikel ini, kita akan bangun versi sederhana dari ini pakai Laravel dan PostgreSQL dengan extension pgvector.

Baca Artikel
Menggunakan Prism PHP untuk Abstraksi Provider AI
Backend, AI
5 menit baca

Menggunakan Prism PHP untuk Abstraksi Provider AI

Artikel ini mengenalkan Prism PHP, package yang memberikan satu interface konsisten untuk banyak provider AI, alih-alih menulis kode custom per provider. Dibahas cara instalasi, penggunaan dasar, menulis ulang service class chatbot dari Part 3 pakai message chain milik Prism, membuat provider bisa dikonfigurasi lewat .env, fitur bawaan yang berguna seperti retry dan pelacakan usage, sampai kapan sebenarnya perlu pakai Prism dibanding manggil API langsung.

Baca Artikel
Membangun Chatbot Sederhana dengan Laravel + LLM API
Backend, AI
5 menit baca

Membangun Chatbot Sederhana dengan Laravel + LLM API

Artikel ini mengembangkan service AI dasar dari Part 2 menjadi chatbot yang bisa mengingat riwayat percakapan. Dibahas kenapa API LLM itu stateless, cara update service class agar menerima seluruh riwayat pesan, membuat chat controller dan view Blade sederhana dengan penyimpanan di session, sampai catatan praktis soal batasan session dan cara menangani batas token saat percakapan makin panjang.

Baca Artikel
Streaming Respons AI di Laravel (SSE) | SejatiDimedia Insights | SejatiDimedia