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

Nível 11

Observabilidade e avaliações

Um trace mostra o que o agente fez numa execução. Uma avaliação dá nota a ele em muitas execuções. Você precisa dos dois: a avaliação te diz que algo quebrou, o trace te diz por quê.
1/59 Dobras do bandoneón:
  • user
  • assistant
  • tool_result
  • tool_result (erro)
Caso 1 de 3. Cada buraco é um caso de avaliação: um prompt, e como é uma boa execução. Par 3: um ferro, depois um putt.

EventBus

O problema

Um agente não é uma função que você consegue ler de cima a baixo. O modelo decide os passos em tempo de execução, e o mesmo prompt pode seguir outro caminho amanhã. Quando um usuário diz “ele reservou a coisa errada”, você precisa saber o que ele fez, passo a passo. E quando você muda o prompt, uma ferramenta ou o modelo, precisa saber se melhorou ou piorou.

Esse é o Blindspot, a caixa-preta. Parece funcionar, até deixar de funcionar, e aí ninguém sabe dizer o que aconteceu, quanto custou ou desde quando. Testar uns prompts na mão e dizer “parece bom” é o jeito como o Blindspot vai para produção.

A solução

Observabilidade significa registrar cada execução como um trace: uma árvore de spans, um por unidade de trabalho, cada um com quanto tempo levou e quanto custou. A execução é um span; cada turno, cada chamada ao modelo e cada ferramenta também. Um span que deu errado é marcado como erro. Todo loop deste site já emite os eventos; um tracer só escuta o EventBus e cronometra. Envie os spans para qualquer ferramenta de OpenTelemetry (o padrão aberto para traces) e você tem a linha do tempo da animação.

As avaliações (evals) são testes para agentes. Um dataset guarda os casos: um prompt, e como é uma boa execução. Você roda cada caso com um agente novo, e os scorers dão nota a cada execução. A fração de casos que passam é o seu número a acompanhar. Há quatro tipos de scorer, e uma boa avaliação mistura todos:

  • A resposta

    Comparar o texto final com o que o caso espera: exatamente, por uma palavra que ele precisa conter ou por um padrão.

    Barato e determinístico. Só vê o final: uma resposta certa alcançada pelo caminho errado passa mesmo assim.

  • A trajetória

    Comparar as ferramentas que o agente chamou, em ordem, com as que o caso espera.

    Pega um caminho ruim até uma boa resposta, como o buraco 3. Rígido demais, reprova execuções que tomaram outra rota válida: permita extras onde eles não fazem mal.

  • A sua própria checagem

    Qualquer função de uma execução para uma nota: tacadas no par ou abaixo, um orçamento de tokens, um campo na saída.

    O que importar para o seu produto. Mantenha-a determinística quando puder.

  • Um modelo como juiz

    Outro modelo lê a pergunta, a resposta e uma rubrica, e dá uma nota.

    Para respostas sem um único texto certo: tom, utilidade, um resumo. Mais lento, custa uma chamada e precisa de uma rubrica precisa e de algumas conferências contra notas humanas.

O buraco 3 é o motivo de você querer mais de um. A resposta dizia “embocada em 4”, e 4 é o par. Só o scorer de trajetória viu o driver onde o caso esperava um ferro. E só o trace mostrou o que aconteceu ali: um span vermelho, uma bola na água.

O elenco

O mesmo elenco de sempre, desta vez num campo de golfe.

Um buraco um caso de avaliação
Um prompt, e como é uma boa execução: o par e os tacos a usar.
O caddie o modelo
Lê o buraco e escolhe um taco. Nunca dá a tacada.
Os tacos as ferramentas
drive, iron e putt. Quem dá a tacada é o Astor.
As linhas de voo o trace
Cada voo fica desenhado no campo, e cada passo é um span na linha do tempo: chamadas ao modelo em azul, ferramentas em laranja, erros em vermelho.
O fiscal o seu código de avaliação
Entrega cada caso a um agente novo e carimba o cartão quando termina.
O cartão o relatório
Uma linha por caso, uma checagem por scorer, e a taxa de aprovação no rodapé.

O código

Com astorlm: attachTracer transforma os eventos de um agente em spans para qualquer exportador. runEval roda um dataset com um agente novo por caso, aplica os seus scorers e devolve um relatório com a taxa de aprovação por scorer. Os dois vivem em astorlm/experimental.

