Como descobrir qual agente errou quando a orquestração entrega um resultado errado

árvore de spans mostrando qual agente errou na orquestração
Resposta rápida

Depurar orquestração de agentes é diferente de montar a orquestração: aqui todo agente aparece como sucesso e mesmo assim o resultado final sai errado, porque a falha mora no espaço ENTRE os agentes, nas fronteiras de comunicação e handoff. O caminho é ler a árvore de spans das convenções GenAI do OpenTelemetry (invoke_agent na raiz, chat por chamada de LLM, execute_tool por ferramenta), propagar um único trace ID ponta a ponta, comparar o que entrou e o que saiu de cada etapa e reexecutar só a etapa suspeita a partir de um checkpoint, sem repetir a cadeia inteira

Fala aí, beleza? Tem poucas coisas mais irritantes que abrir a execução, ver TODO agente marcado como sucesso, e o resultado final estar errado do mesmo jeito

Ninguém falhou, mas a resposta tá torta

Este post não é sobre montar cadeia de agentes, é sobre depurar uma que já existe e já te entregou lixo. A ideia é ir do sintoma ("a saída final tá errada") até o ponto exato da falha: qual etapa, qual fronteira, qual dado se perdeu no meio do caminho

Se liga que quase todo sintoma aqui tem a mesma raiz: você tá olhando log de agente quando deveria estar olhando a passagem entre eles

Sintoma: todo agente aparece como sucesso, mas o resultado final está errado

A causa: falhas em sistemas multiagente costumam estar no espaço entre os agentes, e não dentro da lógica de um agente isolado, como o pessoal da Sentry descreve bem

Cada agente, olhado sozinho, executou direitinho

Recebeu uma entrada, produziu uma saída, fechou sem exceção. Por isso o log individual de ninguém registra erro: a falha está na comunicação e no handoff, não na execução

A solução: parar de perguntar "qual agente quebrou?" e começar a perguntar "o que saiu de um e o que entrou no outro?"

A unidade de investigação vira a FRONTEIRA, não o agente

Como prevenir: tratar handoff como interface, do mesmo jeito que você trataria uma chamada de API entre dois serviços: entrada registrada, saída registrada, e um jeito de comparar as duas depois

Formação Claude Code
Formação Recomendada

Formação Claude Code

Domine Claude Code do absoluto zero até o avançado

  • 120 aulas
  • 4 projetos
  • 9h 45min

Sintoma: você não consegue enxergar a cadeia inteira da orquestração

Você abre o painel e vê pedaços soltos: um trace do orquestrador aqui, outro do subagente ali, e nada que ligue os dois

A causa: o trace reinicia a cada fronteira de agente e a árvore fica picada

A solução: propagar um único trace ID em toda invocação de agente, sem reiniciar quando cruza a fronteira. Com um trace ID só, ponta a ponta, você enxerga quem chamou o quê, em que ordem, com quais entradas e quais saídas

E aí entra a parte que padroniza o desenho da árvore

Como as convenções GenAI do OpenTelemetry organizam a execução:

As convenções semânticas GenAI do OpenTelemetry organizam a execução em uma árvore de spans assim:

  • um span de nível superior invoke_agent, que é a raiz
  • spans filhos chat, um pra cada chamada de LLM
  • spans execute_tool, um pra cada uso de ferramenta

No span de execução de ferramenta o nome da ferramenta é obrigatório, e é ele que dá nome ao span execute_tool

Isso é MUITO prático na hora de depurar: você bate o olho na árvore e já lê qual ferramenta rodou dentro de qual agente, sem ficar adivinhando por timestamp

Como prevenir: instrumentar antes de precisar, não depois do incidente. Instrumentação feita no meio do fogo só cobre a execução seguinte, e a que quebrou já era

Nota pra não te perder na documentação: as convenções de IA generativa saíram do repositório principal de semantic conventions e passaram a viver em um repositório dedicado de convenções GenAI. A página antiga de spans de agente no site hoje aparece marcada como "Moved", então se você caiu nela por link velho, é só seguir a mudança de casa

Sintoma: a etapa parou no meio e você não sabe por quê

A saída de uma etapa veio curtinha, e a cadeia seguiu em frente com meia informação

A causa: sem atributos padronizados, "saída curta porque o modelo resolveu assim" e "saída truncada porque bateu o limite" parecem exatamente a mesma coisa no seu log

Duas causas completamente diferentes, o mesmo sintoma visual

A solução: ler os atributos padronizados que acompanham esses spans:

