HomeRessources, Guides & Actualités – Actualités de l’intelligence artificielleDéfinitionStreaming LLM : Guide d’Architecture, Protocoles et d’Intégration du Serveur à l’Interface Utilisateur

Streaming LLM : Guide d’Architecture, Protocoles et d’Intégration du Serveur à l’Interface Utilisateur

Imaginez un utilisateur assis devant son écran. Une icône de chargement tourne sans fin. Les secondes défilent. Le doute s’installe. C’est l’expérience frustrante que génère une réponse non streamée. En revanche, l’apparition progressive du texte mime le fil d’une pensée humaine. L’attention reste captive. Le sentiment de vitesse augmente de 75 % sans changer la puissance du processeur.

Je constate ce phénomène chaque jour dans mes projets d’intelligence artificielle. Le streaming transforme la perception de la performance.

  • Réduction de la latence perçue : Le temps d’affichage du premier jeton (TTFT) chute sous la barre des 300 millisecondes. L’application gagne en réactivité aux yeux des utilisateurs.
  • Gestion des ressources serveurs : L’envoi progressif impose des connexions persistantes. Les architectures asynchrones deviennent obligatoires pour éviter la saturation de la mémoire.
  • Complexité du format JSON : Traiter des structures de données en cours de réception exige des outils d’analyse spécifiques capables de corriger la syntaxe à la volée.
  • Interruption précoce : Le client conserve le contrôle. Il peut stopper la génération dès que la réponse s’égare ou apporte la solution attendue.

Pourquoi le streaming transforme l’expérience utilisateur

  • Faible latence perçue : Les caractères s’affichent en quelques fractions de seconde. L’attente passive disparaît.
  • Engagement accru : L’effet de machine à écrire retient le regard et instaure une relation de confiance avec la machine.
  • Arrêt à la demande : L’utilisateur coupe la connexion si l’algorithme s’écarte du sujet ou s’il obtient sa réponse dès les premières lignes. Cela préserve vos ressources de calcul.

Le fonctionnement de la chaîne technique

Trois composants collaborent pour acheminer les données sans interruption.

  • Le fournisseur du modèle : Nous activons le paramètre stream: true dans la requête. Le moteur de calcul renvoie les fragments de texte au fur et à mesure de leur création.
  • Le serveur intermédiaire : Il capte le flux du fournisseur et le redirige vers l’application cliente à l’aide d’événements envoyés par le serveur (SSE).
  • L’interface utilisateur : Le navigateur décode le flux binaire en continu pour injecter le texte dans la page.

Implémentation d’un serveur asynchrone avec FastAPI

Ce code fait office de passerelle entre le fournisseur d’accès à l’intelligence artificielle et l’utilisateur final.

from fastapi import FastAPI
from fastapi.responses import StreamingResponse
from openai import OpenAI

app = FastAPI()
client = OpenAI(api_key="VOTRE_CLE_API")

def generate_chunks(prompt: str):
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": prompt}],
        stream=True
    )
    for chunk in response:
        content = chunk.choices[0].delta.content
        if content:
            yield f"data: {content}\n\n"

@app.get("/stream")
def stream_ai_response(prompt: str):
    return StreamingResponse(generate_chunks(prompt), media_type="text/event-stream")

Réception et affichage des données en JavaScript

Côté client, nous lisons le flux brut ligne par ligne pour mettre à jour l’affichage sans saccade.

async function fetchAIResponse(prompt) {
  const response = await fetch(`/stream?prompt=${encodeURIComponent(prompt)}`);
  const reader = response.body.getReader();
  const decoder = new TextDecoder();
  let outputContainer = document.getElementById("chat-box");

  while (true) {
    const { value, done } = await reader.read();
    if (done) break;

    const chunk = decoder.decode(value);
    const lines = chunk.split("\n");
    for (const line of lines) {
      if (line.startsWith("data: ")) {
        const text = line.replace("data: ", "");
        outputContainer.innerText += text;
      }
    }
  }
}

Comparatif des protocoles de communication

Le choix du canal de transmission détermine la stabilité de votre architecture.

CritèreServer-Sent Events (SSE)WebSockets
Sens de communicationUnidirectionnel (Serveur vers Client)Bidirectionnel (Double sens)
ProtocoleHTTP standardProtocole spécifique ws://
ComplexitéSimple (Reconnexion automatique intégrée)Élevée (Gestion manuelle des sessions)
Usage idéalGénération de texte LLMJeux en temps réel, outils collaboratifs

Anatomie d’un paquet de données brut

Le flux n’envoie pas de texte brut. Il transmet des objets structurés enveloppés dans un format événementiel.

data: {“id”:”chatcmpl-123″,”object”:”chat.completion.chunk”,”created”:1677652288,”model”:”gpt-4o”,”choices”:[{“index”:0,”delta”:{“content”:” apprentissage”},”finish_reason”:null}]}

