Wanneer je een applicatie bouwt die gebruikmaakt van grote taalmodellen (LLM's) zoals Claude of DeepSeek, merk je al snel dat tekstgeneratie tijd kost. Omdat modellen tekst token voor token voorspellen, kan een lang antwoord secondenlang op zich laten wachten. Zonder streaming kijkt je gebruiker in die tijd naar een lege laad-indicator. Met streaming responses los je dit op.
Hoe werkt het? (Server-Sent Events)
Vrijwel alle moderne LLM-API's gebruiken Server-Sent Events (SSE) voor streaming. In plaats van de HTTP-verbinding te sluiten zodra één grote JSON-payload is berekend, houdt de server de verbinding open.
Terwijl het model tokens genereert, pusht de API deze direct naar de client als losse stukjes tekst. Zodra de stroom voltooid is, stuurt de server een specifieke terminator, vaak [DONE], waarna de verbinding sluit.
UX-voordeel: De belangrijkste metriek hier is Time-to-First-Token (TTFT). Omdat de gebruiker de eerste woorden direct op het scherm ziet verschijnen, voelt de applicatie razendsnel, zelfs als de totale generatietijd hetzelfde blijft.
Wanneer is streaming onmisbaar?
Bij simpele, korte datatransformaties (zoals JSON-extractie of classificatie) op de backend kun je vaak beter wachten op het volledige antwoord. Streaming is echter essentieel in de volgende scenario's:
- Chat-interfaces: Voor een natuurlijke, mensachtige respons-ervaring.
- RAG-applicaties met vector search: Omdat de retrieval-stap (het doorzoeken van je vector database) al latentie toevoegt, wil je de daaropvolgende LLM-generatie direct zichtbaar maken om de wachttijd voor de eindgebruiker te maskeren.
Valkuilen bij implementatie
Het bouwen van een robuuste streaming-client kent enkele uitdagingen:
- Markdown Rendering: Omdat tokens incrementeel binnenkomen, is je Markdown-structuur in real-time vaak incompleet (bijv. een geopende codeblok zonder sluiting). Gebruik een parser die vergevingsgezind is voor incomplete syntax om UI-geflikker te voorkomen.
- Error Handling: Bij een time-out midden in een stream geeft SSE niet altijd een standaard HTTP 500-fout via de headers. Je moet foutmeldingen binnen de datastroom afvangen.
- Token Billing: Vroeger gaven streaming-API's geen totaal aantal tokens terug. Tegenwoordig kun je bij de meeste providers via een parameter (zoals
stream_options: {"include_usage": true}) de finale rekenstatistieken in de laatste chunk meesturen.
Pseudocode: SSE Stream Consumeren
Hieronder zie je de basislogica om een streaming endpoint te bevragen in de frontend. We gebruiken de standaard Fetch API om de ReadableStream te decoderen.
async function fetchLLMStream(prompt) {
const response = await fetch('https://api.example.com/v1/chat/completions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: "jouw-gekozen-model",
messages: [{ role: "user", content: prompt }],
stream: true // Zet streaming aan
})
});
const reader = response.body.getReader();
const decoder = new TextDecoder("utf-8");
while (true) {
const { value, done } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split('\n').filter(line => line.trim() !== '');
for (const line of lines) {
if (line === 'data: [DONE]') return;
if (line.startsWith('data: ')) {
const data = JSON.parse(line.slice(6));
const token = data.choices[0]?.delta?.content || "";
// Update de UI incrementeel
appendToChatInterface(token);
}
}
}
}
Met deze opzet zorg je voor vloeiende, realtime interactie, ongeacht de totale denkkracht die het model nodig heeft.