Comment reprendre sans provoquer de tempête
Une limite de débit est une règle : un service n’accepte qu’un certain nombre d’appels en peu de temps. Une reprise est un nouvel essai après le refus d’un appel. Une reprise sans précaution peut aggraver une surcharge.
Le module sur le coût, le traçage et la fiabilité reprend une fois, puis se replie. Cette leçon va plus loin. Elle ajoute des attentes qui grandissent et une gigue qui disperse les appelants. Elle limite les essais et rend une reprise sûre à répéter.
À la fin de cette leçon
Remarque
Je peux reprendre un appel sans provoquer de tempête.
Mots clés
| Mot | Ce que ça veut dire | Exemple |
|---|---|---|
| Limite de débit | le maximum d’appels qu’un service accepte dans une fenêtre | |
| Attente croissante | une attente qui grandit après chaque essai raté | 400, 800, 1600 |
| Gigue | une petite attente différente pour chaque appelant | |
| Tempête | de nombreux appelants qui reprennent au même instant | |
| Idempotent | sans risque à répéter : le faire deux fois vaut une fois | |
| Horloge simulée | un nombre qu’on avance à la main au lieu d’attendre |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.
Comment compte une limite de débit
Le service découpe le temps en fenêtres de 1000 millisecondes. Dans chaque fenêtre, il accepte un nombre fixe d’appels, ici 2. Il refuse les autres jusqu’au début de la fenêtre suivante.
Le temps est ici un nombre qu’on avance à la main. Rien n’attend : chaque exécution donne la même réponse.
Exemple détaillé : 2 appels par fenêtre
| Instant (ms) | Fenêtre | Appels déjà acceptés | Résultat |
|---|---|---|---|
| 0 | 0 | 0 | Accepté |
| 0 | 0 | 1 | Accepté |
| 0 | 0 | 2 | Refusé, la fenêtre est pleine |
| 400 | 0 | 2 | Refusé de nouveau |
| 1200 | 1 | 0 | Accepté, nouvelle fenêtre |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.
Une erreur courante
Attention
Beaucoup pensent que reprendre aussitôt est le plus rapide pour obtenir une réponse. Si le service est surchargé, chaque reprise immédiate ajoute de la charge, et le service va plus mal pour tout le monde. Attendez d’abord, et de plus en plus longtemps.
Attente croissante et gigue
L’attente croissante double. Avec une base de 400, les attentes sont 400, 800, 1600. Mettez-lui un plafond, ici 2000, sinon elle grandit sans fin.
Si six appelants échouent ensemble et attendent tous 400, ils reviennent tous ensemble. Une gigue ajoute un petit nombre différent pour chacun : ils reviennent un par un.
Exemple détaillé : base 400, plafond 2000
| Essai raté | Attente | Gigue de la demande 3 | Prochain essai à (depuis 0) |
|---|---|---|---|
| 1 | 400 | 34 | 434 |
| 2 | 800 | 47 | 1281 (depuis 434) |
| 3 | 1600 | 10 | 2891 (depuis 1281) |
| 4 | 2000, le plafond | Non utilisée | Abandon si les essais sont limités à 4 |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.
Une reprise doit pouvoir se répéter sans risque
Parfois un appel a réussi mais la réponse s’est perdue en chemin. L’appelant croit à un échec et réessaie. Si la reprise débite ou envoie encore, vous avez agi deux fois.
Donnez une clé à chaque action. Le serveur retient les clés déjà vues. Une répétition avec une clé connue ne fait rien de nouveau.
É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 de reprises, partie 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { demander } = require(\"./model\");' 'function attente(essai, base, plafond) { return Math.min(base * Math.pow(2, essai - 1), plafond); }' 'function gigue(id, essai) { return (id * 7 + essai * 13) % 50; }' 'function executer(nombre, limite, fenetre, base, plafond, essais, gigueActive) {' ' const file = [];' ' const historique = {};' ' const comptes = {};' ' for (let id = 1; id <= nombre; id++) { file.push([0, id, 1]); historique[id] = []; }' > reprises.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 de reprises, partie 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' ' while (file.length > 0) {' ' file.sort(function (a, b) { return a[0] - b[0] || a[1] - b[1] || a[2] - b[2]; });' ' const item = file.shift();' ' const creneau = Math.floor(item[0] / fenetre);' ' historique[item[1]].push(item[0]);' ' comptes[creneau] = comptes[creneau] || 0;' ' if (comptes[creneau] < limite) { comptes[creneau] = comptes[creneau] + 1; continue; }' ' if (item[2] < essais) {' ' const ajout = gigueActive ? gigue(item[1], item[2]) : 0;' ' file.push([item[0] + attente(item[2], base, plafond) + ajout, item[1], item[2] + 1]);' ' }' ' }' ' return historique;' '}' >> reprises.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 de reprises, partie 3
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' ' for (const active of [false, true]) {' ' const h = executer(6, 2, 1000, 400, 2000, 4, active);' ' const vus = {};' ' let pire = 0;' ' for (const id in h) h[id].slice(1).forEach(function (t) { vus[t] = (vus[t] || 0) + 1; pire = Math.max(pire, vus[t]); });' ' console.log((active ? \"avec gigue\" : \"sans gigue\") + \" : plus de reprises sur une milliseconde \" + pire);' ' console.log(\" la requête 3 essaie à \" + h[3].join(\" \") + \", la requête 5 à \" + h[5].join(\" \"));' ' }' ' const reponse = await demander([{ role: \"user\", content: \"En une phrase courte, pourquoi ajouter une attente aléatoire à une reprise ?\" }], \"Pour que de nombreux appelants ne reprennent pas tous au même instant et ne surchargent pas à nouveau le service.\", 60);' ' console.log(\"le modèle dit : \" + reponse.texte);' '}' 'main();' >> reprises.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 les reprises
docker exec -w /home/node/project agent node reprises.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 |
|---|---|
| sans gigue : plus de reprises sur une milliseconde 4 | Quatre appelants sont revenus au même instant |
| la requête 3 essaie à 0 400 1200, la requête 5 à 0 400 1200 2800 | Sans gigue, les instants sont réguliers et identiques |
| avec gigue : plus de reprises sur une milliseconde 1 | Deux reprises ne partagent jamais un instant |
| la requête 3 essaie à 0 434 1281 2891, la requête 5 à 0 448 1259 2883 | Chaque appelant attend un peu différemment |
| 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.