Engenharia

Observability para agents é outro esporte

Logs te dizem o que o código fez. Eles não conseguem te dizer por que um agent decidiu fazê-lo, e para um sistema não-determinístico, o porquê é a única unidade de debugging que importa.

ASR

Apollo Space Research

Apollo Space

· 11 min de leitura

Um usuário digita uma frase para um agent, o agent faz a coisa errada, e você abre os logs para descobrir por quê. Os logs estão impecáveis. A função foi chamada com os argumentos com que foi chamada, retornou o que retornou, a request foi um 200 limpo. Cada linha é verdadeira e nenhuma delas te diz a coisa de que você precisa: por que o agent decidiu fazer aquilo, em vez da coisa certa?

Essa lacuna não é uma linha de log faltando. É outro esporte.

Logs respondem “o que o código fez.” Debugar um agent precisa de “por que ele decidiu aquilo”, e para um sistema não-determinístico, o decision trace é a única unidade que importa.

Este post é sobre por que os dashboards que rodaram software por quarenta anos ficam mudos exatamente quando um agent dá errado, e o que você tem que registrar em vez disso.

A versão ingênua: trate o agent como um serviço

O movimento óbvio, quando você coloca um agent atrás de uma API, é observá-lo do jeito que você observa todo outro serviço. Você já tem a stack. Request entra, span sai, latência num gráfico, error rate com um alerta nele, um stack trace quando algo lança. É um problema resolvido. Você conecta o agent ao mesmo pipeline e considera coberto.

Funciona até o agent estar errado sem estar quebrado.

Aqui está o caso que quebra o modelo. Um usuário pede ao agent para fazer uma coisa. O agent faz uma coisa diferente, plausível, em vez disso, confiantemente, com sucesso, sem nenhuma exception em lugar nenhum. O usuário pediu para ele buscar algo; ele foi e escreveu algo. A request teve sucesso. A latência estava boa. O error rate não se moveu. Por todo sinal que sua observability de nível de serviço coleta, aquela request foi uma boa request.

Então você abre o trace, e o que está nele é o formato do código: qual função rodou, o que retornou, quanto tempo levou. O que não está nele é o formato da decisão: que o agent leu a frase, classificou a intenção como um write quando era um read, planejou uma sequência em torno daquela classificação errada, e a executou perfeitamente. O bug não está em nenhuma função. O bug está numa escolha. E seus logs não registram escolhas, porque por quarenta anos o código não fazia nenhuma, ele as seguia.

O gargalo nunca desaparece. Ele só se move, de “o código rodou” para “por que o agent escolheu.”

Essa é toda a diferença, e ela não é pequena.

Por que “o quê” deixa de ser suficiente

Dê um passo atrás e olhe para o que fez os logs tradicionais funcionarem por tanto tempo. A razão pela qual um stack trace é útil é que código comum é determinístico. Mesmo input, mesmo caminho, toda vez. Então quando algo dá errado, “o que aconteceu” é “por que aconteceu”, o caminho que o código tomou é totalmente explicado pelos inputs e pelos branches. Você lê o trace, encontra a linha, encontrou a causa. O quê e o porquê são a mesma palavra.

Um agent rompe isso. Mesmo input não significa mesmo caminho. O modelo pode ler uma frase de dois jeitos em duas execuções. Ele pode escolher a tool A hoje e a tool B amanhã na request idêntica. O caminho não é mais explicado só pelo input, é explicado por um julgamento que o modelo fez dado o input. Então “o que aconteceu” e “por que aconteceu” se separam, e todo o valor de um log, que o quê e o porquê são a mesma coisa, se separa com eles.

Uma vez que você vê isso, a unidade certa de observação muda. Para código determinístico, a unidade é a chamada: esta função, estes argumentos, este retorno. Para um agent, a chamada é a coisa menos interessante da sala. A unidade de que você realmente precisa é a decisão, e uma única mensagem de usuário produz uma cadeia curta delas.

