Comment fonctionne un disjoncteur
Un disjoncteur est une petite règle qui cesse d’appeler un service qui échoue sans cesse. Tant qu’il est ouvert, la fonction utilise un repli au lieu d’attendre un appel qui échouera.
Le module sur le coût, le traçage et la fiabilité liste les replis. Cette leçon va plus loin : quand arrêter d’appeler, quand réessayer, et un interrupteur d’urgence qu’une personne peut presser.
À la fin de cette leçon
Remarque
Je peux garder une fonction utile pendant que son modèle échoue.
Mots clés
| Mot | Ce que ça veut dire | Exemple |
|---|---|---|
| Disjoncteur | une règle qui arrête les appels à un service en échec | |
| Fermé | l’état normal : les appels passent | |
| Ouvert | l’état d’arrêt : aucun appel, un repli répond | |
| Semi-ouvert | un appel d’essai pour voir si le service est revenu | |
| Repos | les pas à attendre avant un essai | 4 |
| Interrupteur d’urgence | une commande qui coupe une fonction aussitôt |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.
Trois états
Un disjoncteur est fermé quand tout va bien. Les appels passent. Il compte les échecs d’affilée. Quand le compte atteint le seuil, ici 3, il s’ouvre.
Ouvert, il ne fait aucun appel. Après le repos, ici 4 pas, il devient semi-ouvert et laisse passer un appel. Un succès le ferme. Un échec le rouvre.
Exemple détaillé : les pas 3 à 9
| Pas | L’appel | État trouvé | Servi | Pourquoi |
|---|---|---|---|---|
| 3 | Échouerait | Fermé | Échoué | Premier échec d’affilée, compte 1 |
| 4 | Échouerait | Fermé | Échoué | Compte 2 |
| 5 | Échouerait | Fermé | Échoué | Le compte atteint 3, donc il s’ouvre |
| 6 | Marcherait | Ouvert | Ignoré | Aucun appel. 1 pas depuis l’ouverture |
| 7 | Marcherait | Ouvert | Ignoré | 2 pas depuis l’ouverture |
| 8 | Marcherait | Ouvert | Ignoré | 3 pas depuis l’ouverture |
| 9 | Marcherait | Semi-ouvert | Modèle | 4 pas, l’essai marche, donc il se ferme |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.
Une erreur courante
Attention
Beaucoup pensent qu’un disjoncteur doit s’ouvrir au premier échec. Un seul échec est souvent un accident. Comptez les échecs d’affilée et laissez un succès remettre le compte à zéro, sinon un service sain est coupé par un seul mauvais appel.
Ce que dit le repli
Un repli doit être honnête et utile. Le meilleur est une réponse enregistrée à la même question. S’il n’y en a pas, dites simplement que l’assistant est occupé et quand réessayer.
N’affichez jamais une réponse inventée comme si le modèle l’avait donnée.
L’interrupteur d’urgence est une décision humaine
Un disjoncteur réagit aux échecs. Une personne presse l’interrupteur d’urgence quand le modèle marche mais ne doit pas servir. Il peut donner de mauvaises réponses, ou coûter trop cher.
Tant qu’il est actif, chaque appel est ignoré. Il ne touche pas au disjoncteur, qui garde son propre compte et son propre état.
Écrire l’assistant 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\";' '// Les jetons d’une réponse, ou null si elle n’en donne pas.' 'function compter(donnees, nom) {' ' return donnees.usage && typeof donnees.usage[nom] === \"number\" ? donnees.usage[nom] : null;' '}' > 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’assistant du modèle, partie 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' '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 || 100 }),' ' signal: AbortSignal.timeout(30000),' ' });' ' if (res.ok === false) throw new Error(\"statut \" + res.status);' ' const d = await res.json();' ' return { texte: d.choices[0].message.content, entree: compter(d, \"prompt_tokens\"), sortie: compter(d, \"completion_tokens\"), enregistre: false };' ' } catch (erreur) {' ' console.log(\"Échantillon enregistré : aucun modèle n’a répondu (\" + erreur.message + \").\");' ' return { texte: echantillon, entree: null, sortie: null, enregistre: true };' ' }' '}' '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 le script du disjoncteur, partie 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { demander } = require(\"./model\");' 'function pas(b, issue, i, seuil, repos, coupure) {' ' if (coupure) return [b.etat, \"ignore\"];' ' if (b.etat === \"ouvert\" && i - b.ouvertA >= repos) b.etat = \"semi_ouvert\";' ' const trouve = b.etat;' ' if (trouve === \"ouvert\") return [trouve, \"ignore\"];' ' if (issue === 1) { b.etat = \"ferme\"; b.echecs = 0; return [trouve, \"modele\"]; }' ' b.echecs = b.echecs + 1;' ' if (trouve === \"semi_ouvert\" || b.echecs >= seuil) { b.etat = \"ouvert\"; b.ouvertA = i; b.echecs = 0; }' ' return [trouve, \"echoue\"];' '}' > disjoncteur.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 le script du disjoncteur, partie 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const issues = [1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1];' 'function derouler(coupures) {' ' const b = { etat: \"ferme\", echecs: 0, ouvertA: 0 };' ' return issues.map(function (o, k) {' ' const r = pas(b, o, k + 1, 3, 4, coupures.includes(k + 1));' ' return r[0] + \":\" + r[1];' ' });' '}' >> disjoncteur.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 le script du disjoncteur, partie 3
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' ' const trace = derouler([]);' ' console.log(trace.slice(0, 10).join(\" \"));' ' console.log(trace.slice(10).join(\" \"));' ' const faits = trace.filter(function (t) { return t.endsWith(\"ignore\") === false; }).length;' ' console.log(\"appels faits \" + faits + \" sur \" + trace.length);' ' console.log(\"coupure 9 et 10 : \" + derouler([9, 10]).slice(8, 11).join(\" \"));' ' const q = \"En une phrase courte, que doit dire une fonction quand son modèle est en panne ?\";' ' const reponse = await demander([{ role: \"user\", content: q }], \"Dire simplement que l’assistant est occupé, et proposer une réponse enregistrée ou une autre voie.\", 60);' ' console.log(\"le modèle dit : \" + reponse.texte);' '}' 'main();' >> disjoncteur.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 le disjoncteur
docker exec -w /home/node/project agent node disjoncteur.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.
Ce que le Run affiche
| Ligne | Ce qu’elle montre |
|---|---|
| ferme:modele ferme:modele ferme:echoue ... | Les pas 1 à 10, l’état trouvé et comment l’appel a été servi |
| ouvert:ignore ouvert:ignore ouvert:ignore semi_ouvert:modele | Le disjoncteur s’ouvre, attend, et un essai marche |
| appels faits 14 sur 20 | Six appels ont été économisés |
| coupure 9 et 10 : ouvert:ignore ouvert:ignore semi_ouvert:modele | L’interrupteur a ignoré deux pas et le repos a continué |
| le modèle dit : ... | Une phrase du labo. Un échantillon enregistré si aucun modèle ne répond |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.