Le marqueur data: annonce l’arrivée d’une information. Le dictionnaire delta contient la nouveauté textuelle. La clé finish_reason reste vide jusqu’au dernier envoi où elle prend la valeur “stop”.

Le cycle de traitement de l’interface graphique

Le navigateur applique une routine stricte à chaque octet reçu :

  1. Conversion du flux binaire en texte UTF-8 via l’utilitaire de décodage.
  2. Découpage de la chaîne de caractères à chaque retour à la ligne.
  3. Extraction de la valeur nichée dans le dictionnaire de données.
  4. Ajout des nouveaux symboles sur l’écran de l’internaute.

Une connexion instable peut lier des paquets entre eux ou couper une ligne en son milieu. Votre logique de réception doit stocker les morceaux incomplets jusqu’à la détection de la balise de fin de ligne.

Les défis majeurs de l’affichage progressif

Afficher du texte brut est simple. Formater du Markdown en temps réel s’avère bien plus délicat. Si vous passez un texte incomplet à un moteur de rendu classique, l’affichage va tressauter de façon désagréable.

Lorsqu’un modèle écrit une balise de mise en gras comme **Bonjour, l’absence des caractères de fermeture perturbe l’analyseur graphique. Le document change d’aspect à chaque seconde.

Pour éliminer ce défaut visuel, utilisez des analyseurs syntaxiques incrémentaux. Ces outils adaptent l’arbre du document sans reconstruire la page entière.

Optimisation des ressources du serveur

Le streaming maintient la liaison ouverte durant toute la phase de création de la réponse, parfois durant de longues dizaines de secondes. Un serveur classique attribue un fil d’exécution unique à chaque visiteur. Si cinquante personnes se connectent simultanément, la machine s’arrête.

La parade repose sur l’utilisation de serveurs asynchrones. Ces derniers gèrent des milliers de requêtes parallèles sur une infrastructure modeste.

De plus, l’usage de HTTP/2 devient obligatoire. Le protocole HTTP/1.1 bloque le navigateur au-delà de six liaisons simultanées vers un même domaine. HTTP/2 supprime cette barrière grâce au multiplexage.

Gestion fine du défilement de l’écran

La page doit descendre à mesure que le texte apparaît pour maintenir les derniers mots visibles. Cependant, si le lecteur remonte manuellement pour relire un passage supérieur, un défilement forcé détruira son confort de lecture.

Suivez les actions de l’utilisateur sur la page. S’il se trouve tout en bas de la fenêtre, accompagnez la descente du texte. S’il remonte de quelques pixels, désactivez le mouvement automatique. Vous pouvez afficher une alerte discrète pour lui proposer de rejoindre le bas de page d’un simple clic.

Modération et contrôle des flux

Les règles de sécurité imposent d’analyser les propos de l’intelligence artificielle avant de les présenter au public. Comment faire si le texte s’affiche instantanément ?

La solution réside dans l’usage d’un filtre à fenêtre glissante. Le serveur conserve un groupe de cinq à dix jetons en mémoire tampon. Il valide ce court segment avant de libérer le plus ancien élément vers l’écran de l’utilisateur. En cas de détection d’un contenu inapproprié, la liaison est coupée sur-le-champ.

Soutien des blocs de code interrompus

Un modèle qui s’arrête brusquement au milieu d’un bloc de code laisse des balises ouvertes. La mise en page globale de votre site peut alors s’effondrer. Votre interface doit analyser la structure du texte reçu et ajouter de force les balises manquantes pour préserver l’intégrité visuelle du site.

Traitement des appels de fonctions en flux continu

Le traitement des appels de fonctions en mode streaming présente une complexité supérieure. L’appareil n’envoie pas de texte lisible mais des fragments de paramètres au format JSON. Notre serveur doit assembler ces pièces, exécuter la fonction locale puis renvoyer le résultat final au modèle de langage.

import json
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
from openai import OpenAI

app = FastAPI()
client = OpenAI(api_key="VOTRE_CLE_API")

def get_weather(location: str):
    if "tunis" in location.lower():
        return {"temperature": "24C", "condition": "Ensoleillé"}
    return {"temperature": "15C", "condition": "Pluvieux"}

AVAILABLE_TOOLS = {
    "get_weather": get_weather
}

tools_schema = [{
    "type": "function",
    "function": {
        "name": "get_weather",
        "description": "Obtenir la météo pour un lieu",
        "parameters": {
            "type": "object",
            "properties": {"location": {"type": "string"}},
            "required": ["location"]
        }
    }
}]

