TL;DR: Sua IA vai inventar campos de API que parecem perfeitos, mas não existem. A solução não é cruzar os dedos para que funcione: é baixar o schema real antes de escrever o código, capturar respostas reais como fixtures e separar o fetch do processamento para poder testar sem dependência de rede. Programação adversarial: programe assumindo que seu copiloto mente.


Você já escreveu código para uma API, tudo compilou, os testes passaram, a lógica fazia sentido… e ao conectar com a API real, nada funcionava?

Se você programa sozinho, isso acontece quando você interpreta mal a documentação. Se você programa com uma IA, acontece porque a IA inventou a documentação.

A alucinação que não parece alucinação

Eu estava desenvolvendo um CLI em Rust para interagir com uma API GraphQL. Pedi ao meu copiloto IA para implementar um filtro para ordenar resultados por prioridade. Ele me devolveu algo assim:

query {
  issues(orderBy: { priority: ASC }) {
    nodes {
      id
      title
      priority
    }
  }
}

Limpo. Razoável. Exatamente o que você esperaria. Apenas um problema: o campo orderBy dessa API não aceita priority como valor. O enum real se chama PaginatedOrder e tem valores como createdAt e updatedAt. Não priority.

Como eu descobri isso? Quando a API retornou um erro 400 que não fazia sentido. Levei 20 minutos para entender que o problema não estava no meu código — era que o campo que eu estava usando não existia.

O padrão é sempre o mesmo

Este não foi um caso isolado. Durante semanas de desenvolvimento, a IA alucinou repetidamente:

  • Campos de filtragem que não existem — state.id.or em vez de state.type.in. Parece lógico, mas a API usa um padrão completamente diferente.
  • Enums inventados — nomes de valores que soam como deveriam ser chamados, mas que nunca foram definidos na API.
  • Padrões de outro ecossistema — em um projeto Rust, ela me sugeriu usar fcntl.flock para file locking. Isso é Python. Em Rust, você usa fs2::FileExt.

Cada erro era plausível. Nenhum era estúpido. Um iniciante poderia ter cometido exatamente os mesmos erros ao interpretar superficialmente a documentação. E é justamente isso que os torna perigosos: não parecem alucinações. Parecem código razoável feito por alguém que entende o domínio “mais ou menos”.

Por que os LLMs inventam APIs?

Em linguagem simples: o LLM não sabe quais campos sua API tem. Ele viu milhares de APIs GraphQL durante seu treinamento e, quando você pede para usar uma, ele faz o que um humano com boa intuição, mas sem acesso à documentação faria: adivinha.

E ele adivinha bem. Quase sempre. O suficiente para te convencer. Esse “quase” é o que arruína seu sprint.

É como trabalhar com um colega brilhante que nunca lê a documentação, mas sempre tem uma resposta convincente. Ele diz “sim, o endpoint aceita um campo priority” com tanta segurança que você não verifica. E quando o código falha em produção, você descobre que ele inventou.

A solução: programação adversarial

Depois da terceira alucinação em uma semana, adotei uma abordagem diferente. Em vez de confiar e verificar depois, comecei a desconfiar e verificar antes. Eu chamo isso de programação adversarial: programe assumindo que seu copiloto vai inventar coisas.

Não é hostilidade. É higiene.

1. Introspecção de schema antes de escrever código

Se você trabalha com uma API GraphQL, antes de pedir qualquer coisa à sua IA, baixe o schema real:

# Baixar o schema completo da API
curl -s https://api.example.com/graphql \
  -H "Authorization: token-here" \
  -H "Content-Type: application/json" \
  -d '{"query":"{ __schema { types { name fields { name type { name kind ofType { name } } } } } }"}' \
  > schema.json

Agora você tem a verdade. Quando a IA sugerir “use orderBy: { priority: ASC }”, você pode buscar no schema e ver que priority não está no enum de ordenação. Passe para ela o fragmento relevante do schema e diga “use apenas esses campos”. Fim das adivinhações.

Para APIs REST, o equivalente é baixar a especificação OpenAPI. Para qualquer API, o princípio é o mesmo: obtenha a fonte da verdade antes de escrever uma linha de código.

2. Fixtures reais, não inventadas

A segunda defesa é capturar respostas reais da API e salvá-las como fixtures para testes:

# Capturar uma resposta real
curl -s https://api.example.com/graphql \
  -H "Authorization: token-here" \
  -d '{"query":"{ items(first: 5) { nodes { id title state { name } } }"}' \
  > tests/fixtures/items_real.json

Esse JSON não foi gerado por nenhum LLM. Ele vem da API real. Contém os campos reais, com os tipos reais, com os valores reais. Quando você escrever um parser, teste-o com esse fixture. Se seu DTO não conseguir deserializar a resposta real, o teste falha. Fim das ficções.

O segredo está na disciplina: nunca deixe que a IA gere fixtures. Se ela gerar, você estará testando uma invenção contra outra invenção. Um castelo de cartas perfeito que desaba ao tocar a realidade.

3. Separar fetch do processamento

Esta é a peça arquitetônica essencial. Se seu código faz fetch + parse + transformação tudo junto, você não consegue testar a análise sem a rede. E sem testes offline, você precisará de mocks. E se a IA gerar os mocks… voltamos ao ponto 2.

A solução é separar em duas camadas:

┌─────────────────────┐
│   Cliente (fetch)    │  ← Fala com a API real
│   Apenas HTTP + JSON │
└────────┬────────────┘
         │ JSON cru
