Dire au modèle ce qui n’allait pas
Une boucle de réparation redemande une réponse après une mauvaise, en renvoyant les fautes pour que le modèle les corrige. C’est utile, car un deuxième essai qui connaît le problème réussit souvent là où un essai à l’aveugle échouerait.
Un outil de formulaire reçoit une réponse avec un mot que le schéma n’autorise pas. La rejeter fait perdre le travail. Redemander, avec la faute dans le message, la sauve le plus souvent.
À la fin de cette leçon
Remarque
Je peux relancer une mauvaise réponse avec l’erreur comme retour, quelques fois et pas plus.
Mots-clés
| Mot | Ce que ça veut dire |
|---|---|
| Boucle de réparation | redemander après une mauvaise réponse, avec un retour |
| Retour | les fautes, renvoyées dans un nouveau message |
| Limite d’essais | le nombre maximal de tentatives en plus |
| Repli | ce que l’on fait quand la limite est atteinte |
| Liste de messages | la conversation envoyée à chaque appel |
| Abandonner | arrêter et utiliser le repli |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.
Renvoyer les fautes
Pour réparer une réponse, redemandez avec toute la conversation jusque-là. Ajoutez la mauvaise réponse comme message de l’assistant. Ajoutez ensuite un message de l’utilisateur qui liste les fautes et répète la règle : répondre uniquement par le JSON.
La liste de messages grandit de deux à chaque fois. Le modèle voit ce qu’il a dit et ce qui n’allait pas.
Exemple travaillé : trois essais
| Essai | Réponse | Faute relevée | Envoyé ensuite |
|---|---|---|---|
| 1 | Probablement oui, le blé en contient. | Pas un objet JSON | La faute, et la règle de répondre uniquement par le JSON |
| 2 | {"reponse": "peut-être"} | reponse doit valoir oui ou non | La faute, et la règle encore |
| 3 | {"reponse": "oui"} | Aucune | Rien : on l’accepte |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.
Une limite et un repli
Chaque nouvelle tentative coûte du temps et du crédit. Fixez donc une limite d’essais : le premier essai et deux de plus au plus. Quand la limite est atteinte, abandonnez et employez le repli.
Un repli est une valeur sûre comme « inconnu », ou une erreur qu’une personne verra. Notez que cela s’est produit, pour voir à quelle fréquence.
É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 boucle de réparation, partie 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { demander } = require(\"./model\");' '' 'const permis = [\"oui\", \"non\"];' 'const MAX_ESSAIS = 3; // le premier essai et au plus deux de plus' '' 'function extraireJson(texte) {' ' const debut = texte.indexOf(\"{\");' ' const fin = texte.lastIndexOf(\"}\");' ' if (debut === -1 || fin < debut) return null;' ' try { return JSON.parse(texte.slice(debut, fin + 1)); } catch (erreur) { return null; }' '}' > reparation3.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 boucle de réparation, partie 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function problemesDe(texte) {' ' const reponse = extraireJson(texte);' ' if (reponse === null) return [\"la réponse n’est pas un objet JSON\"];' ' if (Object.prototype.hasOwnProperty.call(reponse, \"reponse\") === false) return [\"la clé reponse manque\"];' ' if (permis.includes(reponse.reponse) === false) return [\"reponse doit valoir oui ou non\"];' ' return [];' '}' >> reparation3.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 boucle de réparation, partie 3
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function boucleReparation(question, echantillons) {' ' const messages = [{ role: \"user\", content: question }];' ' for (let essai = 1; essai <= MAX_ESSAIS; essai = essai + 1) {' ' const reponse = await demander(messages, echantillons, 60);' ' const trouves = problemesDe(reponse);' ' console.log(\"Essai \" + essai + \" : \" + (trouves.length === 0 ? \"ok\" : trouves.join(\" ; \")));' ' if (trouves.length === 0) return { ok: true, reponse: extraireJson(reponse), essais: essai };' ' messages.push({ role: \"assistant\", content: reponse });' ' messages.push({ role: \"user\", content: \"Corrigez ces problèmes et répondez uniquement par le JSON : \" + trouves.join(\" ; \") });' ' }' ' return { ok: false, reponse: null, essais: MAX_ESSAIS };' '}' >> reparation3.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 boucle de réparation, partie 4
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' ' const question = \"La baguette de tradition contient-elle du gluten ? Réponds par un objet JSON avec la clé reponse, qui vaut oui ou non. Réponds uniquement par le JSON.\";' ' // Échantillons enregistrés, utilisés seulement si aucun modèle ne répond : deux mauvaises réponses, puis une bonne.' ' const echantillons = [\"Probablement oui, le blé en contient.\", JSON.stringify({ reponse: \"peut-être\" }), JSON.stringify({ reponse: \"oui\" })];' ' const resultat = await boucleReparation(question, echantillons);' ' const repli = { reponse: \"inconnu\" };' ' console.log(\"Essais utilisés :\", resultat.essais);' ' console.log(\"Résultat :\", JSON.stringify(resultat.ok ? { reponse: resultat.reponse.reponse } : repli));' '}' 'main();' >> reparation3.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.
Lancer la boucle
docker exec -w /home/node/project agent node reparation3.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 |
|---|---|
| Essai 1 | ok, si le modèle a répondu du JSON propre du premier coup |
| Essai 2 et Essai 3 | Seulement si l’essai 1 a échoué : la faute, ou ok |
| Essais utilisés | Un nombre de 1 à 3 |
| Résultat | La réponse, ou le repli {"reponse":"inconnu"} |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.
Une erreur fréquente
Attention
Beaucoup pensent que réessayer jusqu’à ce que ça marche est sans risque. Si le modèle ne trouve jamais la bonne forme, une boucle sans limite ne finit jamais, et chaque essai coûte du temps et du crédit. Fixez toujours une limite et choisissez d’abord le repli.