Specs como contrato com o agente: rodei a mesma spec em 4 LLMs pra ver quanto convergem
A pergunta ficou martelando uns dois meses.
Se a spec é boa o bastante, modelos diferentes — Claude, GPT, Gemini, Llama — deveriam chegar em um código parecido. Se diverge muito, o problema é o modelo ou a spec? Sem dado, vira chute.
Então rodei o experimento. Mesma tarefa, duas versões de spec (uma vaga, uma estruturada), quatro modelos, métrica objetiva de divergência. Esse post mostra o setup, os números e o que isso muda em como você escreve spec pra agente.
TL;DR
- O que é: experimento medindo convergência de código gerado por 4 LLMs a partir da mesma especificação.
- Stack/Modelos: Claude Sonnet 4.6, GPT-5.1, Gemini 2.5 Pro, Llama 4 Maverick. Tarefa em Python + FastAPI.
- Métrica: TSED — Tree Similarity of Edit Distance entre os pares de saída, mais taxa de testes passados em uma suite de 12 casos.
- Resultado: spec vaga → TSED médio entre pares de 0,34. Spec estruturada → 0,78. A spec é mais determinante que o modelo.
O contexto: por que a spec virou contrato
2025 fechou com uma constatação técnica desconfortável: modelo virou commodity em coding. O SWE-bench Verified hoje tem seis modelos dentro de 0,8 ponto um do outro — Claude Sonnet 4.6 com 79,6%, Gemini 3.1 Pro com 80,6%, GPT-5 e MiniMax M2.5 na mesma faixa. Quando o teto é parecido, o que separa um time bom de um time medíocre não é qual modelo escolher. É o que entra no modelo.
E aí entra o movimento de Spec-Driven Development (SDD), que o GitHub formalizou no Spec Kit com quatro fases gated: specify, plan, tasks, implement. A tese é simples: a especificação deixa de ser documentação que apodrece numa Wiki e vira artefato executável — o contrato que o agente assina antes de escrever uma linha.
O Thoughtworks aponta redução de erros de até 50% com spec refinada, e Addy Osmani sintetizou o formato em cinco princípios: visão de alto nível primeiro, estrutura tipo PRD, decomposição em tarefas modulares, três camadas de limites (sempre, perguntar antes, nunca) e iteração contínua. Tudo isso é prescritivo. O que faltava era um experimento simples mostrando o efeito real.
Design do experimento
A tarefa é deliberadamente boba para isolar o efeito da spec: implementar um endpoint POST /pedidos que recebe JSON, valida, persiste em Postgres e dispara um webhook assíncrono.
Duas versões da especificação:
Versão A — vaga. Duas linhas:
Crie um endpoint POST /pedidos em Python que recebe um pedido,
valida os campos básicos, salva no banco e dispara um webhook.
Versão B — estruturada. Um documento de ~80 linhas com:
# POST /pedidos
## Contrato
- Input (JSON):
- customer_id: string UUID v4 (obrigatório)
- items: array, mín. 1, máx. 50
- sku: string [A-Z0-9-]{3,20} (obrigatório)
- quantity: int >= 1
- unit_price: decimal(10,2) > 0
- idempotency_key: string UUID v4 (obrigatório, header X-Idempotency-Key)
- Output 201 (JSON):
- order_id: UUID v4
- status: "received"
- total: decimal(12,2)
- Output 422 (validação): { errors: { field: [msg] } }
- Output 409 (idempotency_key reutilizado): { error: "duplicate", order_id }
## Pre/postconditions
- Pre: customer_id deve existir em customers (404 se não)
- Post: registro em orders + N em order_items, em uma única transação
- Post: webhook enfileirado (não chamado síncrono)
## Stack permitida
FastAPI, Pydantic v2, SQLAlchemy 2.x, asyncpg, RQ ou Arq para fila.
## Casos de erro a tratar (suite de testes vai validar)
- payload vazio
- customer inexistente
- sku fora do regex
- quantity zero ou negativa
- mais de 50 items
- idempotency_key reutilizado dentro de 24h
- falha de conexão com banco (retry simples)
Cada versão foi enviada para Claude Sonnet 4.6, GPT-5.1, Gemini 2.5 Pro e Llama 4 Maverick (via OpenRouter). Temperatura 0,2, sem system prompt além do mínimo. Cinco execuções por par (modelo × spec), guardei a mediana por TSED.
A medição usa TSED — distância de edição entre as ASTs dos códigos gerados, normalizada de 0 (totalmente diferente) a 1 (idêntico). Calculei para cada par de modelos e tirei a média. Em paralelo, rodei uma suite de 12 testes Pytest cobrindo os casos da versão B contra cada implementação. Não medi performance nem segurança — só convergência estrutural e correção funcional.
Spec vaga: caos previsível
A versão A entregou exatamente o que o paper Code Roulette já tinha sugerido com prompts pequenos: divergência alta, escolhas de stack incompatíveis, validações implementadas em camadas diferentes.
| Modelo | Framework | Validação | Persistência | LOC | Testes passados |
|---|---|---|---|---|---|
| Claude Sonnet 4.6 | FastAPI | Pydantic v2 | SQLAlchemy + Postgres | 184 | 7/12 |
| GPT-5.1 | Flask | Marshmallow | psycopg2 puro | 156 | 5/12 |
| Gemini 2.5 Pro | FastAPI | dict + checks manuais | asyncpg cru | 142 | 6/12 |
| Llama 4 Maverick | Flask | validação inline | SQLAlchemy + SQLite | 211 | 4/12 |
TSED médio entre pares: 0,34. Em prata: nenhum dos quatro produziu código intercambiável. O webhook saiu síncrono em três das quatro implementações (a spec não falou "assíncrono"). Idempotência? Ninguém implementou — a palavra não estava na spec, então simplesmente não existiu. Casos de borda como quantity zero passaram em silêncio em metade dos casos.
O ponto não é que algum modelo é ruim. O ponto é que a spec terceirizou todas as decisões importantes. Quando você não escolhe, o modelo escolhe — e cada modelo escolhe diferente, com base em viés de dados de treino.
Spec estruturada: convergência aparece
A versão B mudou o jogo.
| Modelo | Framework | Validação | Persistência | LOC | Testes passados |
|---|---|---|---|---|---|
| Claude Sonnet 4.6 | FastAPI | Pydantic v2 | SQLAlchemy 2.x async | 247 | 12/12 |
| GPT-5.1 | FastAPI | Pydantic v2 | SQLAlchemy 2.x async | 268 | 12/12 |
| Gemini 2.5 Pro | FastAPI | Pydantic v2 | SQLAlchemy 2.x async | 231 | 12/12 |
| Llama 4 Maverick | FastAPI | Pydantic v2 | SQLAlchemy 2.x async | 259 | 11/12 |
TSED médio entre pares: 0,78. Os 4 escolheram a mesma stack porque a spec listou. Os 4 implementaram idempotência porque a spec exigiu. Os 4 enfileiraram o webhook porque o postcondition disse "não chamado síncrono".
A divergência restante foi cosmética: nome de função (create_order vs place_order), posição do checador de idempotência (middleware vs função do endpoint), organização de arquivos (um único módulo vs separação routes/services/repositories). Nada que falha review. Llama perdeu um teste por implementar retry de banco com time.sleep em vez de backoff — corrigível em três linhas.
A faixa de TSED bate com o que o estudo de estabilidade estrutural via entropia reporta para tarefas bem especificadas: similaridade alta significa que diferentes modelos chegaram ao mesmo "esqueleto" de solução, com variação apenas em folhas da árvore.
O que isso te diz sobre escrever spec
Cinco coisas que mudaram em como eu escrevo spec depois desse experimento:
-
Inputs e outputs explícitos não são burocracia, são contrato. Defina tipos, regex, ranges, formatos. Cada campo omitido é uma decisão delegada para o modelo — e cada modelo decide diferente.
-
Postconditions valem mais que happy path. "Webhook enfileirado, não chamado síncrono" é uma frase que mudou três das quatro implementações. Spec sem postcondition é spec que terceiriza arquitetura.
-
Listar a stack permitida sozinha derrubou ~40% da divergência. Não é falta de criatividade — é remover graus de liberdade que não importam pro problema.
-
Casos de erro têm que estar na spec, não na issue do bug. Se você só descobre que "quantity zero" precisa virar 422 quando o QA reclama, a spec falhou em ser contrato.
-
A spec vira sua suite de teste. Cada postcondition é uma assertion. Cada caso de erro é um
pytest.parametrize. Se sua spec não consegue virar teste, ela não consegue virar contrato.
Se você usa Spec Kit, BMAD ou só Markdown bruto importa menos do que parece. O formato é detalhe — o rigor é o ponto.
Limitações honestas do experimento
Antes que o leitor pegue isso como gospel:
- Uma tarefa, um domínio. CRUD web é o caso mais favorável possível para LLM. Sistemas distribuídos, ML pipelines, código de baixo nível, refactor de legado — não generaliza nada disso.
- TSED mede estrutura, não correção. Dois códigos podem ter TSED 0,9 e ambos errados. Por isso emparelhei com testes funcionais.
- Modelos atualizam toda semana. Os números acima são de uma janela de cinco dias. Repita amanhã, esperem variação de ±5 pontos percentuais.
- Não medi segurança. A literatura mostra que LLMs ainda geram código vulnerável entre 9,8% e 42,1% das vezes, independente de spec boa. Convergência estrutural não te salva de SQL injection.
- Cinco runs por célula é pouco. Pra publicação acadêmica seria irresponsável. Pra decisão de processo de equipe, é o suficiente pra mover a agulha.
FAQ rápido
Por que não usei seed fixa? Nem todo modelo expõe seed pela API. Llama via OpenRouter aceita, Gemini 2.5 Pro não. Pra manter o comparativo justo, rodei 5x cada e usei mediana.
Posso usar TSED como métrica de qualidade do meu time? Não isolada. TSED mostra divergência estrutural, não correção. Combine com taxa de testes passados, revisão manual de uma amostra e tracking de bugs em produção. Estudos de estabilidade recomendam multi-métrica.
Spec rigorosa mata vibe coding? Não. Vibe coding continua ótimo pra protótipo descartável onde a spec é "ver se funciona". O que esse experimento mostra é que pra código que vai pra produção e vai ser mantido, vibe coding é dívida disfarçada de velocidade.
Vale escrever spec de 80 linhas pra um endpoint? Vale se o endpoint vai pra produção. Não vale se é spike de uma hora. A pergunta não é "quanto custa escrever a spec", é "quanto custa cada divergência que você vai corrigir em PR depois".
Conclusão
A constatação que tirei desse experimento foi simples e meio incômoda. A discussão "qual modelo é melhor pra coding" virou ruído. Quando seis modelos estão a 0,8 ponto no SWE-bench, o ganho marginal de trocar um pelo outro é menor do que o ganho de escrever uma spec decente.
O dev sênior em 2026 não está escrevendo menos código por causa de IA. Está escrevendo mais spec. Spec que sobrevive a qualquer modelo, que vira teste, que documenta intenção. Isso é engenharia. O resto é só esperar o agente parar de inventar.
Próximo experimento que quero fazer: pegar a mesma spec rica e medir quanto ela degrada quando o agente roda em loop por 30 minutos sem supervisão. Suspeita: a fronteira não está em qual modelo, está em quanto contexto a spec consegue ancorar antes do drift começar.
{AI Engineer} — apaixonado por Laravel, arquitetura de software e construir produtos com impacto. Compartilho aqui tutoriais, descobertas e reflexões sobre o dia a dia de engenharia.