Aller au contenu
astorlm
Langue: Français
← Carte

Niveau 2

La boucle de l'agent

À lui seul, un modèle ne peut rien faire : il ne fait que générer du texte. La boucle de l'agent est ce qui le transforme en agent. Elle donne l'historique au modèle, exécute les outils que le modèle demande, lui rend les résultats, et recommence jusqu'à ce que le modèle dise « terminé ».
1/17 Plis du bandonéon :
  • user
  • assistant
  • tool_result
Maman oiseau, votre app, appelle agent.run(). Astor (la boucle) joue la question dans son bandonéon (messages[]) et la porte à l'Oracle (le modèle).

EventBus

Le problème

Une maman oiseau demande à un modèle « pouvez-vous nettoyer le fort des cochons ? ». Le modèle ne peut rien lancer. Le mieux qu'il puisse faire, c'est répondre par une demande d'outil : { name: "launch_red", input: { angle: 40 } }.

Si votre code ne fait qu'un seul appel au modèle, la conversation s'arrête là. Vous vous retrouvez avec une demande que personne n'a exécutée et sans réponse. Si vous exécutez l'outil à la main, vous retombez sur le même problème au tour suivant, parce que le modèle peut avoir besoin d'un autre outil, puis d'un autre encore.

La solution

Une boucle avec une seule règle de sortie :

  1. Envoyez au modèle tout l'historique plus la liste des outils disponibles.
  2. Si la réponse se termine par stopReason: "end_turn", renvoyez-la. C'est la seule sortie normale.
  3. Si elle se termine par "tool_use", exécutez chaque outil demandé, ajoutez les résultats à l'historique sous forme de blocs tool_result et revenez à l'étape 1.

Le modèle décide quoi faire ; c'est la boucle qui le fait. Cette séparation est la base de tous les autres patterns : tout le reste (steering, compaction du contexte, sous-agents) se branche à un endroit de cette boucle.

Les personnages

La boucle racontée comme une petite quête. Une fois que vous connaissez les personnages, il n'y a plus rien à décoder.

Astor la boucle
Un petit danseur de tango, et le seul qui bouge. Il porte la question à l'Oracle, court au lance-pierre à chaque tir et rapporte la réponse à maman oiseau.
L'Oracle Provider
Le modèle. Il ne touche jamais au lance-pierre : il écoute seulement le bandonéon et rend une note. Orange s'il a besoin d'un outil, dorée s'il a fini.
Le bandonéon messages[]
L'historique, un pli coloré par message. Le soufflet grandit à chaque tour, et l'Oracle écoute chaque pli à chaque fois. Ce sont les notes qui montent vers l'Oracle.
Le banc ToolRegistry
Un oiseau par outil : launch_red, launch_bomb et un troisième dont personne n'a besoin aujourd'hui. Astor lance celui que la note désigne et montre le résultat : vert si ça a marché.
Maman oiseau agent.run()
Votre code. Elle pose la question et attend.
Trajectoires, score et oiseaux historique, tokens, maxTurns
Chaque tir laisse sa trajectoire dans le ciel, comme l'historique garde chaque résultat. Le score, ce sont les tokens, et il grimpe davantage à chaque tour parce que tout l'historique est renvoyé. Chaque tour coûte un oiseau de la rangée de la barre du haut, le budget de tours. Les chiffres sont illustratifs.

Le panneau EventBus montre les événements que la vraie boucle émet à chaque étape de l'animation.

Le code

Avec astorlm : La même boucle vit dans src/agent/loop.ts, avec le streaming, les nouvelles tentatives, les hooks, l'exécution des outils en parallèle et l'annulation. Vue de l'extérieur, elle ressemble à ceci.

À partir de zéro : Une quarantaine de lignes contre n'importe quel endpoint compatible OpenAI, sans SDK : du fetch brut en TypeScript, la bibliothèque standard en Python. Les trois étapes ci-dessus sont signalées dans les commentaires. Remplissez le bloc LLM du début avec votre propre endpoint, votre modèle et votre clé.

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

// Your game's functions, wrapped as tools: one per bird.
const angle = z.number().min(10).max(80).describe('Launch angle in degrees')

const launchRed = tool({
  name: 'launch_red',
  description: 'Fling the red bird. Good against wood. Returns what fell and how many pigs are left.',
  schema: z.object({ angle }),
  execute: async ({ angle }) => level.fling('red', angle), // your code
})

const launchBomb = tool({
  name: 'launch_bomb',
  description: 'Fling the bomb bird. It explodes on impact: the one to use against stone.',
  schema: z.object({ angle }),
  execute: async ({ angle }) => level.fling('bomb', angle), // your code
})

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: [launchRed, launchBomb],
  maxTurns: 10, // the birds in line: a cap on the laps
})

agent.on('tool-start', (tool) => console.log('→', tool.name, tool.input))

const answer = await agent.run('The pigs took our eggs! Can you clear their fort?')

Remarquez qu'une erreur d'outil n'arrête pas la boucle : elle revient au modèle sous forme de texte, pour qu'il puisse se corriger au tour suivant.

Quand l'utiliser, et à quoi faire attention

Chaque fois que le modèle doit agir (lire, chercher, exécuter quelque chose) avant de pouvoir répondre. Si vous avez seulement besoin de transformer du texte, un seul appel suffit, et coûte moins cher.

  • Le coût augmente à chaque tour. Surveillez le bandonéon et le compteur de tokens : l'Oracle écoute chaque pli à chaque tour, donc chaque tour renvoie tout ce qui précède. Avec assez de tours, le contexte se remplit.
  • Définissez toujours maxTurns. Un modèle perdu peut continuer à demander des outils indéfiniment. Sans limite, la boucle tourne indéfiniment aussi.