Ein Unternehmen startete einen KI-Assistenten zur Bearbeitung von Kundenanfragen. Das Modell generierte eine Antwort in 4–6 Sekunden. Nutzer klickten doppelt auf „Senden“, weil sie dachten, es passiere nichts. Nach der Aktivierung des Streamings generierte dasselbe Modell die ersten Wörter nach 400 ms. Die Gesamtzeit änderte sich nicht. Die Anzahl der Doppelklicks sank um 80%.
Das ist das Wesen des Streamings: Es beschleunigt nicht die Inference, sondern verändert die Wahrnehmung des Wartens.
Was ist Streaming und warum verändert es die UX#
Ein standardmäßiger LLM-Aufruf sieht wie folgt aus: Der Client sendet eine Anfrage, das Modell generiert alle Token im Speicher, der Server überträgt die vollständige Antwort. Der Nutzer sieht für volle 4–6 Sekunden eine leere Seite, dann plötzlich den vollständigen Text.
Token-für-Token-Streaming kehrt die Übertragungsreihenfolge um. Das Modell generiert einen Token, der Server sendet ihn, der Browser rendert ihn. Der Nutzer sieht den Text Buchstabe für Buchstabe wachsen, genau wie bei ChatGPT. Psychologisch ist das ein fundamentaler Unterschied: Das System wirkt responsiv, selbst wenn es rechnerisch genauso langsam ist.
Zwei Metriken sind im Kontext von Streaming relevant:
| Metrik | Was sie misst | Richtwerte (2026) |
|---|---|---|
| TTFT (Time to First Token) | Zeit vom Absenden der Anfrage bis zum ersten Token | 200–900 ms (Cloud-API), 80–400 ms (lokaler GPU) |
| Throughput (Token/s) | Generierungsgeschwindigkeit nach dem ersten Token | 20–80 tok/s (lokaler GPU 7B–13B) |
| Gesamtantwortzeit | TTFT + (Anzahl Token / Throughput) | 2–15 s für eine typische Antwort |
Ohne Streaming wartet der Nutzer auf die Gesamtzeit. Mit Streaming wartet er nur auf die TTFT, der Rest kommt progressiv. Für eine Antwort mit 300 Token bei einem Throughput von 40 tok/s beträgt die Gesamtzeit ca. 7,5 s. Eine TTFT von 400 ms bedeutet, dass der Nutzer nach 0,4 s zu lesen beginnt, nicht nach 7,5 s.
SSE-Architektur: Wie man es richtig aufbaut#
Server-Sent Events (SSE) ist ein HTTP-Protokoll, bei dem der Server eine offene Verbindung aufrechterhält und Text-Ereignisse im Format data: {...}\n\n sendet. Der Client (Browser oder API-Client) empfängt jedes Ereignis separat und rendert es progressiv.
Minimaler Ablauf auf Serverseite (Python/FastAPI):
from fastapi.responses import StreamingResponse
async def generate_stream(prompt: str):
async for token in llm.stream(prompt):
yield f"data: {json.dumps({'token': token})}\n\n"
yield "data: [DONE]\n\n"
@app.post("/v1/chat/stream")
async def chat_stream(req: ChatRequest):
return StreamingResponse(
generate_stream(req.prompt),
media_type="text/event-stream",
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
)
Vier Dinge, die Streaming in der Produktion am häufigsten stören:
Puffernde Proxys. Nginx puffert Antworten standardmäßig. Der Header X-Accel-Buffering: no deaktiviert das Puffern für einen bestimmten Endpunkt. Ohne diesen Header erhält der Client den gesamten Stream auf einmal nach Abschluss, genau wie ohne Streaming.
BFF-Schicht mit gzip. Wenn Next.js oder ein anderer BFF Anfragen über rewrite umschreibt, puffert die gzip-Komprimierung Chunks bis zu einer Schwellengröße. Lösung: File Route Handler, der upstream.body direkt zurückgibt, mit dem Header Cache-Control: no-transform. Bei Cashcrown hatten wir dieses Problem beim Aufbau des Playgrounds, wo der standardmäßige BFF-Rewrite SSE für Browser mit gzip puffert, bis wir auf einen direkten Handler umgestiegen sind.
Fehlender Heartbeat. SSE-Verbindungen, die 30–60 s inaktiv sind, werden von Load Balancern und CDNs geschlossen. Sende alle 15–20 s einen Kommentar ": ping\n\n" während langer Generierungen.
Backpressure. Wenn der Client langsamer liest als der Server schreibt (z. B. langsame mobile Verbindung), wächst der Puffer der Warteschlange. Die Implementierung sollte die Größe der Warteschlange pro Verbindung begrenzen und sie bei Überschreitung schließen, anstatt Daten unendlich zu halten. Bei Cashcrown verwenden wir eine asyncio.Queue(100) pro Verbindung mit automatischem Schließen bei Überschreitung des Limits.
Guardrails im Stream: Was funktioniert, was nicht#
Streaming verkompliziert die Validierung. Bei einer vollständigen Antwort kann der Guardrail den gesamten Text vor dem Senden prüfen. Beim Streaming hat man nur einen Ausschnitt.
Drei in der Praxis angewandte Ansätze:
Eingangs-Guardrail (pre-stream). Arbeitet mit dem Prompt vor dem Senden an das Modell. Prüft auf Injection, verbotene Themen, PII. Wenn der Prompt nicht besteht, startet der Stream nie. Das ist das einfachste und effektivste Muster. Jede Anfrage bei Cashcrown durchläuft screen() vor der Initialisierung von SSE.
Ausgangs-Guardrail mit Fensterpuffer. Halte einen Puffer der letzten N Token und prüfe jedes neue Fenster auf verbotene Muster. Du kannst den Stream mitten drin stoppen, indem du ein Fehlerereignis sendest. Nachteil: Der Nutzer sieht die Hälfte der Antwort und dann eine Fehlermeldung. Wende dies nur bei schweren Verstößen an (sensible Daten, Content Policy).
Post-Stream-Review für irreversible Aktionen. Wenn die Antwort zu einer irreversiblen Aktion führt (Zahlungsbestätigung, E-Mail-Versand, Datenbankeintrag), führe sie niemals basierend auf dem Stream allein aus. Puffere die vollständige Antwort, lasse sie durch einen vollständigen Guardrail laufen und warte auf menschliche Bestätigung. Streaming ist eine UX-Schicht, keine Korrektheitsschicht. Irreversible Entscheidungen erfordern vollständige Validierung und menschliche Freigabe.
Stream-Abbruch und Ressourcenbereinigung#
Der Nutzer kann auf „Stop“ klicken, während die Generierung läuft. Auf Browser-Seite schließt AbortController die SSE-Verbindung. Auf Serverseite muss auf request.is_disconnected() (FastAPI/Starlette) oder Äquivalent gelauscht und die Generierungsschleife abgebrochen werden.
async def generate_stream(prompt: str, request: Request):
async for token in llm.stream(prompt):
if await request.is_disconnected():
break # Generierung stoppen, Semaphoren freigeben
yield f"data: {json.dumps({'token': token})}\n\n"
Fehlende Abbruchbehandlung bedeutet, dass das Modell im Hintergrund weiter generiert, GPU und Parallelitätssemaphor belegt, selbst wenn der Client nicht mehr zuhört. Bei vielen gleichzeitigen Nutzern führt dies zur Erschöpfung des Pools und zu erhöhter Latenz für alle.
Der Artikel Kosten von LLM-Tokens: Wie man sie misst und optimiert beschreibt Semaphor-Muster und Aufrufbudgets, die mit der Streaming-Pipeline zusammenarbeiten.
Observability: Was beim Streaming gemessen werden muss#
Traditionelle HTTP-Metriken (Gesamtzeit) reichen nicht aus, um die Qualität des Streamings zu bewerten. Vier zusätzliche Messpunkte sind erforderlich.
TTFT pro Anfrage. Zeitstempel beim ersten Token minus Zeitstempel des Anfragesendens. Das Ziel hängt vom Use Case ab: Für einen Chat-Assistenten sind unter 600 ms komfortabel, für Dokumentengenerierung sind unter 2 s akzeptabel.
Throughput pro Stream. Gesendete Token geteilt durch die Zeit vom ersten bis zum letzten Token. Ein Rückgang des Throughputs signalisiert GPU-Überlastung oder Netzwerk-Backpressure.
Abbruchrate (cancellation rate). Prozentsatz der Streams, die vom Client vor dem letzten Token abgebrochen werden. Eine hohe Rate (über 15–20%) bedeutet, dass Nutzer die Geduld verlieren. Ursachen: Zu lange TTFT, unzureichende Antwortqualität, UX-Probleme in der Oberfläche.
Partial output errors. Streams, die nach dem Senden mindestens eines Tokens mit einem Fehler enden. Dies ist eine schwierigere Kategorie als ein vollständiger Fehler, da der Nutzer einen abgebrochenen Text sieht.
Muster zur Erfassung dieser Metriken in der Observability: Bei jedem SSE-Ereignis Telemetrie (Zeitstempel, Token-Anzahl, Verbindungs-ID) in einen Ringpuffer schreiben. Beim Schließen der Verbindung in das Metriksystem flushen. Mehr über die Observability-Schicht für KI-Agenten beschreibt Monitoring der Qualität von KI-Agenten.
Wann Streaming keinen Sinn ergibt#
Streaming verbessert die UX bei textuellen Antworten für Menschen. Es verbessert sie nicht und kann sie in einigen Szenarien sogar verkomplizieren:
Bei structured output (JSON, XML) ist ein partieller Output ungültig, bis die Struktur geschlossen ist. Das Streamen von JSON-Feldern führt zu Situationen, in denen der Client {"ergebnis": "poz erhält und warten muss. Besser ist es, zu puffern und das vollständige JSON nach Schema-Validierung zu senden.
Bei Batch-Verarbeitung (Dokumentenverarbeitung ohne Benutzeroberfläche) ist TTFT für keinen Menschen relevant. Der Overhead des Streamings (Aufrechterhaltung von Verbindungen) ist ein Kostenfaktor ohne Nutzen.
Bei kurzen Antworten (unter 20–30 Token) ist der Unterschied zwischen TTFT und Gesamtzeit zu gering, um wahrnehmbar relevant zu sein. Ein Ein-Satz-Klassifikator benötigt kein Streaming.
Der Artikel Lokale LLM: Welche Hardware und GPU Sie wirklich benötigen diskutiert Throughput als Kriterium für die Hardwareauswahl in Produktions-Pipelines.
FAQ#
Beschleunigt Streaming die Token-Generierung durch das Modell?#
Nein. Streaming verändert nicht die Geschwindigkeit der Token-Generierung. Das LLM generiert einen Token nach dem anderen in einer autoregressiven Schleife, unabhängig vom Übertragungsmodus. Streaming verändert nur den Zeitpunkt der Auslieferung an den Client: Jeder Token wird sofort nach der Generierung gesendet, anstatt auf das Ende der gesamten Antwort zu warten. Der Effekt ist wahrnehmungsbezogen, aber entscheidend: Eine TTFT von 400 ms statt 6 s ist der Unterschied zwischen einem „responsiven“ und einem „eingefrorenen“ System in den Augen des Nutzers.
Wie behandelt man einen Fehler mitten im Stream?#
Standard-HTTP hat keinen Mechanismus für „Fehler nach 200 OK“ im Body, da der Status bereits gesendet wurde. SSE-Muster: Sende ein spezielles Fehlerereignis (event: error\ndata: {...}\n\n) vor dem Schließen der Verbindung. Der Client lauscht auf das error-Ereignis separat vom message-Ereignis. Auf der UI-Seite zeige den empfangenen Ausschnitt mit einem Hinweis auf den Abbruch an und biete die Möglichkeit zur Wiederaufnahme. Verheimliche niemals, dass die Antwort unvollständig ist, da der Nutzer möglicherweise Entscheidungen auf Basis des abgebrochenen Textes trifft.
Kann man durch Next.js App Router ohne Pufferung streamen?#
Ja, aber es erfordert einen File Route Handler (app/api/.../route.ts), der new Response(upstream.body, {...}) mit den Headern Cache-Control: no-cache, no-transform und X-Accel-Buffering: no zurückgibt. Der standardmäßige rewrite in next.config.js leitet die Anfrage durch die Node.js-Schicht um, die die Antwort für Browser mit Accept-Encoding: gzip gzip-puffert. Der File Route Handler umgeht diese Schicht und leitet den Body des Upstreams direkt weiter. Der Artikel Prompt Caching für LLM beschreibt komplementäre Optimierungstechniken für die LLM-Pipeline auf Serverseite.
Wie misst man TTFT in der Produktion ohne Client-Zugriff?#
Miss auf Serverseite: Zeitstempel vor dem Senden des ersten data:-Ereignisses minus Zeitstempel der Anfrageannahme. Das ist ein Proxy für die reale TTFT (berücksichtigt nicht die Netzwerkzeit), aber ausreichend, um Trends zu verfolgen und Regressionen zu erkennen. Die reale End-to-End-TTFT misst man durch Client-Instrumentierung: performance.mark('request-sent') beim Senden, performance.mark('first-token') beim ersten SSE-Ereignis. Exportiere beide Werte in dasselbe Metriksystem und korreliere sie.
Welche Grenzen haben Guardrails im Stream?#
Ein Guardrail für den vollständigen Text hat alle Informationen. Ein Guardrail im Stream sieht nur den Präfix. Muster, die den Kontext des gesamten Satzes erfordern (doppelte Verneinung, getarnte Anfragen), sind schwer auf einem Fenster von wenigen Token zu erkennen. Ansatz bei Cashcrown: Vollständiger Eingangs-Guardrail eliminiert die meisten Risiken vor dem Start, der Ausgangs-Guardrail erkennt lexikalische Muster (verbotene Wörter, Teile von Kartennummern, PII). Für Inhalte mit hohem Risiko wird der gesamte Stream gepuffert und vor der Anzeige verifiziert, was den UX-Vorteil des Streamings zunichtemacht. Das ist ein bewusster Trade-off.
