O que aprendi construindo um pipeline com Google ADK, MCP e modelos locais
Sistemas multiagentes costumam ser apresentados de uma maneira muito atraente. Um agente pesquisa, outro escreve, um terceiro revisa e o resultado parece pronto para produção.
O problema aparece quando o resultado está quase certo.
Um título passa do limite permitido. Uma URL parece real, mas não existe. Um agente de revisão recebe o caminho errado. Um validador deveria interromper o fluxo, mas não consegue enxergar o estado produzido na etapa anterior.
Foi justamente para explorar esse tipo de problema que organizei o repositório adk-multiagent. Ele reúne oito agentes em Python usando o Google Agent Development Kit, conhecido como ADK, além de um servidor local MCP, testes offline, integração com RAG, revisão de código, verificação de links e uma cadeia de modelos hospedados e locais.
A ideia central do projeto é simples:
O modelo deve interpretar, decidir e explicar. O código deve medir, limitar, buscar e rejeitar.
Essa separação torna o comportamento dos agentes mais previsível e também mais fácil de testar.
O projeto não é apenas uma coleção de agentes
Cada agente fica em sua própria pasta e expõe um root_agent em agent.py. Existe também um common.py, responsável por montar a cadeia de modelos usada pelos agentes.
A estrutura prática é parecida com esta:
blogger planejamento, escrita e validação
linkcheck verificação de URLs e fontes
seo metadados para publicação
rag documentos indexados e respostas com citações
codereview leitura segura de arquivos e heurísticas
triage classificação e roteamento por confiança
mcp_text_audit agente que consome um servidor MCP local
researcher exemplo mínimo de um agente
common.py cadeia de modelos e fallback
O ADK trata agentes como unidades de execução com modelo, instruções e ferramentas. Aplicações maiores podem combinar essas unidades em hierarquias ou workflows. A documentação oficial explica os conceitos em ADK Agents e ADK Workflows.
No projeto, a divisão de pastas também representa uma divisão de responsabilidades. Cada fronteira existe para controlar um tipo diferente de falha.
| Problema | Solução usada no projeto |
|---|---|
| O modelo erra uma contagem de caracteres | Validação em Python |
| Uma URL parece plausível, mas é falsa | HTTP e busca de fontes |
| O agente tenta ler um arquivo indevido | Caminho limitado ao diretório do projeto |
| Um segredo passa despercebido | Heurísticas executadas antes do modelo |
| O validador não enxerga o resultado anterior | Interpolação explícita do estado |
| Um servidor MCP apresenta comportamento difícil de reproduzir | Servidor local e testes offline |
Por isso, o projeto é mais interessante como estudo de fronteiras de controle do que como demonstração de vários agentes conversando entre si.
O fluxo em uma imagem
O repositório já inclui dois diagramas do fluxo principal, um para temas claros e outro para temas escuros. Eles mostram a sequência entre o agente Blogger, o planejador, o escritor e os validadores.
O diagrama ajuda a perceber que o sistema não é uma conversa livre entre agentes. Existe uma sequência definida, existem artefatos compartilhados e existem pontos claros em que o processo pode ser aprovado ou repetido.
O pipeline de blog mostra onde o estado realmente importa
O agente blogger é o exemplo mais completo do repositório. Ele recebe um tema, cria um outline, escreve o artigo e passa o resultado por validadores.
O fluxo usa dois blocos principais. Um planeja o outline e outro escreve o texto. Cada bloco tem um gerador e um validador. O validador pode responder ok ou retry. Quando o resultado está adequado, um callback encerra o loop. Quando existe um problema, o gerador recebe o feedback e tenta novamente.
Os artefatos intermediários ficam registrados no estado:
blog_outline outline produzido pelo planejador
outline_validation resultado da validação do outline
blog_post artigo produzido pelo escritor
post_validation resultado da validação do artigo
Aqui existe uma diferença importante entre armazenar um valor e torná-lo visível para o modelo.
Não basta escrever no prompt algo como “verifique o conteúdo de blog_outline”. O valor precisa ser interpolado na instrução. O projeto usa expressões como {blog_outline?} e {post_validation?} para isso.
O ponto de interrogação também é importante. Na primeira execução, a chave ainda pode não existir. A forma opcional evita que o pipeline falhe antes mesmo de produzir o primeiro resultado.
Esse detalhe parece pequeno, mas é uma das diferenças entre um agente que apenas possui memória de execução e um agente que realmente consegue trabalhar com os artefatos produzidos por outros agentes.
A validação também não fica restrita a um comentário no final. Ela participa do controle do fluxo. Um resultado aprovado encerra o loop. Um resultado rejeitado gera uma nova tentativa.
O projeto ainda usa LoopAgent nessa parte porque, segundo a documentação do próprio repositório, o novo Workflow não pode ser usado como subagente de um LlmAgent nesse arranjo específico. A documentação atual do ADK recomenda workflows para novas orquestrações determinísticas quando eles atenderem ao caso de uso. Portanto, este exemplo deve ser entendido como uma solução compatível com a arquitetura atual do projeto, não como uma regra para todo sistema novo.
O que deve sair do prompt e virar código
Uma das decisões mais importantes do projeto foi retirar algumas tarefas simples do modelo.
Metadados para SEO
O agente de SEO produz título, descrição, slug, tags e texto alternativo. Os limites são verificados em Python, incluindo quantidade de caracteres, normalização do slug e número máximo de tags.
O modelo continua sendo útil para escolher palavras e adaptar o texto. Mas não é razoável depender dele para provar que uma string tem menos de cem caracteres.
Uma instrução pode pedir “mantenha o título abaixo de cem caracteres”. Uma função pode verificar essa condição com precisão.
Essa é uma boa regra para qualquer sistema com LLM: deixe o modelo sugerir o valor e deixe o código validar o contrato.
Revisão de código
O agente de revisão lê o arquivo real antes de opinar. A ferramenta de leitura aceita apenas arquivos dentro do diretório definido para o projeto. O modelo não recebe liberdade para escolher qualquer caminho do sistema.
Antes de chamar o modelo, o código também procura padrões associados a chaves de API, strings de conexão com credenciais e blocos except: genéricos.
O modelo pode explicar a gravidade do problema e organizar os achados. Ele não deveria ser a única camada responsável por descobrir um segredo escrito literalmente em um arquivo.
Verificação de links
O agente linkcheck separa duas perguntas diferentes:
- A URL responde por HTTP?
- Existe uma fonte sobre o assunto que a URL deveria representar?
A primeira verificação encontra domínios inválidos, páginas removidas e erros de rede. A segunda tenta encontrar URLs que parecem convincentes, mas não correspondem ao assunto informado.
Isso também mostra por que “a citação está correta” não é um teste único. É necessário verificar existência, identidade, assunto e, dependendo do caso, se a fonte realmente sustenta a afirmação.
MCP é uma fronteira de integração
O exemplo mcp_text_audit conecta um agente ADK a um servidor MCP local por standard input e standard output.
O servidor expõe três ferramentas determinísticas:
- Contar caracteres, palavras, linhas e URLs.
- Encontrar placeholders não preenchidos.
- Identificar padrões que podem indicar segredos gravados no código.
São tarefas adequadas para uma ferramenta porque um modelo pode responder com muita confiança e ainda assim errar uma contagem ou ignorar um placeholder.
O projeto também documenta duas armadilhas práticas.
A primeira é que mensagens de diagnóstico em stdout podem corromper o fluxo JSON RPC. Logs devem ir para stderr.
A segunda é que a criação dos parâmetros de conexão não significa necessariamente que a conexão já foi aberta. Alguns erros só aparecem quando a sessão é iniciada. Por isso, o tratamento de exceções precisa envolver a operação que realmente abre a conexão.
Esses pontos não são exclusivos do ADK. Um servidor MCP só é confiável quando seu processo, transporte, timeout, schema e tratamento de erros também são observáveis e testados. A especificação oficial pode ser consultada no Model Context Protocol.
Uma execução real do agente MCP
Para testar o servidor local, foi enviada a seguinte entrada:
Audite o texto abaixo usando as ferramentas disponíveis:
Título: Guia de deploy {ENVIRONMENT}
API_KEY=demo_key_123456
URL: https://example.com/documentacao
Este texto possui um placeholder não preenchido e uma credencial de demonstração.
O trace registrou a inicialização da sessão MCP, a listagem das ferramentas e três chamadas de ferramenta: achar_placeholders, achar_segredos e contar_texto. A execução completa levou aproximadamente 18,43 segundos na interface observada.
O resultado confirmou o placeholder {ENVIRONMENT}, contou 180 caracteres, 24 palavras, 4 linhas e uma URL. A ferramenta de segredos retornou limpo: true para esse texto de demonstração.
Esse último resultado merece uma observação. O texto continha a string API_KEY=demo_key_123456, mas a heurística não a classificou como segredo. Isso não significa que o detector esteja errado. A ferramenta pode ter sido desenhada para reconhecer padrões específicos, ou pode considerar valores explícitos de demonstração como não sensíveis. O que a execução comprova é mais limitado: esse formato específico não foi marcado pela regra atual.
Essa é uma boa ilustração da diferença entre ter uma ferramenta e ter cobertura completa. O MCP retirou do modelo a tarefa de contar e procurar padrões, mas as regras determinísticas ainda precisam ser ampliadas e testadas contra uma coleção de exemplos positivos e negativos.
O trace também mostra uma segunda chamada ao modelo depois das ferramentas. O servidor executa as operações determinísticas, o agente recebe os resultados e então produz uma resposta legível para o usuário. O fluxo observado é:
inicialização MCP → descoberta das ferramentas → chamadas determinísticas → resposta do agente
Triage com classificação e confiança
O próximo experimento foi o agente triage, responsável por classificar mensagens de suporte e escolher uma fila.
A entrada usada foi:
O cliente não consegue acessar o painel desde esta manhã.
A autenticação retorna erro 401 mesmo depois da redefinição da senha.
O problema afeta toda a equipe.
O trace mostrou uma latência total aproximada de 8,09 segundos. Também apareceram duas chamadas ao modelo, uma chamada à função classificar_ticket e uma geração final de resposta.
O resultado observado foi:
Route: auto:technical
Department: technical
Confidence: 1.00
Urgency: yes
Frustration: low
Runner-up category: billing
Runner-up confidence: 0.00
O chamado foi encaminhado para a área técnica porque descrevia um erro de autenticação 401 que afetava toda a equipe. A urgência foi marcada como verdadeira e a confiança retornada foi 1,00.
Esse resultado mostra o comportamento do agente nessa execução, mas não prova que a confiança esteja calibrada. Para afirmar calibração seria necessário usar um conjunto de tickets rotulados, comparar as previsões com os resultados reais e medir se uma confiança de 0,80, por exemplo, corresponde aproximadamente a 80% de acertos.
Também é importante distinguir o que o print comprova sobre o JEV. As imagens mostram o agente, a função de classificação, as chamadas ao modelo e o resultado final. Elas não mostram explicitamente o nome do provedor ou do modelo usado em cada chamada. Para documentar o JEV com precisão, será necessário capturar a configuração ativa ou o log que identifica o modelo.
Mesmo assim, o experimento já demonstra uma propriedade importante do pipeline: a classificação não é apresentada apenas como uma categoria. Ela inclui departamento, urgência, frustração, confiança e uma alternativa de roteamento.
Onde o JEV entra na arquitetura
A execução do triage usa o OpenRouter como provider para acessar o JEV. Isso é diferente de usar um modelo Gemini diretamente pelo caminho suportado pelo ADK.
O OpenRouter funciona aqui como uma camada de acesso ao modelo. O agente continua sendo construído e executado pelo ADK, mas a chamada de inferência passa por um provider externo compatível com a interface utilizada pelo projeto.
Na tela do OpenRouter, o modelo aparece como Jev 1.13, associado à TypeSafe. A descrição também indica que o modelo retorna decisões estruturadas e foi projetado para roteamento e classificação, em vez de texto livre.
Essa arquitetura explica uma diferença importante do laboratório. O ADK oferece o runtime do agente, o estado, as ferramentas e a orquestração. O OpenRouter fornece o acesso ao modelo selecionado. O JEV, por sua vez, produz a decisão estruturada usada pelo agente de triage.
Não devemos descrever isso como suporte nativo do ADK ao JEV. A formulação mais precisa é:
O projeto integra o JEV ao ADK por meio do OpenRouter, usando uma camada externa de acesso ao modelo.
Essa distinção também é relevante para avaliação. O eval do ADK foi projetado para o caminho de modelos Gemini usado diretamente no ambiente ADK. A execução com JEV via OpenRouter pode funcionar no runtime, mas não é automaticamente coberta pelo mesmo mecanismo de avaliação.
Antes de publicar a captura do OpenRouter, é necessário ocultar o endereço de e-mail visível no canto superior esquerdo. O print também contém informações de uso e custo da conta. Podemos manter apenas o nome do modelo, a associação com a TypeSafe e os detalhes técnicos necessários para explicar a integração.
RAG começa com uma decisão de indexação
O agente de RAG indexa documentos no ChromaDB e responde com citações. O projeto separa a criação do índice da consulta. Isso evita gerar embeddings novamente a cada pergunta.
Também existem regras diferentes para Markdown e código. Documentos Markdown são divididos por cabeçalhos. Código é dividido por blocos de linhas que preservam melhor as funções.
Essa separação revela dois custos diferentes:
- O custo de criar e atualizar o índice.
- O custo de recuperar contexto para cada pergunta.
O README do projeto relata o uso de gemini-embedding-001 e observa que a cota de embeddings pode ser consumida antes da cota de geração de texto. Essa observação é útil para entender o ambiente utilizado no projeto, mas não deve ser tratada como uma garantia de custo ou desempenho para qualquer aplicação.
O mesmo cuidado vale para os números registrados sobre modelos locais, latência e qualidade. Eles dependem do hardware, da quantização, do prompt, da versão do modelo e da data da medição.
Uma execução observada no ADK Web
Depois de executar o agente pela linha de comando, a interface Web do ADK permitiu observar o mesmo fluxo por dentro.
A aba de eventos mostra o agente Blogger chamando o planejador e depois o escritor. Também aparecem os artefatos registrados no estado, como blog_outline, outline_validation, blog_post e post_validation.
Esse detalhe é mais importante do que o artigo final isoladamente. A interface mostra que o pipeline possui etapas intermediárias observáveis. O resultado não surge de uma única chamada invisível. Ele passa por planejamento, escrita e validação.
A aba de traces decompõe a execução em chamadas de modelo, agentes e validadores. Nesta execução específica, a latência total exibida foi de aproximadamente 54,81 segundos. O planejador aparece com cerca de 14,49 segundos e o escritor com cerca de 20,50 segundos. Os validadores também aparecem como etapas separadas.
Esses números não são um benchmark. Eles representam uma única execução em um ambiente específico. Ainda assim, são úteis para mostrar como a interface ajuda a localizar onde o tempo foi consumido.
Durante a mesma análise, a interface apresentou um aviso de performance informando que as instruções do sistema haviam sido modificadas entre turnos. Isso causou um context cache miss e aumentou a latência.
Esse aviso não indica uma falha do agente. Ele mostra que o custo de uma execução não depende apenas do modelo. A forma como as instruções são construídas e reutilizadas também pode afetar o desempenho.
Essa é uma das razões pelas quais traces são importantes em sistemas agentic. Sem eles, uma execução lenta pode parecer apenas um modelo lento. Com eles, é possível investigar chamadas individuais, retries, mudanças de instrução e etapas de validação.
O output completo de uma execução
O comando utilizado foi:
adk run blogger "Escreva um artigo técnico sobre structured concurrency em Python"
O output completo foi o seguinte:
Log setup complete: /tmp/agents_log/agent.20260929_090719.log
To access latest log: tail -F /tmp/agents_log/agent.latest.log
[ADK] Aviso experimental: FALLBACK_MODEL habilitado.
[ADK] Aviso experimental: InMemoryCredentialService habilitado.
Session ID: [omitido nesta publicação]
[ADK] Aviso experimental: JSON_SCHEMA_FOR_FUNC_DECL habilitado.
[Blogger]: Aqui está o artigo técnico solicitado sobre Concorrência Estruturada em Python.
# Concorrência Estruturada em Python: O Fim do Caos em Código Assíncrono
Se você escreve código assíncrono em Python há algum tempo, provavelmente já se deparou com tarefas que esqueceram de terminar, exceções silenciosas que derrubaram seu serviço ou vazamentos de memória causados por corrotinas que continuaram rodando em background muito depois de sua função principal ter retornado.
O culpado por esses problemas costuma ser a concorrência não estruturada.
## O Problema dos Fire and Forget
Em sistemas assíncronos tradicionais, é muito fácil disparar uma tarefa e esquecê-la:
async def processar_dados():
asyncio.create_task(tarefa_background())
return "Sucesso"
O problema aqui é o ciclo de vida. Se a função processar_dados falhar, a tarefa background pode continuar rodando. Se ela levantar uma exceção, essa exceção pode não ser tratada pelo bloco principal.
## O Que é Concorrência Estruturada?
A concorrência estruturada propõe que tarefas assíncronas sigam regras de escopo semelhantes às estruturas tradicionais de controle.
Uma tarefa deve ter início, meio e fim definidos dentro de um escopo. Quando o escopo termina, as tarefas relacionadas devem terminar ou ser canceladas de forma previsível.
## A Evolução: Trio e asyncio.TaskGroup
A ideia foi popularizada pela biblioteca Trio, com o conceito de Nursery. O Python 3.11 introduziu o asyncio.TaskGroup na biblioteca padrão.
Antes:
await asyncio.gather(tarefa_1(), tarefa_2())
Com Python 3.11 ou superior:
async def main():
async with asyncio.TaskGroup() as tg:
t1 = tg.create_task(tarefa_1())
t2 = tg.create_task(tarefa_2())
print("Todas finalizadas com sucesso.")
## Benefícios principais
1. Gestão de erros mais determinística.
2. Maior previsibilidade sobre o ciclo de vida das tarefas.
3. Melhor rastreabilidade durante a investigação de falhas.
## Limitações e cuidados
O uso de TaskGroup não elimina a necessidade de tratar cancelamentos. As tarefas devem executar a limpeza necessária em blocos finally e respeitar asyncio.CancelledError.
Também é importante revisar afirmações sobre asyncio.gather. Quando uma tarefa falha, gather pode propagar a primeira exceção sem cancelar automaticamente as outras tarefas. Elas podem continuar executando, dependendo do contexto.
## Títulos alternativos
1. Concorrência Estruturada: Como o Python 3.11 tornou seu código assíncrono mais seguro.
2. Por que você deve parar de usar fire and forget em Python.
3. Domando o caos assíncrono com asyncio.TaskGroup.
## Hooks para redes sociais
TaskGroup no Python 3.11 mudou a forma de estruturar tarefas assíncronas. O importante não é apenas iniciar tarefas, mas também controlar seu ciclo de vida.
Esse output é uma evidência de geração bem sucedida do pipeline. Ele não é, sozinho, uma prova de correção técnica. A saída ainda precisa passar por revisão humana, principalmente em afirmações sobre cancelamento, exceções e diferenças entre asyncio.gather e asyncio.TaskGroup.
Quando o laboratório falha
Nem toda execução terminou com um artigo. Em uma tentativa posterior, a interface do ADK Web apresentou um erro HTTP 400 com o código INVALID_ARGUMENT:
Please ensure that a function call turn comes immediately after a user turn
or after a function response turn.
Esse erro indica que o backend rejeitou a sequência de mensagens usada durante uma chamada de ferramenta. O contrato esperado exige que uma chamada de função apareça imediatamente depois de uma mensagem do usuário ou depois de uma resposta de função.
Neste ponto, é importante não transformar a mensagem em uma conclusão maior do que a evidência permite. O print comprova que a execução falhou durante o processamento de uma chamada de função. Ele não comprova, sozinho, se a causa foi o modelo, o adaptador, a montagem do histórico, uma repetição do loop ou uma combinação específica de mensagens.
O próximo passo de investigação é consultar o trace da execução e verificar a sequência exata de mensagens antes da chamada que falhou. Também vale comparar essa execução com a execução bem sucedida, observando:
- Qual modelo estava ativo.
- Qual agente estava executando.
- Se havia ocorrido um
retryantes do erro. - Qual ferramenta foi chamada.
- Se existiam duas mensagens de ferramenta ou duas chamadas consecutivas sem a resposta intermediária esperada.
Esse incidente também mostra por que uma demo técnica precisa registrar falhas. Um agente pode funcionar em uma execução e falhar na seguinte por causa de uma diferença no histórico, no modelo ou no formato de tool calling. Documentar apenas o resultado final esconderia justamente a parte mais útil do experimento.
Por enquanto, a conclusão segura é: o pipeline possui uma dependência sensível ao protocolo de chamadas de função, e essa dependência precisa ser observada em cada combinação de ADK, modelo e adaptador.
O trace dessa tentativa acrescenta uma informação importante. A execução teve latência total de aproximadamente 898,85 milissegundos. O maior bloco aparece associado ao agente Blogger, com cerca de 708,09 milissegundos. Dentro dele, a chamada call_llm levou aproximadamente 658,22 milissegundos e generate_content cerca de 640,01 milissegundos.
O trace não mostra chamadas ao RobustBlogPlanner, ao RobustBlogWriter ou aos validadores. Isso sugere que a falha ocorreu antes de o pipeline entrar nas etapas de planejamento e escrita. A evidência disponível aponta para a primeira chamada ao modelo, durante generate_content, e não para um problema no loop de validação.
Ainda não é possível concluir apenas com esse trace se o histórico já estava inválido antes da chamada ou se o backend rejeitou o formato enviado naquele momento. Para diferenciar essas hipóteses, seria necessário inspecionar o payload ou os eventos detalhados da chamada e comparar a configuração do modelo com a execução bem sucedida.
A mesma execução em uma sessão nova
Para verificar se o erro era reproduzível, a mesma solicitação foi executada em uma sessão nova do ADK Web:
Escreva um artigo técnico sobre structured concurrency em Python
Nesta segunda tentativa, o fluxo foi concluído. A interface mostrou novamente o planejador, o escritor, os artefatos de estado e o resultado final. O estado terminou com post_validation: "ok".
Essa comparação é mais informativa do que olhar apenas para a mensagem de erro. O mesmo prompt falhou em uma sessão e funcionou em outra. Portanto, não há evidência suficiente para atribuir o problema diretamente ao conteúdo do artigo ou ao código do validador.
O incidente passa a ser classificado como uma falha intermitente ou dependente do estado da sessão. As hipóteses mais prováveis ainda precisam ser verificadas no trace detalhado:
- Histórico de mensagens deixado pela sessão anterior.
- Sequência de chamadas de função construída durante a execução.
- Estado temporário do backend ou do modelo.
- Diferença de contexto entre a sessão reutilizada e a sessão nova.
O resultado também mostra uma prática útil para laboratórios com agentes: quando uma execução falhar, repetir o mesmo caso em uma sessão limpa ajuda a separar um defeito determinístico de uma falha transitória.
Um caso com requisitos mais rígidos
A etapa seguinte foi executada com um prompt que exigia uma estrutura explícita: comparação entre asyncio.gather e asyncio.TaskGroup, exemplo de código, explicação sobre asyncio.CancelledError, limitações, exatamente três referências externas e uma conclusão com cinco itens.
O agente produziu o artigo e terminou com post_validation: "ok". A sequência exibida pela interface mostra novamente o planejador, o estado do outline, o escritor e o estado final do artigo.
Essa execução não provocou um retry. Isso também é um resultado válido: significa que, nesse caso, o primeiro texto produzido já foi considerado compatível com os requisitos configurados no validador.
É importante não interpretar esse resultado como prova de que o agente sempre acerta de primeira. Ele mostra apenas que uma execução específica satisfez o contrato. Para medir a consistência, seria necessário repetir o mesmo caso várias vezes e registrar quantas execuções terminam com ok na primeira tentativa, quantas precisam de retry e quantas falham.
O valor da captura está justamente na combinação entre o prompt e o estado final. O leitor consegue ver os requisitos enviados, acompanhar as etapas do pipeline e confirmar que a execução terminou com aprovação.
Testes offline colocam um limite honesto nas conclusões
O projeto mantém uma suíte offline para testar o máximo possível sem depender de rede ou de uma chave de API. Existem também testes e benchmarks separados que fazem chamadas reais.
Essa divisão é importante porque uma suíte verde não prova que o sistema completo funciona em qualquer modelo.
Um teste offline pode provar que um validador devolve retry para um determinado fixture. Ele não prova que um modelo hospedado fará essa escolha de forma consistente.
Um servidor falso pode provar que o adaptador envia um payload válido. Ele não prova que um GGUF específico seguirá o template correto para tool calling.
Um mock pode provar que a regra de roteamento funciona. Ele não calibra um limiar de confiança sem um conjunto de dados rotulado.
O próprio projeto registra essa diferença. Um modelo pode inventar o resultado de uma ferramenta, e nenhum teste puramente offline consegue detectar esse comportamento.
Essa é uma boa prática para qualquer sistema de IA: declarar o que o teste prova e também declarar o que ele não prova.
A limitação do eval do ADK
Existe uma diferença importante entre fazer um agente funcionar e conseguir avaliá-lo com a ferramenta oficial de avaliação.
No estado atual deste projeto, o eval do ADK funciona quando o fluxo usa os modelos Gemini. Quando a execução utiliza OpenRouter ou Gemma servido localmente por llama.cpp, os agentes conseguem funcionar dentro dos requisitos definidos no código, mas não entram no mesmo caminho de avaliação do ADK.
Isso cria duas camadas diferentes de confiança.
Na primeira camada, ficam os testes de engenharia. Eles verificam se o agente monta o estado corretamente, se as ferramentas respondem, se o fallback é acionado, se o loop termina e se os contratos de saída são respeitados.
Na segunda camada, fica a avaliação oficial do ADK. Ela é útil para medir o comportamento do agente com os modelos Gemini, mas não cobre automaticamente as outras combinações de provedor e runtime presentes no projeto.
O resultado é uma assimetria que pode passar despercebida. O mesmo pipeline pode ser executado com Gemini, OpenRouter e llama.cpp, mas a evidência disponível não é equivalente entre os três ambientes.
Isso é particularmente relevante para modelos locais. O agente pode produzir o resultado esperado em um teste funcional e ainda apresentar diferenças no formato de tool calling, no uso do contexto, no veredito ok ou retry e na tendência de repetir o loop. Sem uma avaliação comparável, essas diferenças ficam mais difíceis de medir de maneira sistemática.
Neste projeto, portanto, é mais correto dizer que os modelos alternativos foram validados dentro dos contratos e testes disponíveis. Não é correto dizer que eles foram avaliados pelo mesmo mecanismo do ADK usado para Gemini.
MLflow só que não: traces e avaliação
Uma opção interessante para essa lacuna seria instrumentar o pipeline com MLflow.
Essa integração ainda não foi testada e nem sei se isso é possível, então deve ser vista como uma possibilidade de evolução, não como uma capacidade já comprovada do repositório.
O MLflow possui tracing para aplicações GenAI e agentes. A documentação atual descreve integrações automáticas com Google ADK, além de instrumentação manual com @mlflow.trace e spans. Os traces podem registrar entradas, saídas e etapas intermediárias, como chamadas de ferramentas, recuperação de documentos e decisões de roteamento. Consulte a documentação de tracing para agentes e de instrumentação automática.
Uma integração possível seria registrar cada execução com metadados como:
| Campo | Exemplo |
|---|---|
| Provedor | Gemini, OpenRouter ou llama.cpp |
| Modelo | Nome e versão do modelo usado |
| Agente | Blogger, RAG, SEO ou Code Review |
| Iteração | Primeira tentativa ou retry |
| Estado | Outline, validação ou artigo final |
| Ferramenta | HTTP, MCP, filesystem ou busca |
| Resultado |
ok, retry, erro ou timeout |
| Custo e latência | Quando o provedor disponibilizar esses dados |
Com esses traces, seria possível comparar os mesmos casos de teste entre provedores sem depender exclusivamente do eval do ADK.
O mlflow.genai.evaluate() também permite avaliar traces já coletados usando scorers prontos ou métricas customizadas. A documentação do MLflow cita avaliações de relevância, groundedness, aderência a guidelines e comportamento intermediário do agente. Veja avaliação de traces e a referência de mlflow.genai.evaluate.
Para este repositório, os scorers mais interessantes provavelmente não seriam apenas métricas de qualidade textual. Eles poderiam verificar:
- Se o agente chamou a ferramenta esperada.
- Se o arquivo acessado estava dentro do diretório permitido.
- Se o resultado foi validado antes de ser entregue.
- Se o agente interrompeu o loop depois de
ok. - Se o RAG citou um trecho realmente recuperado.
- Se um modelo de fallback preservou o mesmo schema de saída.
Essa abordagem permitiria separar três tipos de avaliação:
- Testes determinísticos para regras de aplicação.
- Avaliação de traces para o comportamento do agente.
- Avaliação específica do modelo para qualidade de texto, tool calling e aderência às instruções.
O MLflow não resolveria automaticamente a diferença entre provedores. Ainda seria necessário definir um dataset comum, normalizar os campos de saída, decidir qual modelo será usado como juiz e separar qualidade do texto de correção do workflow.
Mas ele poderia oferecer a camada de observabilidade e comparação que falta quando o mesmo agente roda em Gemini, OpenRouter e llama.cpp.
Fallback melhora disponibilidade, mas não garante qualidade
O common.py monta uma cadeia com modelo principal, modelo alternativo e, opcionalmente, um servidor local baseado em llama.cpp.
Esse desenho ajuda quando existe um erro temporário, uma limitação de cota ou a necessidade de executar localmente. Mas trocar de modelo pode alterar várias coisas ao mesmo tempo:
- O formato das chamadas de ferramentas.
- A disposição para devolver exatamente
okouretry. - A forma de lidar com contexto.
- A latência e o custo.
- A qualidade da revisão e não apenas a qualidade do primeiro rascunho.
Essa última parte é especialmente importante em um pipeline com loops. Um modelo que critica excessivamente um resultado já aceitável pode consumir outra iteração e mais uma chamada de API.
Nesse caso, o melhor modelo não é necessariamente aquele que escreve o texto mais bonito. É aquele que respeita o contrato de controle com consistência suficiente para o loop terminar.
Por isso, modelos de fallback devem ser avaliados com os contratos reais do sistema. Uma pergunta genérica não é suficiente para validar um modelo que precisa produzir tool calls, obedecer schemas e decidir quando encerrar uma revisão.
O que temos neste projeto
- Divide o fluxo por responsabilidade.
- Torna os artefatos intermediários explícitos.
- Usa validadores como parte do controle de fluxo.
- Coloca regras exatas em código.
- Limita o acesso a arquivos e ferramentas.
- Separa testes de transporte dos testes de modelo.
- Mantem uma suíte determinística offline.
- Registra limitações e tarefas pendentes.
🚨 Isso não prova que oito agentes são melhores do que um.
Também não apresenta um benchmark independente comparando ADK com outros frameworks. Os números de custo, latência, cota e qualidade dependem do ambiente descrito no projeto e podem mudar com versões futuras.
🚨 Isso não reduz o valor do repositório. Apenas define melhor o que ele representa: um exemplo concreto de como tornar sistemas agentic mais observáveis, testáveis e fáceis de explicar.
Uma lista a considerar para um sistema com ADK
Antes de começar a criar mais um agente, vale responder estas perguntas:
- Existe uma responsabilidade nova ou apenas mais um prompt para a mesma responsabilidade?
- Qual artefato atravessa a fronteira entre as etapas?
- Como o próximo agente recebe esse artefato?
- Quais regras precisam ser verificadas por código?
- O que o modelo pode acessar e o que deve ficar limitado pela aplicação?
- Quais falhas precisam de duas verificações independentes?
- O comportamento pode ser testado sem modelo ao vivo ou sem rede?
- O modelo de fallback respeita o mesmo schema e o mesmo critério de término?
- Quais afirmações são testadas, quais são apenas observadas e quais ainda são desconhecidas?
Os sistemas multiagentes mais maduros não são necessariamente os que têm a hierarquia mais complexa. São os que possuem contratos claros entre as etapas, condições explícitas de término e uma noção honesta do que cada teste realmente comprova.
Multiagentes não são uma resposta universal
Existe muito entusiasmo em torno de arquiteturas multiagentes e de modelos especializados como o JEV. Esse entusiasmo pode ser útil para descobrir novas possibilidades, mas não deve virar uma decisão automática de arquitetura.
Dividir uma aplicação em agentes especializados pode facilitar a organização das responsabilidades. Também pode tornar o sistema mais fácil de explicar, testar e evoluir. Ao mesmo tempo, cada divisão cria uma nova fronteira de comunicação, uma nova dependência e um novo ponto onde o fluxo pode falhar.
Neste projeto, por exemplo, uma execução pode depender de vários componentes diferentes:
- O runtime do ADK.
- O modelo principal.
- O modelo de fallback.
- O provider usado para acessar um modelo externo.
- O servidor MCP.
- A sequência de chamadas de ferramentas.
- O estado compartilhado entre os agentes.
- Os validadores e suas condições de término.
Adicionar fallback também não garante disponibilidade. O caminho Gemini pode falhar por cota ou indisponibilidade. O OpenRouter pode apresentar erro de rede ou de autenticação. O JEV pode não responder como esperado. O llama.cpp pode estar indisponível, lento ou incompatível com determinado formato de tool calling.
Um fallback aumenta as opções de recuperação, mas também aumenta a superfície que precisa ser observada e testada. Se os modelos não obedecerem ao mesmo contrato de saída, a troca de provider pode apenas transformar um erro de disponibilidade em um erro de comportamento.
Por isso, a pergunta mais importante antes de adotar uma arquitetura multiagente não é “qual framework está em alta?”. É:
A divisão em agentes resolve um problema real deste caso de uso ou apenas adiciona complexidade ao sistema?
Se uma única chamada bem delimitada resolve o problema, uma arquitetura com vários agentes pode ser uma escolha ruim. Se existem responsabilidades claramente diferentes, ferramentas distintas, ciclos de validação ou limites de contexto que justificam a separação, o desenho multiagente pode fazer sentido.
A recomendação mais segura é experimentar e validar no próprio contexto. Comece com uma implementação simples. Meça custo, latência, taxa de erro, qualidade e facilidade de manutenção. Depois compare com uma versão multiagente usando os mesmos casos de teste.
O resultado dessa comparação é mais importante do que a popularidade de uma técnica. Arquitetura deve seguir necessidade real, evidência e comportamento observado.
O próprio agente Blogger como guia para este post
Este texto também foi estruturado usando o próprio gerador de Blogger apresentado no projeto.
A ideia não foi pedir que o agente escrevesse um artigo definitivo e publicar o resultado sem revisão. O gerador foi usado como guia para organizar o tema, testar o fluxo de planejamento, observar os artefatos intermediários e entender como o sistema lida com validação, falhas e novas tentativas.
O processo acabou funcionando como uma demonstração do próprio argumento deste artigo:
- O agente ajudou a produzir uma primeira estrutura.
- O ADK Web permitiu observar eventos, estado e traces.
- O output foi revisado contra o comportamento real do repositório.
- As execuções bem sucedidas e as falhas foram documentadas.
- Afirmações que não podiam ser comprovadas foram tratadas como limitações ou hipóteses.
Esse ciclo é mais interessante do que a ideia de geração automática sem supervisão. O modelo acelera a organização e a produção de material, mas a qualidade final depende da combinação entre código, testes, observabilidade e revisão técnica.
No fim, o objetivo de um sistema multiagente não deveria ser esconder o processo atrás de uma resposta convincente. Deveria ser tornar o processo claro o suficiente para que outra pessoa consiga entender o que aconteceu, reproduzir o experimento e decidir o que ainda precisa ser melhorado.
É esse o principal aprendizado do projeto: agentes são mais úteis quando fazem parte de um sistema com contratos, ferramentas determinísticas, traces e limites explícitos.
Referências
O projeto funcional está no github em vongrossi/adk-multiagent.
Alguns conceitos usados neste artigo,
ADK Agents, ADK Workflows, o repositório Python do ADK e a especificação do MCP.
Para a estratégia de avaliação proposta, consulte a documentação de MLflow Tracing, instrumentação automática e avaliação de traces.













Top comments (2)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.