Aller au contenu principal

Streaming SSE

Mis à jour le 29 juillet 2026

Réponses en temps réel avec Server-Sent Events

Par défaut, l’API Chat Completions attend que le modèle ait terminé sa réponse complète avant de vous la renvoyer. Sur une question courte, personne ne s’en aperçoit ; sur un rapport de mille mots, cela signifie plusieurs secondes d’écran vide, pendant lesquelles votre utilisateur se demande si l’application a planté. Le streaming résout ce problème en envoyant les tokens au fur et à mesure de leur génération, ce qui donne l’effet de frappe progressive familier des interfaces conversationnelles.

Le protocole utilisé est Server-Sent Events (SSE), un standard HTTP qui permet au serveur de pousser des données vers le client en continu sur une connexion persistante. Rien d’exotique : pas de WebSocket à installer, pas de dépendance supplémentaire.

Streaming ou non-streaming

En mode non-streaming, celui qui s’applique par défaut, le client envoie la requête et attend la réponse complète :

Client ──── requête ────► Serveur
         (attend 3-10s)
Client ◄── réponse complète ── Serveur

L’intérêt est la simplicité : la réponse est atomique, c’est un seul objet JSON, facile à traiter, à valider et à stocker. Son défaut est tout aussi net : la latence perçue est élevée, puisque l’utilisateur ne voit rien pendant toute la génération.

En mode streaming, le serveur envoie les tokens un par un, ou par petits groupes :

Client ──── requête ────► Serveur
Client ◄── chunk 1 ─────── Serveur  (après ~200ms)
Client ◄── chunk 2 ─────── Serveur  (après ~250ms)
Client ◄── chunk 3 ─────── Serveur  (après ~300ms)
...
Client ◄── [DONE] ──────── Serveur

La latence perçue s’effondre — le premier caractère apparaît en quelques centaines de millisecondes — et l’expérience utilisateur y gagne considérablement. En contrepartie, c’est à vous de reconstruire le message côté client et de gérer un état qui n’existe plus en un seul morceau.

Le protocole SSE en détail

Voici à quoi ressemble concrètement le flux HTTP que vous recevez :

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

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

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

data: [DONE]

Chaque ligne commence par data: suivi d’un objet JSON, et les lignes vides servent de séparateur entre les événements. Notez le détail qui piège tout le monde à la première implémentation : le fragment de texte se trouve dans le champ delta, et non dans message comme en mode classique. Le flux se termine par un événement data: [DONE], qui signale explicitement la fin — sans lui, vous ne sauriez pas distinguer une génération terminée d’une connexion interrompue.

Activer le streaming en Python

Avec le SDK Mistral, le streaming s’active en appelant chat.stream() au lieu de chat.complete() :

from mistralai 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": "Expliquez le fonctionnement d'un moteur électrique."}
    ]
)

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

print()  # Retour à la ligne final

Le flush=True mérite une explication, car son oubli est la cause classique d’un streaming qui « ne marche pas » : sans lui, Python garde les caractères en mémoire tampon et les affiche par blocs, ce qui annule visuellement tout le bénéfice du streaming alors que les chunks arrivent bien.

Structure d’un chunk

Un chunk de streaming ne se lit pas comme une réponse complète, et les chemins d’accès diffèrent :

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

# Le premier chunk contient souvent le rôle
# chunk.data.choices[0].delta.role == "assistant"

# Les chunks suivants contiennent le texte par fragments
# chunk.data.choices[0].delta.content == "Bon"

# Le dernier chunk contient finish_reason
# chunk.data.choices[0].finish_reason == "stop"

Le premier chunk porte généralement le rôle sans texte, les suivants transportent les fragments, et le dernier apporte le finish_reason. C’est pourquoi le test if content: du code précédent n’est pas une précaution superflue : concaténer un None ferait échouer la boucle dès le premier tour.

Reconstruire la réponse complète

Afficher les fragments ne suffit pas dès que vous devez conserver la réponse — pour l’ajouter à l’historique de conversation, la journaliser ou la stocker en base. Il faut donc accumuler en parallèle de l’affichage :

from mistralai 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": "Listez 5 langages de programmation."}]
)

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"Réponse complète ({len(full_response)} caractères)")

La variable finish_reason est capturée dans la même boucle, et pour la même raison qu’en mode classique : une réponse tronquée par max_tokens s’affiche à l’écran exactement comme une réponse complète, et seul ce champ vous le dira.

Quand utiliser le streaming

Le streaming n’est pas systématiquement meilleur. Il est indiqué dès qu’un humain regarde l’écran, superflu voire nuisible quand une machine traite la sortie.

Cas d’usageRecommandation
Chatbot interactifStreaming (meilleure UX)
Traitement batchNon-streaming (plus simple)
API backend pour frontendStreaming (SSE vers le navigateur)
Pipeline de donnéesNon-streaming (traitement atomique)
Extraction structuréeNon-streaming (parsing JSON plus fiable)

Le dernier cas est le plus instructif. Si vous attendez du JSON, streamer n’apporte rien : vous ne pouvez pas parser un objet incomplet, et vous devrez de toute façon attendre le dernier fragment. Autant garder l’appel atomique et le code simple.

Points clés à retenir

  • Le streaming réduit la latence perçue en envoyant les tokens au fil de la génération
  • Le protocole SSE est un standard HTTP simple et robuste
  • En streaming, utilisez delta.content au lieu de message.content
  • Accumulez les fragments côté client pour reconstruire la réponse complète
  • Privilégiez le streaming pour les interfaces utilisateur, le non-streaming pour le traitement automatisé