┌────────▼────────────┐
│     Processador      │  ← Faz parse, transforma, formata
│     Apenas dados     │
└─────────────────────┘

O cliente é enxuto: faz a requisição HTTP e retorna JSON cru. O processador recebe esse JSON e o transforma. Para testar o processador, você utiliza as fixtures reais. Não precisa de mock para o HTTP. Não precisa da rede. E não dá oportunidade para a IA inventar o formato do JSON, pois você já o tem.

4. Checklist adversarial

Antes de aceitar código que interage com uma API externa, siga este checklist:

PerguntaSe a resposta for não…
Tenho o schema/spec da API no projeto?Baixe antes de continuar
Os campos usados pelo código existem no schema?Procure. Se não estão lá, a IA inventou
As fixtures de teste vêm da API real?Capture-as. Não permita que a IA as gere
Posso testar o parse sem fazer HTTP?Separe fetch do processamento
Os tipos no código coincidem com os da API?Compare sua struct/DTO com o schema

Cinco perguntas. Trinta segundos. Economiza horas de debugging fantasma.

O dogfooding brutal

Agora vem a parte dolorosa. Enquanto desenvolvia este CLI, sofri na própria pele exatamente os problemas que a ferramenta pretendia resolver.

O CLI existia para simplificar a interação com uma API de gestão de tarefas. Durante o desenvolvimento, toda vez que eu precisava criar uma tarefa para registrar um bug… eu tinha que lutar contra a API que estava envolvendo. O dogfooding não foi uma escolha — foi uma condenação.

Exemplos concretos deste inferno:

  • Escapamento de JSON quebrado. Para colocar uma descrição com aspas em uma tarefa, era necessário escapar em três níveis: shell, JSON, GraphQL. Um parêntese mal posicionado e a API retornava um erro críptico. Passei mais tempo escapando corretamente a descrição de um bug do que corrigindo o próprio bug.
  • UUIDs para relacionamentos. Quer atribuir uma tarefa a um projeto? Não pode usar o nome do projeto. Precisa do UUID. E como você consegue o UUID? Com outra query. E o rótulo? Outro UUID, outra query. Para criar uma tarefa com projeto, rótulo e estado, precisei de 4 queries encadeadas.
  • 32 requisições para criar dependências. Queria criar 8 tarefas com dependências entre elas. Cada dependência requer uma mutação separada com os UUIDs de ambas as tarefas. 8 creates + 24 relações = 32 chamadas à API para algo que deveria ser um arquivo YAML.

Cada um desses problemas alimentou diretamente o design da ferramenta. O escapamento quebrado → entrada por arquivo, nunca inline. Os UUIDs → resolução automática por nome. As 32 requisições → operações em batch.

Evolução convergente do output

E então aconteceu algo curioso. Um dos princípios de design era que o output fosse compacto — projetado para que um LLM o consumisse gastando poucos tokens. Desenhei desde zero um formato que eliminava chaves repetidas e usava posição e delimitadores leves:

PROJ-42 [Backlog] backend — Refatorar o parser de configuração (14d)
PROJ-43 [In Progress] api — Implementar rate limiting (3d, overdue!)

Cerca de 25 tokens por elemento, comparado aos ~50 em JSONL. Sem chaves repetidas ("state":, "labels":, "title":) porque o LLM entende a estrutura por posição.

Meses depois, descobri o TOON (Token-Oriented Object Notation), um formato publicado em novembro de 2025 que faz exatamente isso: elimina redundâncias do JSON para reduzir o consumo de tokens quando o consumidor é um LLM. TOON usa headers de schema e linhas tabulares — sintaxe diferente, mas o mesmo princípio.

Eu não copiei. Eu nem conhecia. Evolução convergente: quando dois times resolvem o mesmo problema (JSON é muito verboso para LLMs), chegam à mesma solução (eliminar chaves repetidas, usar posição). É a mesma razão pela qual golfinhos e tubarões têm a mesma forma, embora um seja mamífero e o outro peixe.

Quando dois projetos independentes chegam à mesma conclusão, é a melhor validação de que o problema é real.

O que mudou

Depois de adotar programação adversarial, o índice de alucinações em código para API caiu drasticamente. Não para zero — a IA ainda é uma IA — mas os erros que restaram foram erros de lógica, não de ficção. Erros normais. Erros de programador. Não erros do tipo “invente um campo que não existe e construa um castelo em cima”.

A diferença crucial está no momento em que você descobre o erro:

Sem adversarialCom adversarial
Descobre em runtimeDescobre antes de escrever código
Debug de 30 minutos procurando “por que não funciona”O schema avisa “esse campo não existe”
A fixture inventada passa no testeA fixture real quebra o teste
Três camadas de ficção em cima de ficçãoUma camada de realidade desde o início

Sua vez

Se você programa com um LLM lidando com APIs externas, comece assim:

  1. Baixe o schema de cada API que usa. Coloque no repositório. É sua fonte de verdade.
  2. Capture fixtures reais. Um comando curl e > fixture.json. Dez segundos.
  3. Separe fetch do processamento. Seus parsers devem ser testados com arquivos, não com a rede.
  4. Desconfie de nomes plausíveis. Se a IA sugerir “use orderBy.priority”, procure no schema antes de implementar.

Não é paranoia. É engenharia. A IA é uma ferramenta extraordinária, mas seu pior modo de falha não é o erro óbvio — é o erro que parece estar correto. E contra isso, a única defesa é a realidade.

Vamos com tudo.


Série: Programação Adversarial