Plus facile à lire avec un fond crème, une teinte, plus de contraste ou un texte plus grand ? Choisissez vos couleurs et votre texte, une fois pour toutes les pages.

Leçon gratuite

Réponses en flux d’une API de modèle

Après cette leçon: Je peux lire une réponse qui arrive par morceaux et l’assembler en un seul texte.

Leçon de 60 min4 commandesLab agent de code IA : utiliser opencode pour expliquer et corriger du code, et vérifier son travail

Une réponse qui arrive mot après mot

Une réponse en flux est une réponse qu’un modèle de langage envoie par petits morceaux pendant qu’il écrit. Elle n’arrive pas d’un seul bloc à la fin. Les applications de discussion s’en servent pour que les mots apparaissent tout de suite.

C’est utile, car une longue réponse peut durer plusieurs secondes. Avec un flux, le lecteur voit le début presque aussitôt. Votre programme doit lire les morceaux et les assembler.

À la fin de cette leçon

Remarque

Je peux lire une réponse qui arrive par morceaux et l’assembler en un seul texte.

Mots clés

MotCe que ça veut direExemple
Fluxune réponse envoyée par morceaux pendant qu’elle s’écrit
Événements du serveurun moyen pour un serveur de pousser des lignes vers un client
Ligne de donnéesune ligne du flux qui commence par data: et contient du JSON
Deltale nouveau morceau de texte d’une ligne de données
Marqueur de finla dernière ligne de données, qui dit que le flux est finidata: [DONE]

Les cellules que les étudiants remplissent restent vides dans cet aperçu.

Un flux est une suite de lignes

Pour demander un flux, ajoutez "stream": true au corps de la requête. Le serveur répond alors par des événements envoyés par le serveur. C’est du texte simple, une ligne à la fois, avec une ligne vide entre les événements.

Chaque ligne qui commence par data: porte un morceau de JSON. Le texte est dans choices[0].delta.content. La dernière ligne est data: [DONE].

Les lignes d’un flux

La ligne commence parCe qu’elle contient
data: {"choices":[{"delta":{"role":"assistant"}}]}L’ouverture : un rôle, pas encore de texte
data: {"choices":[{"delta":{"content":"Un"}}]}Un morceau de texte à ajouter
data: {"choices":[{"delta":{},"finish_reason":"stop"}]}La raison de la fin de la réponse
data: [DONE]Le marqueur de fin, qui n’est pas du JSON
: keep-aliveUne ligne de commentaire à ignorer

Les cellules que les étudiants remplissent restent vides dans cet aperçu.

Voir les paquets d’octets qui arrivent

Commande
docker exec -w /home/node/project agent node -e 'const labo = require("./labo.js"); const evenement = (texte) => "data: " + JSON.stringify({ choices: [{ delta: { content: texte } }] }) + "\n\n"; const tout = evenement("Un") + evenement(", deux") + evenement(", trois.") + "data: [DONE]\n\n"; (async () => { let paquets = []; let etiquette = "Flux"; try { const reponse = await labo.envoyer({ stream: true, max_tokens: 30, messages: [{ role: "user", content: "Comptez de un à cinq." }] }); if (!reponse.ok) throw new Error("HTTP " + reponse.status); const decodeur = new TextDecoder(); for await (const octets of reponse.body) paquets.push(decodeur.decode(octets, { stream: true })); } catch (erreur) { etiquette = "Exemple enregistré"; paquets = tout.match(/[^]{1,45}/g); } console.log(etiquette + " : premiers paquets d’octets"); paquets.slice(0, 4).forEach((un, i) => console.log((i + 1) + " " + JSON.stringify(un))); console.log("... " + paquets.length + " paquets en tout."); })();'

Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.

Un paquet du réseau n’est pas une ligne

Les octets arrivent à votre programme par paquets, et un paquet peut s’arrêter au milieu d’une ligne. Gardez donc un tampon. Ajoutez-y chaque paquet, découpez chaque ligne complète et conservez le reste pour le paquet suivant.

