DevSecOpsARTIGO

Interfaces e Contratos: Como Agentes Respeitam Fronteiras

Interfaces e Contratos: Como Agentes Respeitam Fronteiras
Imagem: Iago Cavalcante

Duas equipes. Dois serviços. Um prazo apertado. Cada time trabalha no seu canto por duas semanas. No dia da integração, ligam os dois serviços e… nada funciona.

O serviço A manda o campo user_id como string. O serviço B espera um inteiro. O serviço A retorna erros com um campo error_message. O serviço B procura por error. O serviço A usa /api/v1/users/:id. O serviço B chama /users/get. E por aí vai.

Cada serviço funciona perfeitamente sozinho. Juntos, são incompatíveis.

Agora troca “duas equipes” por “dois prompts dados a um agente de IA” e o cenário é exatamente o mesmo.

O problema da interface implícita

Quando dois times não definem uma interface explícita antes de começar a codar, cada um inventa a sua. E como cada time tem contextos, experiências e preferências diferentes, as interfaces inventadas raramente são compatíveis.

Com humanos, isso é inconveniente mas recuperável. Os devs sentam juntos, olham os contratos, negociam, ajustam. Leva um dia, talvez dois. Frustrante, mas factível.

Com agentes de IA, o problema é mais profundo. O agente não negocia. Ele não olha o código do outro serviço pra ver como adaptar o seu. Quando recebe um prompt dizendo “crie um serviço de usuários”, ele gera a interface que acha mais provável com base no que aprendeu. E quando outro prompt pede “crie um serviço de pedidos que consome o serviço de usuários”, o segundo agente inventa a sua própria versão de como o serviço de usuários deveria funcionar.

O resultado é o mesmo das duas equipes: dois sistemas que funcionam isolados e quebram na integração.

Interface-first: a lição dos RFCs

Em engenharia de software, esse problema foi resolvido há décadas. A solução tem um nome simples: contract-first design – ou, no mundo dos RFCs, definição de interface antes da implementação.

A ideia é direta. Antes de qualquer time escrever uma linha de código, todos concordam com o contrato. O contrato define:

  • Endpoints: quais caminhos existem e o que cada um faz

  • Tipos de entrada: o que cada endpoint recebe, com tipos exatos

  • Tipos de saída: o que cada endpoint retorna, em caso de sucesso e de erro

  • Códigos de status: quais respostas HTTP (ou equivalentes) cada cenário produz

  • Formato de erros: como erros são representados

Quando o contrato existe antes do código, cada time pode implementar livremente por dentro. Pode mudar a arquitetura interna, trocar o banco de dados, refatorar tudo – desde que o contrato continue sendo respeitado. A interface é a fronteira. Dentro dela, liberdade total. Fora dela, responsabilidade compartilhada.

Esse princípio é tão fundamental que aparece em praticamente todo RFC bem escrito. A seção de “interfaces” ou “API specification” quase sempre vem antes da seção de implementação.

O mesmo princípio, aplicado a agentes

Quando você pede a um agente de IA para criar um serviço, o prompt é o seu RFC. E se o RFC não define interfaces, o agente improvisa.

Vamos ver isso na prática com um exemplo concreto.

Time A – prompt sem contrato:

Crie um serviço de usuários em Elixir/Phoenix que permita

criar, buscar e atualizar usuários. Use JSON para comunicação.

Time B – prompt sem contrato:

Crie um serviço de pedidos em Elixir/Phoenix que crie pedidos

para usuários existentes. Consulte o serviço de usuários

para validar que o usuário existe antes de criar o pedido.

O que acontece?

O agente do Time A pode retornar usuários assim:

{

"user": {

"id": "usr_abc123",

"full_name": "Maria Silva",

"email": "maria@example.com",

"created_at": "2026-03-12T10:00:00Z"

}

}

E o agente do Time B pode gerar código que espera isso:

{

"data": {

"user_id": 42,

"name": "Maria Silva",

"email": "maria@example.com"

}

}

O campo id virou user_id. O tipo mudou de string pra inteiro. O wrapper mudou de user pra data. O campo full_name virou name. Nenhuma dessas decisões está “errada” isoladamente – são escolhas válidas. Mas juntas, são incompatíveis.

O contrato como fundação do prompt

A solução é a mesma nos dois mundos: definir o contrato primeiro. Só que no mundo dos agentes, o contrato precisa estar dentro do prompt.

Veja como o mesmo cenário funciona quando você inclui o contrato:

Contrato compartilhado (incluído em ambos os prompts):

## Contrato da API de Usuários

### GET /api/v1/users/:id

Resposta de sucesso (200):

{

"id": integer,

"name": string,

"email": string

}

Resposta de erro (404):

{

"error": "not_found",

"message": string

}