async def process_and_stream(prompt: str):
    messages = [{"role": "user", "content": prompt}]

    response = client.chat.completions.create(
        model="gpt-4o",
        messages=messages,
        tools=tools_schema,
        stream=True
    )

    tool_calls_buffer = {}
    is_tool_call = False

    for chunk in response:
        delta = chunk.choices[0].delta

        if delta.content:
            yield f"data: {delta.content}\n\n"
            continue

        if delta.tool_calls:
            is_tool_call = True
            for tool_call in delta.tool_calls:
                index = tool_call.index

                if index not in tool_calls_buffer:
                    tool_calls_buffer[index] = {
                        "id": tool_call.id,
                        "name": tool_call.function.name,
                        "arguments": ""
                    }

                if tool_call.function.arguments:
                    tool_calls_buffer[index]["arguments"] += tool_call.function.arguments

    if not is_tool_call:
        return

    openai_tool_calls_format = []

    for index, call in tool_calls_buffer.items():
        parsed_args = json.loads(call["arguments"])
        tool_function = AVAILABLE_TOOLS[call["name"]]
        tool_result = tool_function(**parsed_args)

        openai_tool_calls_format.append({
            "id": call["id"],
            "type": "function",
            "function": {"name": call["name"], "arguments": call["arguments"]}
        })

        messages.append({"role": "assistant", "tool_calls": openai_tool_calls_format})
        messages.append({
            "role": "tool",
            "tool_call_id": call["id"],
            "name": call["name"],
            "content": json.dumps(tool_result)
        })

    final_response = client.chat.completions.create(
        model="gpt-4o",
        messages=messages,
        stream=True
    )

    for chunk in final_response:
        content = chunk.choices[0].delta.content
        if content:
            yield f"data: {content}\n\n"

@app.get("/chat")
def chat_endpoint(prompt: str):
    return StreamingResponse(process_and_stream(prompt), media_type="text/event-stream")

Les requêtes simultanées forcent le système à surveiller chaque appel à l’aide d’un index numérique unique pour éviter le mélange des arguments reçus.

Analyse des chaînes de caractères JSON tronquées sur le client

Pour afficher l’état d’avancement d’une action alors que la structure JSON est incomplète, la fonction de décodage classique de JavaScript s’avère insuffisante. Nous devons corriger la structure à la volée.

import { partialParse } from 'partial-json-parser'; 

async function streamStructuredJSON(prompt) {
  const response = await fetch(`/chat?prompt=${encodeURIComponent(prompt)}`);
  const reader = response.body.getReader();
  const decoder = new TextDecoder();

  let rawBuffer = "";
  let uiStatusBox = document.getElementById("status-indicator");

  try {
    while (true) {
      const { value, done } = await reader.read();
      if (done) break;

      const textChunk = decoder.decode(value);
      const lines = textChunk.split("\n");

      for (const line of lines) {
        if (line.startsWith("data: ")) {
          const content = line.replace("data: ", "").trim();
          rawBuffer += content;

          try {
            const completedObject = partialParse(rawBuffer);

            if (completedObject.status) {
              uiStatusBox.innerText = `Action: ${completedObject.status}`;
            }
            if (completedObject.query) {
              uiStatusBox.innerText += ` pour "${completedObject.query}"`;
            }
          } catch (parseError) {
            console.debug("Attente du prochain fragment...");
          }
        }
      }
    }

    const finalCleanObject = JSON.parse(rawBuffer);
    uiStatusBox.innerText = `Terminé: ${finalCleanObject.status}`;

  } catch (err) {
    console.error("Erreur de flux:", err);
  }
}

Si vous refusez l’usage d’une dépendance externe, un filtre par expression régulière extrait les valeurs des propriétés cibles sans se soucier des accolades manquantes.

function extractPartialStringValue(rawJsonString, key) {
  const regex = new RegExp(`"${key}"\\s*:\\s*"([^"]*)`);
  const match = rawJsonString.match(regex);
  return match ? match[1] : "";
}

Arbitrer entre Streaming et Non-Streaming

Chaque méthode de distribution répond à des impératifs bien distincts.

CaractéristiqueStreamingNon-Streaming
DistributionJeton par jeton (Progressif)Bloc complet d’un coup
Délai initial (TTFT)Tranquillisant (200ms – 500ms)Pesant (2s – 30s)
Surcharge serveurÉlevée (Maintien des connexions)Faible (Fermeture immédiate)
Traitement des donnéesComplexe en amont du renduAisé (Filtres, stockage en base)

Je préconise l’utilisation du streaming pour les espaces de discussion interactifs ou la rédaction de textes longs. Vos usagers apprécieront la réactivité immédiate.

Adoptez le traitement d’un seul bloc pour l’extraction de données structurées, la classification automatique ou les interventions programmées de courte durée.

Leave a Reply

Your email address will not be published. Required fields are marked *