Do zero: O loop do nível 2 com um cronômetro em volta de cada chamada ao modelo e de cada ferramenta, e um for pelos casos com uma função por scorer.

import { OpenAIProvider, createLocalAgent } from 'astorlm'
import { attachTracer, createInMemoryExporter } from 'astorlm/experimental/tracing'
import { contains, runEval, toolTrajectory, type EvalCase, type Scorer } from 'astorlm/experimental/evals'

// Any OpenAI-compatible endpoint: OpenAI, Ollama, LM Studio, vLLM, a proxy…
const LLM = { baseURL: 'http://localhost:11434/v1', apiKey: 'YOUR_API_KEY' } // local servers usually ignore the key

const newGolfer = () =>
  createLocalAgent({
    provider: new OpenAIProvider({ ...LLM, model: 'your-model' }), // e.g. 'llama3.1', 'gpt-4o-mini'
    tools: [drive, iron, putt], // your code: each swing moves the ball and says where it landed
    maxTurns: 10,
  })

// 1. SEE one run: a tracer turns the agent's events into spans (run → turn → model call / tool).
const golfer = await newGolfer()
const exporter = createInMemoryExporter() // in production: an OpenTelemetry exporter instead
attachTracer(golfer, { exporter })
await golfer.run('Hole 3: par 4, 330 m to the pin. Water crosses the fairway at 200 m.')
for (const span of exporter.spans) {
  console.log(span.name, span.endTime! - span.startTime, 'ms', span.status) // e.g. "tool drive 320 ms error"
}

// 2. GRADE many runs: a dataset of cases, and scorers that decide pass or fail.
const dataset: EvalCase[] = [
  { id: 'hole-1', input: 'Hole 1: par 3, 150 m to the pin. Play it out and report your score.', expected: { par: 3, clubs: ['iron', 'putt'] } },
  { id: 'hole-2', input: 'Hole 2: par 4, 360 m to the pin. Play it out and report your score.', expected: { par: 4, clubs: ['drive', 'iron', 'putt'] } },
  {
    id: 'hole-3',
    input: 'Hole 3: par 4, 330 m to the pin. Water crosses the fairway at 200 m. Play it out and report your score.',
    expected: { par: 4, clubs: ['iron', 'iron', 'putt'] }, // lay up short of the water
  },
]
type Expected = { par: number; clubs: string[] }

// Each case expects its own clubs, so wrap the built-in trajectory scorer.
const path: Scorer = {
  name: 'trajectory',
  score: (result) => toolTrajectory((result.case.expected as Expected).clubs, { mode: 'ordered-subset' }).score(result),
}
// Your own scorer: any function from a run to a 0..1 score.
const par: Scorer = {
  name: 'par',
  score: (result) => {
    const strokes = Number(/in (\d+)/.exec(result.output)?.[1] ?? Infinity)
    const passed = strokes <= (result.case.expected as Expected).par
    return { scorer: 'par', score: passed ? 1 : 0, passed, details: `${strokes} strokes` }
  },
}

const report = await runEval({
  dataset,
  createAgent: () => newGolfer(), // a fresh agent per case: no shared history
  scorers: [contains('Holed out'), path, par], // add llmJudge({ provider, rubric }) for fuzzy answers
})

console.log(report.summary) // { total: 3, passed: 2, passRate: 0.67, byScorer: { … } }

O que observar

  • Rode as avaliações a cada mudança. Um prompt novo, uma descrição de ferramenta ou uma versão de modelo podem consertar um caso e quebrar dois. Mantenha o dataset no repositório, rode-o no CI e faça o build falhar quando a taxa de aprovação cair.
  • Faça o dataset crescer com falhas reais. Todo bug que um usuário relata vira um caso, com o trace como evidência. Um dataset só com casos fáceis passa para sempre e não prova nada.
  • As execuções variam. O mesmo caso pode passar hoje e falhar amanhã. Rode os casos importantes várias vezes e observe a taxa, não um único resultado.
  • Os traces guardam dados dos seus usuários. Prompts e entradas e saídas de ferramentas vão parar neles. Oculte o que precisar (um hook, nível 6), e trate o armazenamento de traces como um banco de dados com dados pessoais.
  • Custo também é métrica. Registre os tokens por span. Um agente que passa em todos os casos com o dobro de chamadas é uma regressão que a sua taxa de aprovação não vai mostrar.