Achamos útil nomear os quatro elos que recorrem em quase toda turn:

  • Classify, o que o agent decidiu que o usuário realmente queria? É aqui que a confusão read-vs-write vive, onde “agendar uma call” é lido como “rascunhar um email,” onde a turn inteira é ganha ou perdida antes de qualquer tool rodar.
  • Plan, dada aquela leitura, que sequência ele decidiu fazer? Um passo ou cinco, em que ordem, com quais suposições.
  • Tools, quais capacidades ele de fato alcançou, com quais argumentos, e o que voltou? Não só “a API retornou 200,” mas por que esta tool e não aquela.
  • Reply, o que ele disse ao usuário, e aquela resposta estava grounded no que as tools de fato retornaram, ou improvisada sobre uma lacuna?

Um log captura o terceiro, tools, e só a metade mecânica dele. O bug geralmente vive no primeiro. Você estava olhando o único elo que estava funcionando.

Uma turn de agent não-determinística é uma cadeia de quatro decisões, classify da intenção, plan dos passos, escolher e chamar tools, compor a reply, e um log tradicional só captura a chamada de tool. O elo classify, onde a maioria dos bugs de agent de fato nasce, é invisível para ele.

O decision trace: registre o porquê, não só o quê

Então a correção não é um formato de log melhor. É uma coisa diferente para registrar.

Um decision trace captura, para uma turn de usuário, a cadeia de escolhas que o agent fez e a razão anexada a cada elo. Não “tool X rodou.” E sim: o agent classificou esta mensagem como este tipo de intenção, que é por que ele planejou estes passos, que é por que ele chamou esta tool com estes argumentos, e compôs esta reply a partir daquele resultado. O trace é um registro de julgamento, não só de execução, cada elo carrega a escolha e o suficiente do porquê para discutir com ela.

A diferença aparece no instante em que algo dá errado. Imagine o bug do read-classificado-erroneamente-como-write de novo, o que o 200 limpo escondeu.

Com um log, você vê uma write tool disparar e ter sucesso. Beco sem saída, o write funcionou, então onde está o bug? Você começa a adivinhar. Talvez seja a tool. Talvez sejam os dados. Talvez o usuário tenha frasiado de forma estranha. Você está reconstruindo o raciocínio do agent de fora, como ler um veredicto sem julgamento.

Com um decision trace, você abre a turn e o primeiro elo te diz tudo: o passo classify rotulou um read como um write. O plan estava correto para aquele rótulo errado. A tool rodou corretamente para aquele plan errado. A reply foi honesta sobre aquela tool errada. A cadeia inteira foi internamente impecável e enraizada numa única chamada ruim no elo um, e você consegue ver, porque o trace registrou a chamada, não só suas consequências. Você não adivinha. Você lê o veredicto e o julgamento.

Esse é o movimento: pare de reconstruir o raciocínio do agent depois do fato, e registre-o enquanto acontece. A razão pela qual “por que ele decidiu aquilo” parecia sem resposta é que a resposta nunca foi escrita. A decisão existiu e então evaporou, e você ficou segurando o escapamento dela, a chamada de tool, e foi pedido para inferir o motor.

Um log é o escapamento do agent. Um decision trace é o raciocínio dele, escrito antes de evaporar.

E uma vez que o raciocínio está escrito, duas coisas que você não conseguia fazer antes viram rotina. Você pode reexecutar a mesma mensagem em dois modelos diferentes e colocar as cadeias de decisão deles lado a lado, mesmo input, julgamento diferente, e agora você pode ver exatamente qual elo divergiu. E você pode dar nota a uma turn não por “deu 200,” mas por “cada decisão na cadeia fez sentido dada a anterior.” O trace deixa de ser uma ferramenta forense que você busca depois de um incêndio. Ele vira a coisa que você assiste para saber se o agent está raciocinando bem de jeito nenhum.

Duas sessões de debugging para o mesmo bug de ação errada. Com um log, você vê só que uma write tool retornou 200, um beco sem saída que te manda adivinhar a tool, os dados, o fraseado. Com um decision trace, o elo classify mostra que um read foi rotulado como write, e o resto da cadeia está correto para aquela única chamada ruim: a causa é visível, não inferida.

