Aller au contenu
astorlm
Langue: Français
← Carte

Niveau 6

Streaming et événements

Un modèle écrit sa réponse quelques tokens à la fois. Le streaming vous remet chaque morceau dès qu'il est écrit, et les événements de la boucle racontent au reste de votre app ce que fait l'agent, pendant qu'il le fait.
1/37 Plis du bandonéon :
  • user
  • assistant
  • tool_result
C'est l'heure du show. L'Oracle chante, les notes descendent la piste et la personne à la guitare les joue pour le public. D'abord, une requête simple : sans streaming.

EventBus

Le problème

Appeler un modèle et attendre la réponse entière, ça marche dans un script. Devant une personne, non : une longue réponse peut prendre dix ou vingt secondes, et pendant tout ce temps l'écran n'affiche rien. Un agent aggrave les choses, car une requête peut enchaîner plusieurs tours et outils avant que la réponse finale existe.

Et il n'y a pas que la personne qui veut suivre. L'interface veut le texte, vos logs veulent chaque étape, la facturation veut les tokens de chaque tour. S'il faut écrire chacune de ces choses dans la boucle, la boucle finit par connaître chaque écran, fichier et base de données de votre app.

La solution

Deux pièces qui vont ensemble. Le streaming : demandez la réponse au fournisseur sous forme de flux, et il envoie chaque morceau de texte dès que le modèle l'écrit. Le temps total est le même, mais le premier mot apparaît en une fraction de seconde au lieu d'arriver à la fin.

Les événements : la boucle annonce chaque étape sur un bus, sans se soucier de qui écoute. Votre app abonne les parties intéressées : l'écran écoute le texte, le log écoute tout, le compteur écoute la fin de chaque tour. Ajouter un auditeur ne touche pas la boucle. Voici les événements qu'astorlm émet lors d'une exécution normale :

  • turn_start

    Un tour commence : la boucle s'apprête à appeler le modèle.

  • text_delta

    Un morceau de la réponse est arrivé. Il y en a beaucoup par tour.

  • assistant_message

    Le message du modèle est complet, texte et appels d'outils compris.

  • tool_execution_start

    Un outil s'apprête à s'exécuter, avec son entrée.

  • tool_execution_end

    L'outil a fini : sa sortie, s'il a échoué, combien de temps il a pris.

  • turn_end

    Le tour est fini, avec les tokens de son appel au modèle.

  • session_end

    L'exécution est finie : terminée, annulée ou en erreur.

Les appels d'outils arrivent aussi du fournisseur en streaming, quelques caractères de leurs arguments à la fois. La boucle les reconstitue avant de les exécuter : sur le bus, un appel d'outil arrive donc entier, dans assistant_message.

Les personnages

Les mêmes personnages que d'habitude, sur une scène rock.

L'Oracle le modèle
Le chanteur, en haut sur l'estrade. Sa réponse descend la piste en notes, une par mot.
La piste le stream
Chaque text_delta est une poignée de notes. Sans streaming, rien ne descend avant la fin.
La personne à la guitare votre app
Joue chaque note quand elle atteint les frettes, et les paroles apparaissent pour le public.
Le câble l'EventBus
Tous les événements le parcourent. Seuls s'allument les auditeurs abonnés à cet événement.
Les auditeurs agent.on(…)
L'écran des paroles ('text'), un magnétophone ('event') et un compteur de tokens (turn_end).
Astor la boucle
Fait tourner la boucle comme d'habitude, et annonce chaque étape sur le bus au fur et à mesure.
La scène les outils
Le bord de scène, la console lumière et la boîte pyro : crowd_mood, set_lights et pyro.

Le code

Avec astorlm : le fournisseur fait toujours du streaming, il n'y a donc rien à activer. Abonnez-vous avec agent.on() avant d'appeler run() : 'text' pour chaque morceau de texte, 'tool-start' et 'tool-end' pour les outils, 'event' pour tout. Chaque appel renvoie une fonction qui annule l'abonnement.

À partir de zéro : la boucle du niveau 2 avec deux changements. La requête utilise stream: true et lit la réponse comme des server-sent events, en recollant les morceaux ; et une liste d'auditeurs reçoit chaque étape via un seul emit().

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

// The stage's functions, wrapped as tools.
const crowdMood = tool({
  name: 'crowd_mood',
  description: 'Look at the crowd from the front of the stage: how they feel, and what they came to hear.',
  schema: z.object({}),
  execute: async () => stage.readCrowd(), // your code
})

const setLights = tool({
  name: 'set_lights',
  description: 'Set the color of every light on the stage.',
  schema: z.object({ color: z.enum(['amber', 'red', 'blue', 'white']) }),
  execute: async ({ color }) => stage.lights(color), // your code
})

const pyro = tool({
  name: 'pyro',
  description: 'Fire the pyrotechnics on both sides of the stage. Once per show.',
  schema: z.object({}),
  execute: async () => stage.firePyro(), // 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: [crowdMood, setLights, pyro],
  maxTurns: 10,
})

// Streaming is always on: subscribe before run(). Each listener only hears what it asked for.
// The lyrics screen: every piece of text, the moment it arrives.
agent.on('text', (piece) => lyrics.append(piece))

// The tape deck: every event the loop emits, in order.
agent.on('event', (event) => tape.write(event))

// The meter: the tokens each turn reported.
agent.on('event', (event) => {
  if (event.type === 'turn_end' && event.usage) meter.add(event.usage.inputTokens + event.usage.outputTokens)
})

// Each on() returns a function that unsubscribes.
const stopTape = agent.on('tool-start', ({ name }) => console.log('→', name))

const last = await agent.run('Open the show: read the crowd, set the mood, and greet them.')
stopTape()
console.log(last.content) // the same text, whole, once the run is over

Points de vigilance

  • Le streaming ne rend pas le modèle plus rapide. La réponse prend autant de temps qu'avant ; ce qui change, c'est le moment où la personne voit le premier mot. Mesurez le temps jusqu'au premier token, pas seulement le temps total.
  • Les auditeurs regardent ; ils ne pilotent pas. Ce qu'un auditeur renvoie est ignoré, et la boucle ne l'attend pas. Pour bloquer un outil ou changer ce que lit le modèle, il vous faut un hook : le niveau suivant.
  • Gardez les auditeurs rapides. Le bus d'astorlm les appelle l'un après l'autre, dans la boucle. Le travail lent, comme écrire dans une base de données, va dans une file où l'auditeur se contente de pousser.
  • Un auditeur cassé échoue en silence. astorlm attrape son erreur pour que l'exécution continue, ce qui veut aussi dire que personne ne le sait. Journalisez dans vos auditeurs.
  • Les morceaux ne respectent pas les mots. Un text_delta peut s'arrêter au milieu d'un mot ou d'un tableau Markdown. Affichez ce que vous avez et redessinez à mesure que la suite arrive.
  • Laissez la personne arrêter. Dès qu'elle voit la réponse arriver, elle voudra couper court à une réponse qui part mal. Branchez un bouton stop sur agent.abort().