Atributo O que ele te conta
gen_ai.request.model qual modelo rodou naquela etapa
gen_ai.usage.input_tokens tokens de entrada consumidos
gen_ai.usage.output_tokens tokens de saída gerados
gen_ai.response.finish_reasons por que aquela etapa parou

Com esses quatro campos a conversa muda de "acho que o modelo alucinou" pra "essa etapa parou por esse motivo, consumindo essa quantidade de contexto"

Como prevenir: exportar esses atributos por padrão em TODA etapa da cadeia, não só nas que você desconfia. O ponto de exportar sempre é justamente não precisar reproduzir o bug pra ter dado

Sintoma: o erro só aparece depois de dois ou três agentes e você não sabe qual estragou o dado

Esse é o clássico: o agente 1 pesquisou certo, o agente 2 resumiu, o agente 3 escreveu, e a informação certa evaporou em algum lugar do meio

A causa: perda de contexto no handoff

Cada vez que um agente resume antes de passar adiante, a informação sofre compressão com perda. E o dilema é real: passar tudo estoura a janela de contexto, resumir perde nuance

Não existe saída elegante, existe escolha consciente

A solução: comparar, dentro do trace, o texto que ENTROU e o texto que SAIU de cada etapa

Você vai lendo passagem por passagem até achar a primeira em que o dado correto já não está mais lá. Essa é a etapa culpada, mesmo que ela tenha fechado com sucesso

Como prevenir: registrar o payload do handoff, não só o veredito do agente. "Agente de pesquisa: ok" não serve pra nada numa investigação, o que serve é o texto que ele efetivamente entregou pro próximo

Sintoma: você isolou a etapa suspeita mas teria que rodar tudo de novo pra testar

Você já sabe onde é o problema, beleza? Só que pra testar a correção você teria que reexecutar a cadeia inteira, do zero, queimando tempo e token em etapas que já estavam certas

A causa: sem persistência de estado, reexecutar significa repetir tudo

A solução: checkpoint

No LangGraph, o checkpointer salva o estado da thread a cada super-step e permite recuperar estados históricos, o que habilita replay, retomada após falha e time travel

O caminho é esse:

  1. use get_state_history pra localizar o checkpoint do ponto que te interessa
  2. chame invoke com a config daquele checkpoint pra reproduzir dali pra frente
  3. se quiser testar uma hipótese ("e se o dado que chegou aqui fosse esse outro?"), use update_state, que aplica os valores usando os writers do nó indicado, registra no checkpoint que aquele nó produziu a atualização, e a execução segue a partir dos sucessores desse nó

Tome cuidado com uma coisa aqui: no replay, os nós ANTERIORES ao checkpoint não são reexecutados, porque os resultados já estão salvos

Mas os nós POSTERIORES rodam de verdade

Isso inclui chamadas de LLM, requisições de API e interrupts, ou seja, não é leitura de cache e o resultado pode sair diferente do original. Já vi gente concluir que "consertou" quando na real só pegou uma amostragem diferente do mesmo prompt

Quem usa LangSmith tem um atalho pra investigação interativa: dá pra abrir um trace, selecionar a run raiz e clicar em "Run in Studio"

Como prevenir: ligar checkpoint desde o primeiro dia do fluxo. Checkpoint é daquelas coisas que só parecem exagero até o dia em que você precisa reexecutar uma etapa e não pode

Sintoma: você ligou o tracing e nada aparece no backend

Esse aqui dói, porque você faz tudo certo na investigação e o painel te devolve o vazio 😅

A causa e a solução mudam por ferramenta, então vou separar

No OpenAI Agents SDK (Python):

O tracing vem ligado por padrão, sem configuração extra, e registra gerações do LLM, chamadas de ferramenta, handoffs e guardrails

Ou seja: se não tá aparecendo, alguém DESLIGOU. E são três formas de desligar:

  1. a variável de ambiente OPENAI_AGENTS_DISABLE_TRACING=1
  2. set_tracing_disabled(True) no código
  3. RunConfig.tracing_disabled=True, que mata o tracing só de uma execução específica

Tem uma quarta possibilidade que é a pegadinha de verdade: set_trace_processors() SUBSTITUI os processadores padrão. Nesse caso os traces deixam de ir pra OpenAI, a menos que você inclua um processador que faça isso

Se a sua intenção era só mandar os traces também pra outro lugar, o certo é add_trace_processor(), que adiciona um processador extra recebendo traces e spans