Por que os alertas também têm que mudar

Há uma segunda metade nisso, e é a que os times descobrem do jeito difícil depois de já terem adicionado decision traces: o alerting também foi construído para o esporte antigo.

Alertas de serviço disparam nas falhas que o código pode ter, uma exception, um timeout, um 500, um spike de latência. Essas são reais e você ainda as quer. Mas a falha característica de agent não lança nenhuma delas. O agent que confiantemente fez a coisa errada com aparência de certa produziu um 200 e uma resposta rápida. Se seu pager só sabe como te acordar para exceptions, ele vai dormir durante todo bug de agent interessante que existe. As piores falhas de um sistema não-determinístico são as que parecem, para um monitor de serviço, sucesso completo.

O instinto ingênio é alertar mais forte nos mesmos sinais, apertar o threshold de latência, observar o error rate mais de perto. Isso não pega nada de novo, porque o bug nunca esteve naqueles sinais.

O que você de fato precisa é alertar no formato da cadeia de decisão. Uma turn onde o agent classificou uma intenção que não tem tool para atender. Uma reply que reivindica uma ação que o log de tools mostra que nunca aconteceu, o agent dizendo “feito” sobre um passo que não rodou. Um plan que ficou em loop sem convergir. Uma cadeia classify-to-reply onde a resposta final não decorre de nada que as tools retornaram. Nenhuma dessas é uma exception. Cada uma delas é uma turn que deu errado. Você só consegue alertar nelas porque registrou as decisões, o mesmo trace que te deixa debugar depois do fato é a coisa que te deixa notar antes do usuário.

O dashboard nunca foi o problema. Ele estava respondendo a pergunta para a qual foi construído, o que o código fez, impecavelmente, sobre um sistema cujas falhas não vivem mais ali.

A virada: você está debugando um colega agora

Aqui está a parte que não é sobre telemetria.

Quando a coisa dentro do seu sistema deixou de ser código que segue instruções e virou algo que faz julgamentos, as perguntas que você faz a ela mudaram, e essa é uma mudança mais humana do que soa. Você não debuga mais uma máquina encontrando a linha quebrada. Você debuga um tomador de decisão entendendo por que ele decidiu o que decidiu. Isso não é uma pergunta de software. É a pergunta que você faz a um colega novo que fez algo surpreendente: não “o que você fez,” que você já consegue ver, mas “me explique seu raciocínio”, e então você encontra a única suposição que mandou o resto pro caminho errado.

Um decision trace é como você pede a um agent para te explicar o raciocínio dele. É a diferença entre gerenciar um colega cujo raciocínio você consegue inspecionar e confiar numa caixa-preta porque a última resposta dela por acaso pareceu certa. Uma dessas escala para rodar trabalho real. A outra é um demo que ainda não falhou na frente de um cliente.

Os agents vão continuar ficando mais capazes. Os modelos sob eles vão continuar ficando mais inteligentes. Nada disso remove a necessidade de ver por que eles decidiram o que decidiram, ela aumenta, porque um agent mais capaz faz escolhas maiores, mais rápidas, mais consequentes, e uma escolha que você não consegue inspecionar é uma escolha em que você não consegue confiar. Logs respondem o que o código fez. Debugar um agent precisa de por que ele decidiu aquilo, e essa pergunta só tem resposta se você escreveu a decisão.


Isso é parte do que estamos construindo na Apollo Space, um sistema operacional onde os agents fazem trabalho real de empresa, e onde toda turn que eles tomam deixa um decision trace que você pode abrir, ler e discutir. Se você já encarou um log perfeitamente limpo ao lado de um resultado perfeitamente errado, você já sabe que “o que aconteceu” nunca foi a pergunta. A pergunta sempre foi por quê.

A Apollo cuida da operação repetitiva da sua empresa pro seu time não precisar.

Entre na lista de espera: acesso antecipado, preço de usuário fundador e um lugar na primeira fila enquanto a gente constrói.

Entrar na lista de espera