Zum Hauptinhalt springen
Lektion 5 von 16 · Chat Completions: Beherrschung der API

Streaming über Server-Sent Events

Lektion 5 von 16 Aktualisiert am 2. November 2026 Entwickler
Auf dieser Seite
  1. Echtzeit-Antworten mit Server-Sent Events
  2. Streaming im Vergleich zum Nicht-Streaming
  3. Das SSE-Protokoll im Detail
  4. Streaming in Python aktivieren
  5. Struktur eines Chunk
  6. Rekonstruktion der vollständigen Antwort
  7. Wann Streaming verwendet werden sollte
  8. Kernaussagen

Echtzeit-Antworten mit Server-Sent Events

Standardmäßig wartet die Chat-Completions-API, bis das Modell die gesamte Antwort generiert hat, bevor sie an Sie gesendet wird. Bei einer kurzen Frage fällt diese Verzögerung niemandem auf. Bei einem Bericht von tausend Wörtern bedeutet dies jedoch mehrere Sekunden lang einen leeren Bildschirm, während Ihre Nutzerin oder Ihr Nutzer sich fragt, ob die Anwendung abgestürzt ist. Streaming löst dieses Problem, indem es Token schickt, sobald sie generiert werden, und erzeugt so den vertrauten Effekt einer Schreibmaschine, wie er in konversationellen Schnittstellen üblich ist.

Das verwendete Protokoll ist Server-Sent Events (SSE), ein HTTP-Standard, der es dem Server ermöglicht, Daten kontinuierlich über eine persistente Verbindung an den Client zu senden. Hier gibt es nichts Exotisches: Es werden keine WebSockets installiert und es sind keine zusätzlichen Abhängigkeiten erforderlich.

Streaming im Vergleich zum Nicht-Streaming

Im Nicht-Streaming-Modus, was der Standard ist, sendet der Client die Anfrage und wartet auf die vollständige Antwort:

Client ──── request ────► Server
         (wait 3-10s)
Client ◄── complete response ── Server

Der Vorteil liegt in der Einfachheit: Die Antwort ist atomar, ein einzelnes JSON-Objekt, das einfach zu verarbeiten, zu validieren und zu speichern ist. Der Nachteil ist ebenso klar: Die wahrgenommene Latenz ist hoch, da der Nutzer während des gesamten Generierungsprozesses nichts sieht.

Im Streaming-Modus sendet der Server Token einzeln oder in kleinen Gruppen:

Client ──── request ────► Server
Client ◄── chunk 1 ─────── Server  (nach ~200ms)
Client ◄── chunk 2 ─────── Server  (nach ~250ms)
Client ◄── chunk 3 ─────── Server  (nach ~300ms)
...
Client ◄── [DONE] ──────── Server

Die wahrgenommene Latenz sinkt drastisch – das erste Zeichen erscheint innerhalb weniger hundert Millisekunden – was die Benutzererfahrung erheblich verbessert. Im Gegenzug müssen Sie die Nachricht auf Client-Seite rekonstruieren und Zustände verwalten, die nicht mehr als einzelner Block ankommen.

Das SSE-Protokoll im Detail

Hier ist, wie der empfangene HTTP-Stream tatsächlich aussieht:

data: {"id":"cmpl-abc","choices":[{"delta":{"content":"Hello"}}],"usage":null}

data: {"id":"cmpl-abc","choices":[{"delta":{"content":" "}}],"usage":null}

data: {"id":"cmpl-abc","choices":[{"delta":{"content":"!"}}],"usage":null}

data: [DONE]

Jede Zeile beginnt mit data: , gefolgt von einem JSON-Objekt, und leere Zeilen dienen als Trennzeichen zwischen den Ereignissen. Beachten Sie die Detail, das alle bei der ersten Implementierung in die Irre führt: Der Textfragment befindet sich im Feld delta, nicht im Feld message wie im klassischen Modus. Der Stream endet mit einem data: [DONE]-Ereignis, das das Ende explizit signalisiert. Ohne dieses Signal könnten Sie eine abgeschlossene Generierung nicht von einer unterbrochenen Verbindung unterscheiden.

Streaming in Python aktivieren

Mit dem Mistral SDK aktivieren Sie Streaming, indem Sie chat.stream() anstelle von chat.complete() aufrufen:

from mistralai.client import Mistral
import os

client = Mistral(api_key=os.getenv("MISTRAL_API_KEY"))

