Aller au contenu
astorlm
Langue: Français
← Carte

Niveau 5

Les erreurs dans la boucle

Les outils échouent. Les modèles envoient de mauvaises entrées. Les serveurs tombent une seconde. Une boucle d'agent doit décider, pour chaque échec, qui s'en occupe : le modèle, votre code, ou personne.
1/40 Plis du bandonéon :
  • user
  • assistant
  • tool_result
  • tool_result (erreur)
Un agent qui joue à un jeu d'aventure : ses outils sont les verbes du bas, LOOK AT et USE. Cette boucle a été écrite à la main, et elle exécute les outils sans try/catch.

EventBus

Le problème

Un agent joue à un jeu d'aventure. Ses outils sont les verbes en bas de l'écran, look_at et use, et la tâche est « ouvre la porte du temple ». Le modèle fait l'évidence : utiliser la clé sur la porte. Mais la serrure est rouillée, et l'outil lève une exception.

Face à ce genre de moment, les vieux jeux d'aventure se divisaient en deux écoles. Dans certains, un mauvais coup vous tuait : game over, retour à votre dernière sauvegarde. Dans d'autres, on ne pouvait pas mourir ; le jeu vous disait simplement pourquoi ça ne marchait pas, et vous essayiez autre chose. Une boucle d'agent doit, elle aussi, choisir son école.

Si la boucle n'attrape pas l'erreur, Krash gagne : l'exception remonte à travers la boucle et run() la lève. L'exécution est perdue, alors que l'erreur disait exactement quoi faire : « huile-la d'abord ». Le seul lecteur qui pouvait se servir de ce message ne l'a jamais reçu. Attrapez-la, renvoyez-la comme résultat d'outil, et le modèle la lit et réessaie. Cela coûte un tour. Ne pas l'attraper coûte toute l'exécution.

Trois sortes d'échecs

  • Le modèle a mal demandé

    Entrée invalide, JSON cassé, un outil qui n'existe pas, une clé qui ne tourne pas.

    Le modèle. L'erreur revient comme un résultat d'outil marqué en erreur. Le modèle la lit et essaie autre chose au tour suivant.

  • Le serveur du modèle a échoué

    429 (trop de requêtes), 5xx (problème côté serveur), une connexion coupée.

    Votre code, en réessayant. Attendez un peu et rappelez, un peu plus longtemps à chaque fois. Le modèle n'en sait jamais rien : il n'y a eu aucun tour à lire.

  • Personne ne peut corriger

    Un 401 (mauvaise clé d'API), un bug dans votre code, l'utilisateur a annulé.

    Personne. Laissez l'exception remonter. Réessayer n'y changera rien, et le cacher au modèle ne fait que le pousser à deviner.

Toute l'astuce est de ne pas les mélanger. Réessayer un 400 renvoie la même requête cassée. Montrer un 503 au modèle gaspille un tour sur quelque chose qu'il ne peut pas réparer. Et attraper un bug de votre propre code ne fait que le cacher.

Le code

Avec astorlm : Les outils se contentent de lever des exceptions, et astorlm se charge de les attraper. Vous activez retry pour le serveur du modèle, et vous écoutez l'EventBus pour voir les deux couches à l'œuvre.

À partir de zéro : La boucle du niveau 2, avec une fonction par couche : callModel réessaie le serveur avec backoff, runTool transforme chaque échec d'outil en texte lisible par le modèle, et tout le reste est laissé libre de lever. Par-dessus, un petit compteur abandonne quand le modèle se heurte sans cesse au même mur.

import { OpenAIProvider, createLocalAgent, tool } from 'astorlm'
import { z } from 'zod'

const room = { lockOiled: false, doorOpen: false }

const use = tool({
  name: 'use',
  description: 'Use an inventory item with something in the room.',
  schema: z.object({ item: z.enum(['key', 'oil_can']), target: z.string() }),
  execute: async ({ item, target }) => {
    if (item === 'oil_can' && target === 'lock') {
      room.lockOiled = true
      return 'You oil the lock. It looks like it might turn now.'
    }
    if (item === 'key' && target === 'door') {
      // Just throw. astorlm catches it and sends the message back to the model, marked is_error.
      if (!room.lockOiled) throw new Error('The lock is rusted shut and the key won’t turn. Oil it first: use oil_can with lock.')
      room.doorOpen = true
      return 'Click! The key turns and the door swings open.'
    }
    throw new Error(`Nothing happens when you use ${item} with ${target}.`)
  },
})

const agent = await createLocalAgent({
  // Any OpenAI-compatible endpoint: OpenAI, Ollama, LM Studio, vLLM, a proxy…
  provider: new OpenAIProvider({
    baseURL: 'http://localhost:11434/v1', // e.g. Ollama's default address
    model: 'your-model', // e.g. 'llama3.1', 'gpt-4o-mini'
    apiKey: 'YOUR_API_KEY', // local servers usually ignore it
  }),
  tools: [lookAt, use],
  // Off by default: without it, a single 429 ends the run.
  retry: { maxAttempts: 3, baseDelayMs: 1000 }, // waits up to 1s, then up to 2s
  maxTurns: 10, // also the ceiling for a model stuck on the same wrong move
})

// Watch both layers as they happen.
agent.on('tool-end', ({ name, output, isError }) => {
  if (isError) console.warn(`${name} failed, and the model will read why: ${output}`)
})
agent.on('event', (event) => {
  if (event.type === 'provider_retry') {
    console.warn(`Model call failed. Attempt ${event.attempt + 1}/${event.maxAttempts} in ${event.delayMs}ms`)
  }
})

try {
  const last = await agent.run('Open the temple door.')
  console.log(last.content)
} catch (err) {
  // Only what nobody could handle gets here: retries used up, a 401, a bug in your code.
  console.error('The run failed:', err)
}

Points de vigilance

  • Écrivez les erreurs pour le modèle. Dites ce qui n'allait pas et quoi faire à la place : « la serrure est bloquée par la rouille. Huile-la d'abord : use oil_can avec lock ». Avec « Error 400 », vous obtiendrez le même appel une fois de plus.
  • Ne laissez pas fuiter ce que le modèle ne devrait pas voir. Les stack traces, les chemins de fichiers et les chaînes de connexion vont dans vos logs. Le modèle reçoit une phrase claire.
  • Les erreurs aussi peuvent tourner en boucle. Un modèle peut retenter le même mauvais coup jusqu'à épuisement de maxTurns. Arrêtez-vous après quelques erreurs d'affilée, ou interceptez-le avec un hook (niveau suivant).
  • Réessayez avec une limite et une attente aléatoire. Quelques tentatives, un plafond qui double à chaque fois, et un peu d'aléatoire (jitter) pour que cent clients ne reviennent pas tous à la même seconde.
  • Prudence en réessayant des outils qui modifient des choses. Si un appel qui déplace de l'argent ou réserve une chambre a expiré, il a peut-être abouti quand même. Vérifiez avant de le refaire.