Aller au contenu
astorlm
Langue: Français
← Carte

Niveau 6

Les hooks

Les événements vous permettent d'observer la boucle. Les hooks vous permettent de la changer. Un hook est une fonction à vous que la boucle appelle à un point fixe, et quoi qu'elle renvoie, la boucle obéit.
1/26 Plis du bandonéon :
  • user
  • assistant
  • tool_result
  • tool_result (erreur)
Un assistant de voyages d'entreprise. La boucle est un circuit fermé, et chaque point de hook est une cabine le long de la voie. Cette exécution en tient deux : l'une vérifie chaque appel d'outil avant qu'il ne s'exécute, l'autre vérifie chaque résultat avant que le modèle ne le lise.

EventBus

Le problème

Votre assistant de voyages fonctionne. Puis l'entreprise ajoute une règle : pas de billets en première classe sans l'accord d'un responsable. Et le service juridique en ajoute une autre : le numéro d'identité du passager ne doit jamais atteindre le modèle.

Aucune de ces règles ne concerne le modèle. On peut le lui dire, mais un prompt est une demande, pas un verrou. Les deux règles portent sur ce que fait la boucle : quels appels d'outils elle exécute, et ce qu'elle met dans l'historique. Si la boucle ne vous offre aucune entrée, la seule option restante est de copier son code et de le modifier. C'est Ironclad, la boucle scellée.

Les événements n'aident pas non plus. Un événement vous dit que book_ticket est sur le point de s'exécuter. Le temps que votre listener le reçoive, rien de ce que vous y faites ne peut l'arrêter.

La solution

La boucle appelle vos fonctions à des points fixes de chaque tour, et utilise ce qu'elles renvoient. Cinq points couvrent presque tout :

  • beforeTurn

    Au début de chaque tour.

    Vérifier un budget, journaliser le tour, arrêter une exécution qui dure depuis trop longtemps.

  • beforeProviderCall

    Juste avant que la requête parte vers le modèle.

    Changer ce qui est envoyé : élaguer les vieux messages, ajouter la date du jour, cacher un outil pour ce tour.

  • beforeToolExecution

    Après que le modèle a demandé un outil, avant qu'il ne s'exécute.

    Le laisser passer, le refuser (le modèle reçoit votre raison à la place) ou répondre avec un résultat tout prêt.

  • afterToolExecution

    Après l'exécution de l'outil, avant que le résultat n'entre dans l'historique.

    Réécrire ce que le modèle va lire : masquer des données personnelles, raccourcir une sortie énorme.

  • afterTurn

    Une fois arrivés la réponse du modèle et les résultats d'outils.

    Sauvegarder la progression, mettre à jour un tableau de bord, compter le coût.

La règle d'or : les événements observent, les hooks changent. Utilisez un événement quand vous voulez seulement savoir ce qui s'est passé. Utilisez un hook quand vous devez décider de ce qui se passe.

Les personnages

Les mêmes personnages que d'habitude, cette fois sur un train miniature.

Le circuit la boucle
Un anneau de rails fermé qui ne tourne que dans un sens. Chaque tour de piste est un tour : devant l'Oracle, devant les outils, et on recommence.
Astor celui qui parcourt la boucle
Il pompe la draisine sur le circuit, avec le bandonéon des messages sur le dos.
Les cabines hooks
Une par point de hook. Une cabine vide ne fait rien. Une cabine tenue arrête la draisine, vérifie ce qu'elle transporte, et peut baisser sa barrière ou tamponner le chargement. Cette exécution en tient deux : beforeToolExecution et afterToolExecution.
Les tribunes EventBus
Trois spectateurs qui notent tout ce qui passe. Ils voient tout, et ne peuvent toucher à rien.
Les quais outils
find_trains et book_ticket, dans la courbe du fond.

Dans le panneau EventBus, les lignes hook et code signalent vos propres fonctions en train de s'exécuter : vos hooks et vos outils. astorlm n'émet pas d'événements pour elles. Remarquez où elles tombent : tool_execution_end arrive après afterToolExecution, donc il porte déjà le texte tamponné.

Le code

Avec astorlm : Passez un objet hooks à l'agent. beforeToolExecution renvoie { authorize: false } pour refuser un appel, et afterToolExecution renvoie le texte que lira le modèle.

À partir de zéro : La boucle du niveau 2, avec un appel à chacun des cinq hooks. Un hook que personne n'a défini est simplement ignoré.

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

const findTrains = tool({
  name: 'find_trains',
  description: 'List the trains to a destination on a date, with the fare for each class.',
  schema: z.object({ to: z.string(), date: z.string() }),
  execute: async ({ to, date }) => searchTimetable(to, date), // your code
})

const bookTicket = tool({
  name: 'book_ticket',
  description: 'Book one seat on a train for the employee who is asking.',
  schema: z.object({ train: z.number().int(), seat_class: z.enum(['first', 'tourist']) }),
  execute: async ({ train, seat_class }) => reserveSeat(train, seat_class), // 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: [findTrains, bookTicket],
  maxTurns: 10,
  hooks: {
    // The first booth: runs before every tool call, and decides whether it runs at all.
    beforeToolExecution: async ({ toolName, input }) => {
      const { seat_class } = input as { seat_class?: string }
      if (toolName === 'book_ticket' && seat_class === 'first') {
        // The tool never runs. The model reads this text as an error result instead.
        return { authorize: false, mockResult: 'Blocked by policy: first class needs a manager’s approval. Book tourist instead.' }
      }
      return { authorize: true }
    },
    // The second booth: runs after every tool call. What you return is what the model reads.
    afterToolExecution: async ({ output }) => output.replace(/DNI [\d.]+/g, 'DNI ***'),
  },
})

// Events only watch. By the time this fires, the hook has already stamped over the DNI.
agent.on('tool-end', ({ name, output, isError }) => console.log(name, isError ? 'refused:' : 'ok:', output))

const last = await agent.run('Book me the most comfortable seat to Mar del Plata on Friday.')
console.log(last.content)

Points de vigilance

  • Dites pourquoi quand vous refusez. Le refus revient au modèle comme un résultat en erreur. « Bloqué par la politique : réservez en touriste à la place » vous vaut un billet touriste. Un simple « refusé » vous vaut le même appel une fois de plus.
  • Les hooks s'exécutent à chaque appel, alors gardez-les rapides. Un hook qui interroge une base de données ajoute ce délai à chaque outil et à chaque tour.
  • Un hook qui lève une exception fait tomber l'exécution. La boucle attrape les erreurs de vos outils, pas celles de vos hooks. Enveloppez tout ce qui peut échouer.
  • N'utilisez pas un hook pour observer. Si vous ne faites que journaliser, écoutez les événements. Gardez les hooks pour les moments où vous devez changer quelque chose.