Montrer la réponse au fur et à mesure
Une page de chat en flux affiche la réponse d’un chatbot quelques mots à la fois. Le serveur envoie chaque mot au fur et à mesure. C’est utile, car une longue réponse peut prendre plusieurs secondes, et une page vide fait croire que le site est en panne.
Le serveur envoie de petits morceaux. La page ajoute chaque morceau à l’écran. Le terminal du labo n’a pas de navigateur : ici vous construisez la page et le flux sous forme de texte. Sur votre propre machine, vous l’ouvririez dans un navigateur.
À la fin de cette leçon
Remarque
Je peux afficher dans une page web une réponse envoyée en flux, au fur et à mesure.
Mots-clés
| Mot | Ce que ça veut dire |
|---|---|
| Flux | une réponse envoyée en petits morceaux |
| Événements serveur | un moyen pour un serveur de pousser des lignes vers une page |
| Fonction de traitement | la fonction qui répond à une requête |
| Échapper | changer des caractères pour qu’ils s’affichent comme du texte |
| Morceau | une petite partie d’une réponse en flux |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.
Un serveur peut envoyer une réponse en morceaux
Avec des événements serveur, le serveur garde la connexion ouverte et écrit des lignes. Chaque événement commence par data: , contient un morceau en JSON et se termine par une ligne vide. Le type de la réponse est text/event-stream.
Un dernier événement, nommé fin (il a une ligne event: fin et une ligne data: fin), dit à la page que la réponse est terminée.
Exemple travaillé : les lignes d’un flux
| Ligne envoyée | Ce que fait la page |
|---|---|
| data: {"morceau": "Bonjour"} et une ligne vide | Ajoute Bonjour à l’écran |
| data: {"morceau": " et bienvenue"} et une ligne vide | Ajoute les mots suivants après |
| event: fin, data: fin et une ligne vide | Ferme la connexion |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.
La page ajoute et peut s’arrêter
La page contient un petit script. Il ouvre le flux et, pour chaque événement, ajoute le morceau à un paragraphe. Un bouton d’arrêt ferme le flux : plus aucun morceau n’arrive.
Ajouter un morceau avec append et une chaîne ajoute du texte simple. Cela n’ajoute jamais de balisage.
Échappez le texte du modèle
Un modèle peut répéter ce qu’on lui a donné, y compris <script>. Si ce texte entre dans une page comme du HTML, le navigateur peut l’exécuter.
Échapper transforme < en < et les autres caractères spéciaux en codes sûrs. Échappez tout texte de modèle qui va dans du HTML. Faites & en premier, sinon vous échappez vos propres codes.
Une erreur fréquente
Attention
Beaucoup pensent que le texte du modèle est sûr parce qu’ils ont écrit le message. Ce n’est pas vrai. Le modèle peut répéter les mots d’un utilisateur ou d’une source. Traitez chaque réponse comme non fiable : ajoutez-la comme texte, ou échappez-la d’abord.
Écrire l’aide du modèle, partie 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const base = process.env.AI_URL || \"http://ai:4000\";' 'async function demander(messages, echantillon, max) {' ' try {' ' const res = await fetch(base + \"/v1/chat/completions\", {' ' method: \"POST\",' ' headers: { \"content-type\": \"application/json\", authorization: \"Bearer lab\" },' ' body: JSON.stringify({ model: \"lab\", messages: messages, max_tokens: max || 200 }),' ' signal: AbortSignal.timeout(30000),' ' });' ' if (res.ok === false) throw new Error(\"statut \" + res.status);' ' return (await res.json()).choices[0].message.content;' > model.js"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.
Écrire l’aide du modèle, partie 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' ' } catch (erreur) {' ' console.log(\"Échantillon enregistré : aucun modèle n’a répondu (\" + erreur.message + \").\");' ' const liste = [].concat(echantillon);' ' const tour = messages.filter(function (m) { return m.role === \"assistant\"; }).length;' ' return liste[Math.min(tour, liste.length - 1)];' ' }' '}' 'module.exports = { demander };' >> model.js"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.
Écrire la page et le flux, partie 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { demander } = require(\"./model\");' '' 'function citer(texte) { return JSON.stringify(texte); }' 'function echapperHtml(texte) {' ' return texte.replace(/&/g, \"&\").replace(/</g, \"<\").replace(/>/g, \">\")' ' .replace(/\"/g, \""\").replace(/\x27/g, \"'\");' '}' > interface4.js"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.
Écrire la page et le flux, partie 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function pageHtml() {' ' const script = [' ' \"const sortie = document.getElementById(\" + citer(\"reponse\") + \");\",' ' \"const flux = new EventSource(\" + citer(\"/flux\") + \");\",' ' \"flux.onmessage = function (e) { sortie.append(JSON.parse(e.data).morceau); };\",' ' \"flux.addEventListener(\" + citer(\"fin\") + \", function () { flux.close(); });\",' ' \"document.getElementById(\" + citer(\"arret\") + \").onclick = function () { flux.close(); };\",' ' ].join(\"\n\");' ' return \"<html><body><p id=reponse></p><button id=arret>Arrêter</button><script>\" + script + \"</script></body></html>\";' '}' >> interface4.js"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.
Écrire la page et le flux, partie 3
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function ligneSse(morceau) { return \"data: \" + JSON.stringify({ morceau: morceau }) + \"\n\n\"; }' 'function corpsFlux(morceaux, arretApres) {' ' let corps = \"\";' ' morceaux.slice(0, arretApres).forEach(function (morceau) { corps = corps + ligneSse(morceau); });' ' return arretApres >= morceaux.length ? corps + \"event: fin\ndata: fin\n\n\" : corps;' '}' >> interface4.js"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.
Écrire la page et le flux, partie 4
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function traiter(requete, morceaux) {' ' if (requete.url === \"/\") return { statut: 200, type: \"text/html\", corps: pageHtml() };' ' if (requete.url === \"/flux\") {' ' return { statut: 200, type: \"text/event-stream\", corps: corpsFlux(morceaux, requete.arretApres || 99) };' ' }' ' return { statut: 404, type: \"text/plain\", corps: \"Introuvable\" };' '}' >> interface4.js"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.
Écrire la page et le flux, partie 5
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function montrer(morceaux) {' ' const accueil = traiter({ url: \"/\" }, morceaux);' ' console.log(\"GET / ->\", accueil.statut, accueil.type, accueil.corps.length + \" caractères\");' ' const direct = traiter({ url: \"/flux\" }, morceaux);' ' console.log(\"GET /flux ->\", direct.statut, direct.type);' ' console.log(direct.corps.split(\"\n\n\").join(\" | \"));' ' const arrete = traiter({ url: \"/flux\", arretApres: 2 }, morceaux);' ' console.log(\"Arrêt après 2 morceaux :\", arrete.corps.split(\"\n\n\").length - 1, \"morceaux envoyés, pas de ligne fin\");' ' console.log(\"Échappé :\", echapperHtml(\"<b>salut</b> & merci\"));' '}' >> interface4.js"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.
Écrire la page et le flux, partie 6
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' ' // Échantillon enregistré, utilisé seulement si aucun modèle ne répond.' ' const echantillon = \"Bonjour et bienvenue à bord, où souhaitez-vous aller ?\";' ' const reponse = await demander([{ role: \"user\", content: \"Saluez un voyageur de bus en une phrase courte.\" }], echantillon, 40);' ' montrer(reponse.split(\" \").map(function (mot, rang) { return rang === 0 ? mot : \" \" + mot; }));' '}' 'main();' >> interface4.js"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.
Appeler la fonction de traitement
docker exec -w /home/node/project agent node interface4.jsAffiché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 sortie possible
| Ligne | Ce qu’elle peut montrer |
|---|---|
| GET / | 200 text/html et la taille de la page |
| GET /flux | 200 text/event-stream, puis les morceaux d’une réponse séparés par | |
| Arrêt | 2 morceaux envoyés, pas de ligne fin |
| Échappé | <b>salut</b> & merci, toujours pareil |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.