Du modèle à l’écran.
Dans les leçons 14 et 15, la requête a quitté le champ de saisie, traversé le serveur de l’application et le chat template, puis elle est entrée dans le modèle sous forme de tokens. Le modèle a fait son travail : prefill, puis decode token après token, avec son cache KV (leçon 15). Il reste le chemin du retour. Comment ces identifiants redeviennent-ils du texte ? Et pourquoi la réponse s’affiche-t-elle mot après mot, au lieu d’arriver d’un bloc ? Cette dernière leçon répond à ces questions, puis rassemble tout le parcours en une seule vue.
Des identifiants au texte : la détokenisation
Le modèle ne produit que des identifiants. Pour les afficher, on fait le chemin inverse de la leçon 02 : chaque identifiant est retrouvé dans le vocabulaire, et les morceaux de texte sont mis bout à bout. C’est la détokenisation (decoding, ou detokenization). Comme l’espace est collée au mot qui suit (leçon 13), recoller les tokens suffit à retrouver les espaces.
Une subtilité guette les tokenizers qui travaillent sur des octets, comme celui de GPT-2. Un « é » s’écrit avec 2 octets en UTF-8, un emoji avec 4. Il arrive qu’un caractère soit coupé entre deux tokens. Pendant l’affichage au fil de l’eau, il faut alors attendre l’octet suivant avant de montrer le caractère, sinon l’utilisateur verrait un symbole cassé. Notre tokenizer par caractère n’a pas ce problème.
Attendre la fin, ou envoyer au fil de l’eau
Première option : le serveur attend que le modèle ait fini, puis renvoie toute la réponse d’un coup. Simple, mais l’utilisateur fixe un écran vide pendant toute la génération. Dans le laboratoire, avec le prompt « Phileas Fogg » et 60 tokens générés, il faut attendre 3,87 s.
Seconde option : envoyer chaque token dès qu’il est généré. C’est le streaming. La génération ne va pas plus vite, mais le premier mot apparaît après 0,33 s seulement. La réponse semble instantanée, et l’utilisateur commence à lire pendant que le modèle écrit encore. C’est pourquoi tous les assistants conversationnels utilisent le streaming, avec le paramètre stream: true de la leçon 14.
Les Server-Sent Events
Pour streamer, le serveur garde la réponse HTTP ouverte et y écrit au fur et à mesure. Le format le plus utilisé s’appelle les Server-Sent Events (SSE) : la réponse a le type text/event-stream, et chaque événement est une ligne data:, suivie d’une ligne vide.
data: {"type": "start", "prompt_tokens": 13}
data: {"type": "delta", "index": 0, "id": 97, "text": "é"}
data: {"type": "delta", "index": 1, "id": 68, "text": "t"}
…
data: {"type": "done", "finish_reason": "length", "usage": {…}}
data: [DONE]L’API Chat Completions d’OpenAI fonctionne exactement ainsi : chaque morceau (chunk) contient un petit bout de texte, le delta ; le dernier indique le finish_reason de la leçon 09 ; et le flux se termine par data: [DONE]. L’API Responses envoie des événements typés, comme response.output_text.delta. Les SSE ne vont que dans un sens, du serveur vers le navigateur. Pour un échange dans les deux sens en temps réel, comme une conversation vocale, on utilise plutôt les WebSockets.
Côté navigateur : lire le flux
Dans la page, le code lit la réponse morceau par morceau, au lieu d’attendre qu’elle soit complète. À chaque événement reçu, il ajoute le nouveau texte à l’écran.
const response = await fetch(url, { method: "POST", body, signal })
const reader = response.body.getReader()
const decoder = new TextDecoder()
while (true) {
const { value, done } = await reader.read()
if (done) break
// découper sur les lignes vides, lire chaque « data: »,
// puis ajouter le texte du delta à l’écran
}Deux détails font la différence pour l’utilisateur. D’abord, le texte arrive souvent en Markdown (titres, listes, code), qu’il faut afficher proprement alors même qu’il est incomplet. C’est délicat : au milieu du flux, les balises comme ** pour le gras ou ``` pour un bloc de code sont ouvertes, mais pas encore refermées. L’interface doit donc tolérer, à chaque token, un Markdown provisoirement incomplet, sans faire clignoter la mise en page.
Ensuite, un bouton « Arrêter » annule la requête avec un AbortController, qui coupe la connexion HTTP côté navigateur. Le serveur de l’application doit alors détecter cette déconnexion pour interrompre à son tour la boucle de génération, ou fermer sa propre connexion vers l’API du modèle. Arrêter tôt économise du calcul et des tokens, mais pas tous : les tokens du prompt, déjà traités pendant le prefill, et les tokens émis avant l’arrêt restent comptabilisés.
La vitesse vue par l’utilisateur
On retrouve les deux mesures de la leçon 15. Le délai du premier token (TTFT) additionne le trajet réseau, l’attente dans la file du serveur et le prefill. Le débit, en tokens par seconde, dépend du decode. Notre laboratoire imite un vrai modèle avec des délais artificiels : 200 ms plus 10 ms par token de prompt pour le prefill, soit 330 ms pour « Phileas Fogg », puis 60 ms par token généré, soit environ 16,7 tokens par seconde.
À la fin du flux, l’API envoie la raison de l’arrêt et, si on le demande, l’usage : chez OpenAI, l’option stream_options avec include_usage ajoute un dernier morceau avec les tokens consommés, ceux que vous paierez.
Le grand récapitulatif
Vous avez maintenant suivi tout le trajet d’un prompt. Le voici d’un seul coup d’œil, avec la leçon qui explique chaque étape :
1. le navigateur envoie le message au serveur leçon 14
2. messages, rôles, chat template, budget de tokens leçon 14
3. tokenisation en sous-mots, puis identifiants leçons 13, 01, 02
4. embeddings et vecteurs de position leçons 03, 05
5. attention et blocs Transformer, couche après couche leçons 06, 07
6. LayerNorm finale, logits, probabilités leçon 08
7. choix du token : température, top-k, top-p leçon 09
8. boucle autorégressive, prefill, decode, cache KV leçons 04, 09, 15
9. arrêt : token de fin, séquence d’arrêt ou max_tokens leçon 09
10. détokenisation et streaming jusqu’à l’écran leçon 16
des poids appris par pré-entraînement, SFT et RLHF leçons 10, 11, 12Le laboratoire de cette leçon refait ce trajet en direct pour votre propre texte, avec un chiffre clé calculé à chaque étape.
Observez par vous-même
Dans le laboratoire, tapez Phileas Fogg (avec l’espace final) et cliquez sur « Envoyer ». Regardez le texte apparaître token après token, le flux SSE brut défiler et les compteurs de vitesse se remplir. Désactivez ensuite le streaming et renvoyez la même requête : l’écran reste vide pendant près de 4 secondes, puis toute la réponse arrive d’un bloc, dans un seul objet JSON que la carte 04 résume.
Pendant un envoi en streaming, essayez le bouton « Arrêter » : le flux s’interrompt aussitôt. Enfin, descendez jusqu’à la carte du trajet complet, et cliquez sur une étape pour revoir sa leçon.
Et maintenant ?
Notre laboratoire ne fait tourner qu’un modèle par comptage, avec des délais imités ; les vrais modèles affichent des débits bien différents selon leur taille et le matériel. Mais le format du flux, la détokenisation et la mesure de la vitesse sont exactement ceux d’une vraie application.
Vous connaissez désormais tout le trajet, du clic à l’écran, et les notions qu’un professionnel manipule chaque jour. La théorie est terminée. La prochaine partie du parcours vous fait passer à la pratique : préparer votre environnement, puis construire, étape par étape, votre propre projet MiniLab.
La détokenisation retrouve le texte de chaque identifiant et les met bout à bout ; avec un BPE au niveau des octets, il faut attendre les octets manquants d’un caractère avant de l’afficher. Le streaming envoie chaque token dès sa génération : la durée totale ne change pas, mais le délai du premier token (TTFT = réseau + file d’attente + prefill) devient court.
Format SSE : réponse HTTP text/event-stream, une ligne data: … par événement suivie d’une ligne vide ; chez OpenAI, des chunks avec un delta, un finish_reason final, l’usage en option (include_usage) et data: [DONE]. Côté navigateur : lecture du flux avec un reader, affichage progressif du Markdown, annulation avec AbortController.
Streaming API responses · OpenAI ↗
Server-sent events · MDN ↗