Já que você tá mexendo aí, duas coisas que valem a pena:

  • custom_span() pra marcar trechos próprios, com parâmetros como name, data, span_id, parent e disabled. O span criado entra automaticamente no trace corrente, aninhado sob o span mais próximo (o controle é via contextvar do Python), então você não precisa costurar hierarquia na mão
  • RunConfig.trace_include_sensitive_data pra não capturar dado sensível nos traces

No Claude Code:

O Claude Code suporta métricas e eventos de OpenTelemetry, habilitados por variável de ambiente:

export CLAUDE_CODE_ENABLE_TELEMETRY=1

Daí você escolhe o exportador (otlp ou console) pra métricas e logs, e configura o endpoint OTLP, como a documentação de monitoramento mostra (o exemplo com gRPC aponta pra http://localhost:4317)

Pra saber se tá chegando, a documentação indica sinais bem concretos no backend:

  1. a métrica claude_code.session.count, emitida no início da sessão
  2. o evento claude_code.user_prompt, que valida setup só de logs
  3. se nada chegar mesmo assim, rode claude --debug e procure erros de exportação OTel no log

Armadilha que pega geral: o Claude Code NÃO repassa as variáveis OTEL_* pros subprocessos que ele cria

Ferramenta Bash, hooks, servidores MCP e language servers não herdam a configuração de OpenTelemetry do processo pai

Então se você jurava que tinha cobertura completa e o buraco do trace é justo onde roda um MCP, achou o motivo. E, saindo um pouco do assunto, nem toda tarefa merece esse aparato todo: tem casos em que fazer na mão sai mais rápido que orquestrar e depois depurar a orquestração

O que eu vi rastreando um fluxo multiagente na prática

No vídeo abaixo eu monto do zero um fluxo de orquestração no n8n: um agente principal fazendo o papel de orquestrador e 2 subagentes ligados como ferramentas, um de pesquisa na internet e outro de consulta a uma planilha de vídeos do YouTube

Antes eu fazia multiagente por subfluxo, chamando um workflow separado que executava o agente

Funcionava, mas pra DEPURAR era pior, porque as coisas ficavam separadas em execuções diferentes e você ficava pulando de tela em tela pra remontar a história. O subfluxo ainda tem valor quando o objetivo é reaproveitar o mesmo agente em outros fluxos, mas em alguns casos eu prefiro manter tudo em um fluxo só justamente por isso

A parte que interessa aqui é o rastro

Quando perguntei sobre a versão do n8n, eu acompanhei na tela o caminho completo da execução: orquestrador, agente de pesquisa, ferramenta de busca, e volta pro orquestrador montar a resposta final

A resposta dessa rodada saiu desatualizada

E olha que legal: eu consegui localizar de onde veio o dado errado só observando por qual subagente e por qual ferramenta a execução passou. Não precisei abrir o prompt de todo mundo, o caminho já entregava a fronteira suspeita

Depois repeti o teste com uma pergunta sobre vídeos publicados: caiu no agente certo, acessou a planilha e devolveu os 3 vídeos que eu tinha pedido

Um detalhe importante do meu primeiro teste: mandei um cumprimento simples e o orquestrador não acionou nenhum subagente, respondeu direto. Eu li isso como sinal de que o roteamento estava se comportando

Mas foi sorte, e eu falo isso no vídeo com todas as letras

Conforme o fluxo cresce e ganha mais agentes, o orquestrador tende a se perder e chamar o agente errado. Por isso eu voltei no orquestrador e escrevi uma system message declarando o papel dele e listando os agentes disponíveis, com uma descrição de quando cada um deve ser usado

A descrição de cada subagente serve exatamente pra isso: pro orquestrador e o subagente se entenderem sobre quando aquele agente deve ser chamado

Depois da system message refiz os testes: pergunta de atualidade caiu no agente de pesquisa, pergunta de vídeos caiu no agente da planilha, e conversa solta não acionou agente nenhum

Outra coisa que eu achei massa: quando o orquestrador fica em dúvida, ele não entra em um agente qualquer, ele devolve uma pergunta pro usuário. Isso pra mim é sinal de que o prompt precisa melhorar, não de que o fluxo tá quebrado

E, no fim, eu validei o resultado de uma das respostas contra o que eu mesmo uso nos meus fluxos de produção, em vez de aceitar a saída do agente como verdade

Esse é o hábito que mais evita dor de cabeça: agente respondeu, ótimo, agora confere

(se a sua dúvida for onde rodar isso, eu já comparei n8n na nuvem contra self-hosted em outro post)

Próximo passo: instrumentar antes do próximo incidente

Recapitulando a ordem de ataque quando a orquestração entrega resultado errado e ninguém falhou:

  1. Trace único. Um trace ID propagado ponta a ponta, sem reiniciar na fronteira, pra você ver a árvore inteira (invoke_agent na raiz, chat por chamada de LLM, execute_tool por ferramenta)
  2. Atributos por etapa. gen_ai.request.model, gen_ai.usage.input_tokens, gen_ai.usage.output_tokens e gen_ai.response.finish_reasons pra saber o que cada etapa consumiu e por que parou
  3. Fronteira, não agente. Comparar o que entrou e o que saiu de cada passagem, porque a informação some no handoff, não dentro do agente
  4. Checkpoint pra replay. Estado salvo pra reexecutar SÓ a etapa suspeita, lembrando que o que vem depois do checkpoint roda de verdade e pode dar resultado diferente

E o próximo passo concreto é bem chato de tão simples: liga o tracing e o checkpoint no fluxo que você já tem rodando HOJE, provoca uma execução qualquer e reexecuta uma etapa isolada de propósito

Treinar o método com o fluxo funcionando é MUITO mais tranquilo do que descobrir os comandos com o cliente esperando resposta

Quando o erro real chegar, você já vai saber onde clicar 🙂

até o próximo post!

Perguntas frequentes

Qual é a diferença entre o span invoke_agent e os spans chat nas convenções GenAI do OpenTelemetry?

invoke_agent é o span de nível superior, a raiz da árvore daquela execução de agente. Dentro dele ficam os spans chat, um pra cada chamada de LLM, e os spans execute_tool, um pra cada uso de ferramenta. É essa hierarquia que deixa claro, na hora de depurar, o que aconteceu dentro de qual agente.

Dá pra desativar o tracing do OpenAI Agents SDK quando eu não quiser rastrear uma execução?

Dá, e de três formas: variável de ambiente OPENAI_AGENTS_DISABLE_TRACING=1, set_tracing_disabled(True) direto no código, ou RunConfig.tracing_disabled=True pra desligar só numa execução específica. No SDK o tracing já vem ligado por padrão, então essas três opções são justamente pra quando você precisa desligar.

O Claude Code repassa a configuração de OpenTelemetry pra subprocessos como o Bash e servidores MCP?

Não. O Claude Code não repassa as variáveis OTEL_* pros subprocessos que ele cria, então ferramenta Bash, hooks, servidores MCP e language servers não herdam essa configuração do processo pai. Se você depende de rastrear o que roda dentro dessas ferramentas, isso precisa ser instrumentado à parte.

Como eu confirmo que a telemetria do Claude Code está realmente chegando no meu backend?

A documentação indica dois sinais concretos: a métrica claude_code.session.count, emitida no início da sessão, e o evento claude_code.user_prompt, que serve pra validar o setup só com logs. Se nenhum dos dois aparecer, o próximo passo é rodar claude –debug e procurar erro de exportação OTel no log.

Dá pra editar o estado de um agente no LangGraph antes de retomar a execução a partir de um checkpoint?

Dá, com update_state. Ele aplica os valores usando os writers do nó indicado, registra no checkpoint que aquele nó produziu a atualização, e a execução segue a partir dos sucessores desse nó. É o jeito de corrigir um dado errado sem reexecutar tudo do zero.

Por que reexecutar a partir de um checkpoint no LangGraph pode dar um resultado diferente da execução original?

Porque só os nós anteriores ao checkpoint não são reexecutados, já que o resultado deles já está salvo. Os nós posteriores rodam de novo de verdade, incluindo chamada de LLM, requisição de API e interrupt, e nada garante que a resposta do modelo ou da API externa vai ser idêntica à da primeira vez.




Escrito por | Matheus Battisti

Matheus Battisti
Fundador da Hora de Codar

Programador apaixonado pelo mundo das tecnologias, sempre buscando em aprender e se aprofundar em linguagens, frameworks e o que mais for necessário para executar um bom trabalho. Agora tem uma nova missão que é de passar seu conhecimento adiante para formar novos programadores e especializar mais os que já são.

Subscribe
Notify of
guest

0 Comentários
Oldest
Newest Most Voted

Formações

Formação Vibe Coding

Formação Vibe Coding

Do Prompt ao Produto: Crie Software Real com IA

  • 474 aulas
  • 20 projetos
  • 39h 27min

Blog | Mais populares