Skip to content
Konsultasi

Satu Stream SSE, Dua Versi Jawaban

Adityo Guni Waluyo

Jawaban AI di search dialog butuh dua versi: ringkasan cepat dan penjelasan lengkap. Satu SSE stream dari LLM, tapi UI perlu split — caranya pakai marker-based parsing.

Klik "Tanya Adityo", ketik pertanyaan, tekan Enter. Jawaban muncul dari SSE stream sebagai satu blok teks panjang. User cuma mau baca dua kalimat pertama, tapi kalo butuh detail harus geser terus lewat teks yang panjang. Jadi butuh dua versi dari satu generation yang sama, ringkasan cepat di bubble dan penjelasan lengkap di panel yang bisa di-expand.

Awalnya saya pikir simpel: ambil dua kalimat pertama dari jawaban sebagai summary pakai fungsi firstSentences. sisanya jadi penjelasan. Tapi LLM nggak konsisten. Kadang ringkasan ada di tengah, kadang di akhir, kadang nggak ada sama sekali. Mencoba potong berdasarkan posisi kalimat pecah di production karena output model berubah tiap kali.

Jadi paksa aja dari promptnya.

Marker-based splitting

System prompt sekarang minta output dua bagian: baris pertama diawali RINGKAS: lalu ringkasan maksimal dua kalimat (40 kata), baris berikutnya diawali PENJELASAN: lalu penjelasan lengkap (220 kata). Keduanya dipisah oleh marker teks, dan LLM tetap generate satu stream. Yang penting, ada anchor teks yang bisa di-parsing.

Kenapa nggak pakai structured output kayak JSON? Karena ini SSE streaming. Kalo dipaksa JSON, harus nunggu seluruh output selesai baru bisa parse. Dengan marker, parsing bisa dilakukan mid-stream. Begitu PENJELASAN muncul di delta, frontend langsung tahu di mana batasnya.

Di backend, saya nambah konstanta _ASK_ENHANCED_MARKER dan yield event baru AskEvent("enhanced", ...) setelah marker PENJELASAN terdeteksi di stream. Routes kumpulkan enhanced_parts terpisah dari answer_parts, lalu fungsi replay_frames emit enhanced event ke client.

Cache payload yang dulunya tiga field sekarang jadi empat: sources, answer, enhanced, followups. Bubble hanya simpan summary tanpa marker, enhanced text hidup terpisah. Replay dari cache emit kedua event tanpa pernah melempar raw marker ke UI.

Frontend di case delta strip marker PENJELASAN mid-stream supaya nggak muncul di bubble utama. Case enhanced yang baru accumulation: teks setelah marker ditampilkan di panel kanan, user bisa expand kalo mau baca lengkap. Backward compatible: jawaban lama tanpa field enhanced fallback ke bubble text biasa.

Typed events vs markers

Momen ini bikin saya baca ulang SSE spec. WHATWG sendiri sebenarnya udah support typed events via field event. Penulis bisa pisahkan event dengan tipe berbeda, klien tinggal listen per tipe [1]. OpenAI pakai semantic events kayak response.created dan output_text.delta di Responses API [2]. Vercel AI SDK punya typed parts mulai dari text-start sampai reasoning dan custom [3]. NeuralSummary blog bahkan udah pake enam typed events terpisah: session, status, token, citations, done, error [4].

Tapi situasi saya beda. Ini bukan soal tipe data yang berbeda dalam satu stream, tapi soal memecah satu output jadi dua view dari payload yang sama. Typed events emang bagus untuk membedakan text, reasoning, dan citations. Untuk kasus "satu jawaban, dua representasi", marker-based splitting lebih ringan, nggak perlu ubah protocol level, cukup koordinasi prompt + parser + renderer.

Ada IETF draft yang usulkan standard wire format untuk LLM inference streaming [5], tapi itu masih draft. Dan standardnya fokus ke raw inference tokens, bukan ke application-level splitting kayak yang saya butuhin.

Tiga komponen harus sinkron

Satu stream, dua versi jawaban. Backend harus tahu kapan marker muncul dan yield event enhanced terpisah. Cache harus simpan split-nya terpisah: summary di answer, detail di enhanced. Frontend harus strip marker mid-stream dan render kedua panel. Tiga komponen ini harus sinkron — kalo salah satu miss, user bakal lihat PENJELASAN muncul di bubble, atau panel kanan kosong.

Test covering 110 kasus, termasuk yang baru: about-mode splitting, marker-free cache payload, single-frame replay dari cache. Kalo ada regresi di salah satu layer, test langsung ketangkap.

Setelah sync, ini bisa diskalakan ke format output terstruktur lain tanpa ubah protocol. Tinggal tambah marker baru, yield event baru, dan frontend handle event baru. SSE-nya tetap same old text/event-stream.

Sumber:

[1] Server-Sent Events — WHATWG Living Standard

[2] OpenAI Responses API — Streaming

[3] Vercel AI SDK — Data Stream Protocol

[4] NeuralSummary Blog — Typed Events Architecture

[5] IETF Draft — LLM-Stream Wire Format

Artikel terkait