stream = client.chat.stream(
    model="mistral-large-latest",
    messages=[
        {"role": "user", "content": "Explain how an electric motor works."}
    ]
)

for chunk in stream:
    content = chunk.data.choices[0].delta.content
    if content:
        print(content, end="", flush=True)

print()  # Final newline

Das Argument flush=True verdient eine Erklärung, da das Vergessen desselben die klassische Ursache für Streaming ist, das „nicht funktioniert”. Ohne dieses Argument hält Python die Zeichen im Puffer und zeigt sie in Blöcken an. Dies hebt visuell alle Vorteile des Streaming auf, obwohl die Chunk korrekt ankommen.

Struktur eines Chunk

Ein Streaming-Chunk liest sich nicht wie eine vollständige Antwort, und die Zugriffspfade unterscheiden sich:

# Non-streaming: response.choices[0].message.content
# Streaming:     chunk.data.choices[0].delta.content

# Das erste Chunk enthält oft die Rolle
# chunk.data.choices[0].delta.role == "assistant"

# Nachfolgende Chunks enthalten Textfragmente
# chunk.data.choices[0].delta.content == "Hello"

# Das letzte Chunk enthält finish_reason
# chunk.data.choices[0].finish_reason == "stop"

Das erste Chunk trägt in der Regel die Rolle ohne Text, nachfolgende Chunks tragen die Fragmente, und das letzte liefert das finish_reason. Deshalb ist die Prüfung if content: im vorherigen Code keine unnötige Vorsichtsmaßnahme: Das Anhängen eines None-Werts würde dazu führen, dass der Schleife bei der ersten Iteration fehlschlägt.

Rekonstruktion der vollständigen Antwort

Das Anzeigen von Fragmenten reicht nicht aus, wenn Sie die Antwort behalten müssen – beispielsweise, um sie in die Konversationshistorie aufzunehmen, zu protokollieren oder in einer Datenbank zu speichern. Sie müssen den Text daher parallel zum Anzeigen akkumulieren:

from mistralai.client import Mistral
import os

client = Mistral(api_key=os.getenv("MISTRAL_API_KEY"))

stream = client.chat.stream(
    model="mistral-large-latest",
    messages=[{"role": "user", "content": "List 5 programming languages."}]
)

full_response = ""
finish_reason = None

for chunk in stream:
    delta = chunk.data.choices[0].delta
    if delta.content:
        full_response += delta.content
        print(delta.content, end="", flush=True)

    if chunk.data.choices[0].finish_reason:
        finish_reason = chunk.data.choices[0].finish_reason

print(f"\n\nFinish reason: {finish_reason}")
print(f"Complete response ({len(full_response)} characters)")

Die Variable finish_reason wird im selben Schleifenlauf erfasst, aus demselben Grund wie im klassischen Modus: Eine durch max_tokens abgeschnittene Antwort erscheint auf dem Bildschirm exakt wie eine vollständige Antwort, und nur dieses Feld wird den Unterschied anzeigen.

Wann Streaming verwendet werden sollte

Streaming ist nicht immer besser. Es wird angezeigt, wenn ein Mensch den Bildschirm beobachtet, ist aber bei einer Maschine, die die Ausgabe verarbeitet, überflüssig oder sogar nachteilig.

Use caseRecommendation
Interactive chatbotStreaming (better UX)
Batch processingNon-streaming (simpler)
Backend API for frontendStreaming (SSE to browser)
Data pipelineNon-streaming (atomic processing)
Structured extractionNon-streaming (more reliable JSON parsing)

Der letzte Fall ist der lehrreichste. Wenn Sie auf JSON warten, fügt Streaming nichts hinzu: Sie können ein unvollständiges Objekt nicht parsen, und Sie müssen ohnehin auf das letzte Fragment warten. Sie können die Anfrage einfach atomar belassen und den Code einfach halten.

Kernaussagen

  • Streaming reduziert die wahrgenommene Latenz, indem es Token schickt, sobald sie generiert werden
  • Das SSE-Protokoll ist ein einfacher und robuster HTTP-Standard
  • Im Streaming-Modus verwenden Sie delta.content statt message.content
  • Akkumulieren Sie Fragmente auf Client-Seite, um die vollständige Antwort zu rekonstruieren
  • Bevorzugen Sie Streaming für Benutzerschnittstellen und Nicht-Streaming für automatisierte Verarbeitung