Seule une ligne complète peut être lue sans risque comme du JSON. Ajoutez ensuite chaque morceau de texte à la réponse.

Écrire flux.js, un lecteur de lignes entières

Commande
docker exec -w /home/node/project agent node -e 'require("fs").writeFileSync("flux.js", ["// flux.js : lit une réponse en flux sous forme de lignes complètes, quelle que soit la coupe des paquets.", "async function* lignesDe(reponse) {", "  let retenu = \"\";", "  const decodeur = new TextDecoder();", "  for await (const octets of reponse.body) {", "    retenu += decodeur.decode(octets, { stream: true });", "    const parties = retenu.split(\"\\n\");", "    retenu = parties.pop();", "    yield* parties;", "  }", "  if (retenu !== \"\") yield retenu;", "}", "", "module.exports = { lignesDe };"].join("\n") + "\n")'

Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.

Ajouter morceauxDe, qui donne le texte

Commande
docker exec -w /home/node/project agent node -e 'require("fs").appendFileSync("flux.js", ["", "// Partie 2 : envoie un appel en flux et rend les morceaux de texte, un par un.", "async function* morceauxDe(corps) {", "  const reponse = await require(\"./labo.js\").envoyer({ ...corps, stream: true });", "  if (!reponse.ok) throw new Error(\"HTTP \" + reponse.status);", "  for await (const ligne of lignesDe(reponse)) {", "    if (!ligne.startsWith(\"data: \") || ligne === \"data: [DONE]\") continue;", "    const morceau = JSON.parse(ligne.slice(6)).choices?.[0]?.delta?.content;", "    if (morceau) yield morceau;", "  }", "}", "", "module.exports = { lignesDe, morceauxDe };"].join("\n") + "\n")'

Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.

Une erreur fréquente

Attention

Beaucoup pensent qu’un paquet égale un événement. Ce n’est pas vrai. Un paquet peut contenir trois événements ou la moitié d’un. Votre programme est correct quand il donne le même texte, quelle que soit la coupe des octets. Mettez d’abord en tampon, puis lisez les lignes.

Afficher et assembler les morceaux

Commande
docker exec -w /home/node/project agent node -e 'const { morceauxDe } = require("./flux.js"); const exemple = ["Un", ", deux", ", trois", ", quatre", ", cinq."]; (async () => { const debut = Date.now(); let premier = null, nombre = 0, texte = "", etiquette = "Réponse du modèle"; try { const demande = { max_tokens: 40, messages: [{ role: "user", content: "Comptez de un à cinq." }] }; for await (const morceau of morceauxDe(demande)) { if (premier === null) premier = Date.now() - debut; texte += morceau; nombre++; process.stdout.write(morceau); } } catch (erreur) { etiquette = "Exemple enregistré"; texte = exemple.join(""); nombre = exemple.length; premier = null; process.stdout.write(texte); } console.log("\n" + etiquette + " : " + nombre + " morceaux, " + texte.length + " caractères."); if (premier !== null) console.log("Le premier morceau est arrivé après " + premier + " ms et la réponse entière après " + (Date.now() - debut) + " ms."); })();'

Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.

Exemple travaillé : des lignes au texte

Lignes de données, dans l’ordreMorceau ajoutéTexte jusqu’ici
Ouverture, rôle seulAucunVide
content "Un"UnUn
content ", deux", deuxUn, deux
finish_reason "stop"AucunUn, deux
data: [DONE]AucunUn, deux

Les cellules que les étudiants remplissent restent vides dans cet aperçu.

Pour aller plus loin : chronométrer le premier morceau

Lancez trois fois le script d’assemblage et notez le délai du premier morceau et celui de la réponse entière.

Dites ensuite lequel des deux un lecteur remarque le plus, et pourquoi un flux aide surtout quand la réponse est longue.

À propos de ce labo et de sa source: Lab agent de code IA : utiliser opencode pour expliquer et corriger du code, et vérifier son travail

Vous enseignez à une classe ? Proposez ce module à vos étudiants avec un guide de l'enseignant, des devoirs corrigés et un laboratoire pour chaque étudiant.