Resposta de erro (422):

{

"error": "validation_failed",

"details": [{"field": string, "message": string}]

}

### POST /api/v1/users

Body esperado:

{

"name": string (obrigatório, max 100),

"email": string (obrigatório, formato email)

}

Resposta de sucesso (201): mesmo formato do GET

Time A – prompt com contrato:

Crie o serviço de usuários em Elixir/Phoenix.

Implemente os endpoints conforme o contrato abaixo.

NÃO altere os nomes de campos, tipos ou caminhos dos endpoints.

[contrato acima]

Time B – prompt com contrato:

Crie o serviço de pedidos em Elixir/Phoenix.

Ao validar que o usuário existe, chame o serviço de usuários

conforme o contrato abaixo. Use exatamente os campos e tipos

definidos no contrato para parsear a resposta.

[contrato acima]

Agora os dois agentes trabalham com a mesma fonte de verdade. O serviço A implementa a interface exatamente como definida. O serviço B consome a interface exatamente como definida. A integração funciona na primeira tentativa.

Type specs como contratos no Elixir

Se você trabalha com Elixir, já tem uma ferramenta poderosa pra definir contratos: typespecs. E elas funcionam muito bem dentro de prompts.

Em vez de descrever a interface em texto livre, você pode incluir specs que o agente vai respeitar:

@type user :: %{

id: integer(),

name: String.t(),

email: String.t()

}

@type error_response :: %{

error: String.t(),

message: String.t()

}

@spec get_user(integer()) :: {:ok, user()} | {:error, error_response()}

@spec create_user(map()) :: {:ok, user()} | {:error, error_response()}

Quando você inclui isso no prompt, está dando ao agente um contrato com a precisão de uma linguagem de programação, não a ambiguidade de uma descrição em prosa. O agente sabe exatamente que get_user recebe um inteiro e retorna uma tupla com um mapa de campos específicos.

É a diferença entre dizer “retorne os dados do usuário” e dizer “retorne {:ok, %{id: integer(), name: String.t(), email: String.t()}} ou {:error, %{error: String.t(), message: String.t()}}”. A segunda versão não deixa espaço pra interpretação.

O template contract-first pra prompts

Depois de aplicar esse padrão em vários projetos, cheguei num template que funciona consistentemente:

## Contexto

[O que o serviço faz e onde ele se encaixa no sistema]

## Contratos de Interface

### Interfaces que este serviço EXPÕE

[Endpoints, tipos de entrada, tipos de saída, códigos de erro]

### Interfaces que este serviço CONSOME

[Endpoints externos que ele chama, com formatos esperados de resposta]

## Regras de implementação

[Como o serviço deve funcionar internamente]

## Fora do escopo

[O que NÃO implementar]

A seção de contratos vem antes das regras de implementação. Isso é intencional. O contrato define a forma; a implementação preenche o conteúdo. O agente lê o contrato primeiro e já sabe quais são as fronteiras antes de começar a escrever código.

Por que contratos explícitos importam mais pra agentes do que pra humanos

Um dev humano, quando encontra uma inconsistência na integração, faz o que qualquer profissional faz: investiga, pergunta, adapta. Ele abre o código do outro serviço, lê a documentação, manda uma mensagem no Slack. É um processo lento, mas funciona.

O agente não faz nada disso. Ele trabalha com o que tem no prompt. Se o prompt não tem o contrato, ele inventa. E o que ele inventa é baseado em padrões estatísticos de milhões de repositórios – ou seja, vai ser algo razoável, mas não vai ser o seu contrato.

Isso significa que, paradoxalmente, contratos explícitos importam mais pra agentes do que pra humanos. Humanos compensam a falta de contrato com comunicação informal. Agentes não têm essa opção.

O efeito cascata

Uma interface mal definida num prompt não quebra só um serviço. Ela quebra tudo que depende dele.

Se o agente gera um serviço de usuários com campos diferentes do esperado, o serviço de pedidos quebra. Se o serviço de pedidos está quebrado, o serviço de pagamentos que depende dele também. E se você está usando agentes pra gerar vários serviços ao mesmo tempo, cada interface implícita é uma bomba-relógio.

A abordagem contract-first evita o efeito cascata na raiz. Quando todos os contratos estão definidos antes da implementação, cada agente pode trabalhar de forma independente sem risco de incompatibilidade. É o mesmo princípio que permite que times distribuídos trabalhem em paralelo – o contrato é o ponto de sincronização.

Iago CavalcanteAutor

Iago Cavalcante atualmente trabalha na EasyMateAI como Software Engineer. Apaixonado por tudo que envolve tecnologia. Gosta de contribuir e ajudar no crescimento das comunidades. Busca contribuir com o mundo open-source e acredita na lei do retorno.

Ver perfil