Pular para o conteúdo
astorlm
Idioma: Português
← Mapa

Nível 6

Streaming e eventos

Um modelo escreve a resposta alguns tokens por vez. O streaming entrega cada pedacinho assim que é escrito, e os eventos do loop contam para o resto do seu app o que o agente está fazendo, enquanto ele faz.
1/37 Dobras do bandoneón:
  • user
  • assistant
  • tool_result
Começa o show. O Oráculo canta, as notas descem pela pista e a pessoa da guitarra as toca para o público. Primeiro, uma requisição comum: sem streaming.

EventBus

O problema

Chamar um modelo e esperar a resposta inteira funciona num script. Na frente de uma pessoa, não: uma resposta longa pode levar dez ou vinte segundos, e durante todos eles a tela não mostra nada. Um agente piora isso, porque uma requisição pode rodar vários turnos e ferramentas antes de a resposta final existir.

E não é só a pessoa que quer acompanhar. A interface quer o texto, seus logs querem cada passo, o faturamento quer os tokens de cada turno. Se cada uma dessas coisas tiver que ser escrita dentro do loop, o loop acaba conhecendo cada tela, arquivo e banco de dados do seu app.

A solução

Duas peças que andam juntas. Streaming: peça ao provedor a resposta como um fluxo, e ele manda cada pedacinho de texto assim que o modelo o escreve. O tempo total é o mesmo, mas a primeira palavra aparece numa fração de segundo em vez de no final.

Eventos: o loop anuncia cada passo num bus, e não se importa com quem está ouvindo. Seu app inscreve as partes que se interessam: a tela escuta o texto, o log escuta tudo, o medidor escuta o fim de cada turno. Adicionar um ouvinte não mexe no loop. Estes são os eventos que o astorlm emite numa execução normal:

  • turn_start

    Começa um turno: o loop está prestes a chamar o modelo.

  • text_delta

    Chegou um pedacinho da resposta. São muitos por turno.

  • assistant_message

    A mensagem do modelo está completa, com o texto e as chamadas de ferramenta.

  • tool_execution_start

    Uma ferramenta está prestes a rodar, com sua entrada.

  • tool_execution_end

    A ferramenta terminou: sua saída, se falhou e quanto tempo levou.

  • turn_end

    O turno terminou, com os tokens que sua chamada ao modelo usou.

  • session_end

    A execução terminou: concluída, cancelada ou com erro.

As chamadas de ferramenta também chegam do provedor em streaming, alguns caracteres dos argumentos por vez. O loop as remonta antes de rodá-las, então no bus uma chamada de ferramenta aparece inteira, em assistant_message.

O elenco

O mesmo elenco de sempre, num palco de rock.

O Oráculo o modelo
O vocalista, lá no praticável. Sua resposta desce pela pista como notas, uma por palavra.
A pista o stream
Cada text_delta é um punhado de notas. Sem streaming, nada desce até o final.
A pessoa da guitarra seu app
Toca cada nota quando ela chega aos trastes, e as letras aparecem para o público.
O cabo o EventBus
Todos os eventos correm por ele. Só acendem os ouvintes que se inscreveram naquele evento.
Os ouvintes agent.on(…)
A tela de letras ('text'), um gravador de fita ('event') e um medidor de tokens (turn_end).
Astor o loop
Roda o loop como sempre, e anuncia cada passo no bus conforme avança.
O palco as ferramentas
A beira do palco, a mesa de luz e a caixa de pirotecnia: crowd_mood, set_lights e pyro.

O código

Com astorlm: o provedor sempre faz streaming, então não há nada para ligar. Inscreva-se com agent.on() antes de chamar run(): 'text' para cada pedacinho de texto, 'tool-start' e 'tool-end' para as ferramentas, 'event' para tudo. Cada chamada devolve uma função que cancela a inscrição.

Do zero: o loop do nível 2 com duas mudanças. A requisição usa stream: true e lê a resposta como server-sent events, colando os pedacinhos; e uma lista de ouvintes recebe cada passo por um único 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

O que observar

  • O streaming não deixa o modelo mais rápido. A resposta leva o mesmo tempo de antes; o que muda é quando a pessoa vê a primeira palavra. Meça o tempo até o primeiro token, não só o tempo total.
  • Ouvintes observam; não dirigem. O que um ouvinte devolve é ignorado, e o loop não espera por ele. Para bloquear uma ferramenta ou mudar o que o modelo lê, você precisa de um hook: o próximo nível.
  • Mantenha os ouvintes rápidos. O bus do astorlm os chama um depois do outro, dentro do loop. Trabalho lento, como gravar num banco de dados, vai numa fila para a qual o ouvinte só empurra.
  • Um ouvinte quebrado falha em silêncio. O astorlm captura o erro para a execução seguir, o que também significa que ninguém fica sabendo. Registre os erros dentro dos seus ouvintes.
  • Os pedacinhos não respeitam as palavras. Um text_delta pode terminar no meio de uma palavra ou de uma tabela em Markdown. Mostre o que você tem até agora e redesenhe conforme chega mais.
  • Deixe a pessoa parar. Quando ela pode ver a resposta chegando, vai querer cortar uma que está indo mal. Ligue um botão de parar a agent.abort().