Todo time que começa a usar LLMs para geração de código enfrenta, cedo ou tarde, o mesmo problema: o código gerado parece correto, passa nos testes automatizados, não dispara nenhum lint, e vai para produção. Semanas depois, um bug aparece que nenhuma ferramenta teria encontrado — porque o código resolve o problema errado com elegância, ou porque faz uma assunção sobre o comportamento do sistema que era falsa desde o início, ou porque contém uma vulnerabilidade de segurança sutil que só aparece sob condições específicas de carga. A pergunta não é "o LLM gerou código ruim?" — a pergunta é "o revisor humano fez as perguntas certas antes de aprovar?"
Este conceito é sobre o processo estruturado de revisão de código gerado por IA. Não é diferente de revisar código humano na substância — os princípios de boa revisão são os mesmos — mas é diferente no risco porque código gerado por LLM tem padrões de falha específicos que code review humano não costuma encontrar por acidente. Code review humano-a-humano frequentemente encontra problemas porque o revisor conhece o domínio e faz perguntas naturais de domínio. Code gerado por LLM parece resolver o domínio e pode iludir essas perguntas — a superfície sintática está correta, mas a semântica de domínio pode estar errada. Saber identificar isso sistematicamente é a habilidade deste conceito.
O problema "plausível mas errado"
LLMs são otimizados para plausibilidade linguística: a próxima sequência de tokens que é mais provável dado o contexto. Código plausível é código que parece correto, que compila, que segue convenções, que tem naming consistente, que tem a estrutura que se esperaria de um desenvolvedor experiente. Mas plausibilidade não é equivalente a correção — e a distinção é especialmente perigosa porque o cérebro humano usa os mesmos sinais de plausibilidade para avaliar correção superficialmente.
O erro mais comum ao revisar código gerado por LLM é começar pela leitura do código. O revisor lê o código, reconhece os padrões, acha que parece bem, e aprova. Este processo está invertido: você deve começar pelos requisitos e perguntar se o código os satisfaz, não pelo código e perguntar se parece bom. A distinção importa porque código plausível ativa o viés de confirmação — você procura evidência de que está certo, não de que está errado.
Revise código gerado por IA a partir dos requisitos, não a partir do código. Pergunte primeiro o que o código deveria fazer, depois verifique se o código faz isso. Não o inverso.
Isso significa que antes de abrir o diff, você deve ter em mãos: os requisitos funcionais que o código deveria satisfazer, os casos de borda relevantes para o domínio, as invariantes de negócio que devem ser preservadas, e os contratos com sistemas externos que o código toca. Com isso em mãos, a revisão começa pela verificação de que o código satisfaz cada item — não pela leitura livre do código tentando encontrar problemas.
Padrões de falha específicos de LLMs
Código gerado por LLM tem padrões de falha distintos de código gerado por humanos. Humanos falham por falta de conhecimento, por descuido, por cansaço, por má compreensão de um requisito específico. LLMs falham de formas diferentes, e o revisor precisa saber o que procurar especificamente.
Assunção silenciosa sobre o contexto
LLMs completam o contexto do prompt com conhecimento do pre-training. Se o prompt não é explícito sobre um detalhe do sistema, o modelo assume o default mais comum na literatura que aprendeu. Isso cria assunções silenciosas: que a fila de mensagens entrega pelo menos uma vez (a mais comum), que o banco de dados é SQL padrão, que o timezone da aplicação é UTC, que o serviço externo retorna a estrutura documentada na especificação pública.
Em produção, essas assunções frequentemente não valem: a fila entrega exatamente uma vez e o código não precisa ser idempotente, o banco tem extensões específicas, o timezone é local por razão histórica, o serviço externo tem uma versão legada com esquema diferente. Nenhuma dessas assunções aparece no código como erro — elas aparecem como comportamento incorreto em condições específicas.
Checklist: para cada integração com sistema externo que o código toca, pergunte explicitamente "qual é a assunção que o código faz sobre o comportamento deste sistema? Essa assunção é documentada? É verdadeira no nosso ambiente específico?"
Cópia de padrão sem transferência de invariante
LLMs são muito bons em reconhecer e aplicar padrões estruturais — o padrão Repository, o padrão Factory, o padrão Saga para transações distribuídas. O problema é que padrões vêm com invariantes: o padrão Saga pressupõe que cada passo tem uma ação compensatória, o padrão Repository pressupõe que operações de leitura e escrita seguem as mesmas regras de acesso, o padrão Observer pressupõe que a ordem dos observers não importa.
LLMs aplicam o padrão estrutural mas frequentemente não transferem a invariante para o contexto específico. O resultado é código que tem a forma do padrão mas não satisfaz a invariante: uma Saga onde um passo não tem ação compensatória definida, um Repository que tem lógica de negócio que deveria estar no domínio, um Observer onde a ordem de execução importa para a correção mas não está garantida.
Checklist: para cada padrão de design identificado no código gerado, liste as invariantes que o padrão pressupõe e verifique que o código as satisfaz explicitamente.
Completude de caminho feliz sem completude de caminho de falha
Como documentado no conceito de geração de testes, LLMs são muito mais capazes de gerar o caminho feliz correto do que os caminhos de falha corretos. O mesmo vale para o código em si: a implementação do fluxo principal costuma ser boa, mas o tratamento de erro tem frequentemente problemas sutis.
Problemas comuns: erros que são capturados mas não propagados corretamente, retentativas sem backoff exponencial, timeouts que não estão configurados para o contexto correto (um timeout adequado para um serviço interno não é adequado para uma chamada cross-região), recursos que são alocados antes de um ponto de falha e não liberados no catch block, logging que acontece mas não inclui o contexto necessário para diagnóstico.
Checklist: para cada operação de I/O no código gerado, trace o caminho de falha: o que acontece se esta operação falha? O erro é propagado? Os recursos são liberados? O estado do sistema fica consistente? O log tem informação suficiente para diagnóstico?
Dependência desnecessária
LLMs tendem a introduzir dependências que conheceram durante o pre-training e associam com o problema sendo resolvido, mesmo quando a dependência não é necessária ou quando uma solução mais simples existe na stdlib. Isso acontece especialmente para problemas comuns: parsing de JSON, manipulação de datas, validação de inputs, hashing.
Cada dependência introduzida tem custo: custo de manutenção, custo de atualização quando há vulnerabilidade, superfície de ataque adicional, possível conflito de licença, carga de transitividade na build. Um LLM que introduz uma dependência de 50KB para fazer algo que a stdlib faz em 10 linhas não está sendo econômico — está sendo familiar com o que aprendeu.
Checklist: para cada nova dependência no código gerado, pergunte: a stdlib resolve isso? Uma dependência já existente no projeto resolve isso? Se uma nova dependência é necessária, ela tem histórico de manutenção ativo, licença compatível, e tamanho razoável?
Abstração no nível errado
Sem contexto do histórico e direção do projeto, LLMs escolhem nível de abstração por heurísticas: código que parece complexo demais recebe abstração, código que parece simples permanece simples. Mas "complexo" e "simples" não são propriedades objetivas — são relativas ao contexto e direção do projeto. Uma função de 50 linhas que faz exatamente o que precisa ser feito pode ser preferível a uma abstração de 3 classes que generaliza o problema errado.
O problema inverso também existe: LLMs podem gerar código sem abstração onde o projeto já tem padrões estabelecidos. Se o projeto usa um padrão específico para acessar configuração, e o código gerado acessa a configuração diretamente, a inconsistência cria dívida técnica.
Checklist: o nível de abstração do código gerado é consistente com o restante da base? O código usa os padrões estabelecidos no projeto (acesso a config, logging, error handling) ou introduz alternativas inconsistentes?
O checklist estruturado do revisor
Um checklist estruturado evita que a revisão dependa do que o revisor lembra de verificar em um dia específico. Revisão de código gerado por IA deve ser pelo menos tão estruturada quanto revisão de código humano — e idealmente mais, porque os padrões de falha específicos de LLM precisam ser verificados ativamente.
Categoria 1: Correção funcional
- Satisfaz os requisitos formais? Para cada requisito listado no prompt ou na especificação, existe comportamento verificável no código que o satisfaz?
- Cobre os casos de borda de domínio? Não os casos genéricos (null, lista vazia, string vazia), mas os casos específicos do domínio: o que acontece quando um pedido é cancelado após o pagamento ter sido processado? O que acontece quando dois usuários tentam reservar o último item simultaneamente?
- Preserva as invariantes de negócio? As invariantes que existem no sistema — um usuário ativo sempre tem email válido, um pedido confirmado sempre tem item pelo menos um item — são preservadas por todas as operações no código gerado?
- Os contratos externos estão corretos? Para cada integração, os payloads, headers, e erros esperados estão corretos para a versão específica da API em uso (não a documentação pública geral)?
Categoria 2: Robustez
- Caminhos de falha completos? Cada operação que pode falhar tem tratamento de erro? Os recursos são liberados? O estado fica consistente?
- Timeouts configurados? Chamadas a sistemas externos têm timeout? O valor do timeout é adequado para o contexto (interno vs. externo, síncrono vs. assíncrono)?
- Retentativas com backoff? Se há retentativa, há backoff exponencial com jitter? A operação é idempotente para retentativa ser segura?
- Limites de recursos? Operações em coleções têm limite? Paginação está implementada onde necessário? Existe proteção contra input malicioso que causaria consumo de memória desproporcional?
Categoria 3: Segurança
- Validação de input na fronteira? Todo input de origem externa (usuário, API, fila de mensagens, arquivo) é validado antes de uso?
- Dados sensíveis tratados corretamente? PII, credenciais, e tokens não aparecem em logs, não são persistidos sem criptografia, não são retornados em respostas desnecessariamente?
- Controle de acesso verificado? O código verifica que o usuário tem permissão para a operação sendo executada, não apenas que está autenticado?
- Dependências têm vulnerabilidades conhecidas? Novas dependências introduzidas pelo código gerado foram verificadas contra bases de CVE?
- Injeção SQL/NoSQL/command? Queries usam parâmetros, não concatenação. Comandos de shell, se existirem, não interpolam input de usuário.
Categoria 4: Manutenibilidade
- Consistência com a base de código? Naming, estrutura de arquivo, padrões de acesso a infraestrutura, tratamento de erro — tudo é consistente com o restante do projeto?
- Abstrações no nível correto? O código não é nem sub-abstraído (código duplicado que deveria estar centralizado) nem super-abstraído (generalização prematura)?
- Dependências justificadas? Novas dependências são necessárias? São mantidas ativamente? Têm licença compatível?
- O código pode ser entendido sem o contexto do prompt? Um desenvolvedor que não participou da geração consegue entender o propósito e funcionamento do código?
Categoria 5: Testabilidade
- Os testes cobrem casos de domínio? Não apenas os casos que um LLM geraria (happy path, null, empty), mas os casos específicos do domínio que um engenheiro que conhece o negócio escreveria?
- Os testes falham por razões certas? Se você quebrar a implementação de uma forma específica, o teste correspondente falha? (Teste de mutation verifica isso sistematicamente.)
- Os testes são determinísticos? Testes que dependem de tempo, de ordem de execução, ou de estado compartilhado são fontes de flakiness que corroem a confiança na suíte.
- Os testes de integração testam o contrato real? Mocks que não refletem o comportamento real do sistema mockado têm valor negativo — passam quando o sistema real falharia.
Usar o mesmo LLM para gerar o código e para revisar o código sem intervenção humana. O modelo tende a validar suas próprias saídas — ele achará plausível o que gerou porque aplicou os mesmos padrões na geração e na revisão. A revisão por LLM só agrega valor quando complementa, não substitui, a revisão humana.
A ilusão de cobertura de testes
Coverage é a métrica de qualidade mais citada e uma das mais enganosas quando o foco é código gerado por LLM. Alta cobertura de linha ou branch significa que as linhas foram executadas — não que as linhas fazem o que deveriam fazer. Especificamente para código gerado por LLM, onde os testes frequentemente também são gerados pelo LLM, coverage mede a qualidade da geração, não a qualidade da implementação.
O problema fundamental: quando um LLM gera código e depois gera testes para esse código, os testes tendem a testar o comportamento que o código tem, não o comportamento que o código deveria ter. Se o código implementa a lógica errada, os testes verificarão que a lógica errada está presente. 100% de cobertura com lógica errada é pior do que 60% de cobertura com lógica correta — porque cria falsa confiança.
Mutation testing como verificação real
Mutation testing insere bugs sintéticos no código — inverte uma condição, muda um operador, remove uma linha — e verifica se os testes falham. Se os testes passam com o bug sintético, ou bem os testes não cobrem aquele caminho, ou bem eles cobrem mas não verificam o comportamento esperado.
Para código gerado por LLM, mutation testing revela uma categoria específica de problema: testes que executam o código mas não fazem assertions sobre o comportamento relevante. Um LLM que gera um teste pode chamar a função e verificar que não lança exceção sem verificar que o resultado está correto. O teste passa, a coverage aumenta, mas o comportamento não é verificado.
Ferramentas de mutation testing: Stryker para C#/.NET e JavaScript, mutmut ou mutpy para Python, go-mutesting para Go. Não é necessário rodar mutation testing em cada PR — é mais eficaz rodar em áreas críticas de negócio e quando há suspeita de que a suíte de testes pode estar superficial.
O teste que o LLM não escreve
Como documentado no conceito 05, LLMs são consistentemente fracos em um conjunto específico de testes: testes que verificam invariantes de negócio que não estão explícitas no código, testes de comportamento na ausência de algo (o sistema não deve fazer X quando Y), e testes de falhas específicas do domínio.
Para avaliar se uma suíte de testes gerada por LLM é adequada, pergunte: quem escreveu os casos de teste que especificam o comportamento de negócio correto? Se a resposta é "o LLM", os testes provavelmente verificam que o código funciona, não que o sistema se comporta corretamente do ponto de vista de negócio. Alguém com conhecimento do domínio precisa ter escrito, revisado, ou pelo menos aprovado explicitamente os casos de teste de domínio.
Revisão de segurança: o que linters não veem
Code review de segurança em código gerado por LLM requer atenção especial a categorias que ferramentas automáticas não detectam bem: problemas de lógica de autorização, problemas de design de autenticação, e vulnerabilidades que emergem da interação entre componentes.
Autorização: verificação de permissão vs. presença de token
Um erro comum em código gerado por LLM: o código verifica que o usuário está autenticado (tem um token válido) mas não verifica que o usuário tem permissão para a operação específica. Isso é especialmente comum em endpoints de consulta: "se o token é válido, retorne os dados" sem verificar se o usuário que detém o token tem direito de ver aqueles dados específicos.
A distinção entre autenticação e autorização é familiar para qualquer engenheiro sênior, mas um LLM que não tem o contexto do modelo de permissão do sistema pode implementar autenticação correta e autorização incorreta porque a autenticação é mais visível no código existente que serviu de contexto.
Checklist de segurança específico: para cada endpoint ou operação no código gerado, trace quem pode chamar isso, com quais dados. Existe verificação de que o chamador tem permissão de operar sobre o recurso específico sendo acessado? Ou apenas verificação de identidade?
Exposure de dados em respostas e logs
LLMs que geram serialização de objetos tendem a serializar mais do que o necessário — o objeto completo, incluindo campos que não deveriam sair da API. Isso cria data exposure: a API retorna campos internos (IDs de banco, timestamps de auditoria, dados de outros usuários em objetos relacionados) que não deveriam estar expostos.
Igualmente, código de logging gerado por LLM frequentemente inclui o objeto completo para facilitar debugging, sem considerar que o objeto pode conter PII. Um log de "processando pedido: {order}" onde order tem dados pessoais é uma violação de LGPD que passou em code review porque o código "parecia útil para debugging".
Cryptographic mistakes
LLMs cometem erros previsíveis em criptografia: usar MD5 ou SHA1 para hashing de senha (algoritmos de propósito geral, não adequados para senha), usar modo ECB em AES (modo que não tem randomização por bloco e vaza padrões), usar IVs estáticos ou previsíveis, não usar salt em hashes, implementar comparação de strings ao invés de comparação em tempo constante para tokens.
Esses erros são conhecidos na literatura de segurança mas LLMs os cometem porque viram código histórico com esses padrões durante o pre-training. Para qualquer código que toca criptografia, a revisão deve verificar especificamente: algoritmo adequado para o caso de uso, modo de operação correto, randomização adequada, e comparação em tempo constante onde necessário.
Rastreabilidade: saber o que foi gerado
Uma dimensão menos óbvia de avaliar código gerado por IA é a questão de rastreabilidade: quando um bug aparece em produção daqui a seis meses, é possível saber o que foi gerado por LLM, com qual prompt, e com qual contexto? Sem rastreabilidade, é impossível auditar sistematicamente — ou aprender com os bugs que aparecem.
O problema da atribuição
Git blame mostra quem commitou o código, não quem o escreveu. Quando todo o time usa LLMs, git blame torna-se menos útil para entender a origem de uma decisão — o commit é do desenvolvedor, mas a implementação pode ser de Claude, de Cursor, de Copilot. Isso importa quando:
- Você precisa entender por que uma decisão foi tomada (o LLM pode ter feito uma assunção que o desenvolvedor não percebeu)
- Você está auditando por vulnerabilidades e quer saber quais partes foram geradas automaticamente
- Você está treinando ou avaliando o LLM e precisa de exemplos de código aceito vs. rejeitado
- Regulamentação exige auditoria de código (financeiro, saúde, segurança funcional)
Estratégias de rastreabilidade
A estratégia mais simples é uma convenção de mensagem de commit: commits que são substancialmente gerados por LLM incluem uma tag, por exemplo [gen: claude] ou Co-authored-by: Claude. Isso permite filtrar por origem via git log sem precisar de ferramentas externas.
Para rastreabilidade mais granular — nível de arquivo ou função — algumas equipes mantêm um arquivo de metadados (por exemplo .ai-generated.json) que lista arquivos ou seções gerados automaticamente com referência ao prompt que os originou. Isso adiciona overhead de manutenção mas permite auditoria precisa.
Para regulamentação ou compliance que exige rastreabilidade formal, ferramentas como Copilot for Business incluem logs de geração que podem ser integrados ao sistema de auditoria. A decisão de nível de rastreabilidade necessário deve ser feita no início da adoção, não depois que o primeiro incidente acontece.
Defina uma convenção de rastreabilidade antes de escalar uso de LLMs no time. Retroativamente adicionar rastreabilidade a um repositório com histórico misto é impraticável. Começa simples — uma tag de commit é suficiente — e escala se houver necessidade regulatória.
Quando rejeitar e pedir regeneração vs. editar diretamente
Um momento crítico no fluxo de trabalho com LLM é a decisão: quando o código gerado tem problemas, qual é a resposta correta? Editar o código gerado diretamente, pedir ao LLM que corrija, ou rejeitar e gerar novamente com um prompt melhor?
Edite diretamente quando
O problema é localizado e você entende a correção: um nome de variável confuso, um caso de borda específico que você sabe resolver, uma dependência desnecessária que você quer remover. Editar diretamente é mais rápido e preserva o controle. O risco de pedir ao LLM para corrigir é que ele pode introduzir problemas novos ao corrigir o problema original — especialmente se a correção envolve mudanças em múltiplas partes do código.
Peça regeneração quando
O problema é estrutural — o código implementa a abordagem errada, não apenas tem um erro específico. Editar código com estrutura errada é mais trabalho do que regenerar com um prompt melhor. Mas a regeneração deve vir acompanhada de um prompt melhorado que explica o que estava errado e por que a abordagem anterior não funcionou.
Regeneração sem aprendizado do prompt repete o mesmo erro. O ciclo correto: identificar o problema → entender por que o prompt levou a esse problema (falta de contexto? instrução ambígua? restrição não especificada?) → melhorar o prompt → regenerar. Isso transforma o erro em aprendizado de prompting.
Rejeite completamente quando
O código gerado revela que o LLM não tem o contexto necessário para gerar uma solução adequada — não porque o prompt foi ruim, mas porque o problema requer conhecimento do sistema que não cabe em um prompt razoável. Neste caso, LLM para geração é a ferramenta errada para este problema específico. Pode ser adequado para partes isoladas (gerar os testes, gerar a documentação, gerar o boilerplate) mas não para a implementação central.
Reconhecer quando parar de tentar com LLM é parte da habilidade. O custo de iteração em prompts para um problema que o LLM fundamentalmente não consegue resolver é mais alto do que implementar sem assistência de IA.
A responsabilidade não migra
O ponto mais importante deste conceito não é técnico — é organizacional. Em qualquer regime de responsabilidade profissional de software, a responsabilidade pelo código que vai para produção pertence ao engenheiro que o commitou, não à ferramenta que o gerou. Isso não é apenas uma posição legal — é uma posição de engenharia: o engenheiro é quem tem o contexto completo do sistema, das invariantes de negócio, das restrições operacionais, e dos riscos específicos do domínio. Nenhum LLM tem esse contexto completo.
Isso significa que "o LLM gerou" não é uma explicação aceitável para um bug em produção — da mesma forma que "o stackoverflow sugeriu" não era aceitável antes. O engenheiro que aceitou o código é responsável por ter verificado que o código estava correto para o contexto específico. A velocidade que LLMs proporcionam não reduz essa responsabilidade — ela aumenta a necessidade de processo estruturado de revisão porque a velocidade de geração pode superar a velocidade de revisão cuidadosa.
"O LLM gerou esse código" não é mitigação de responsabilidade — é confissão de que o código não foi adequadamente revisado antes de ir para produção. A velocidade de geração não é desculpa para processo de revisão superficial.
Equipes que percebem melhora na velocidade com LLMs sem degradação de qualidade são equipes que estabeleceram processos de revisão robustos que escalam com a velocidade de geração. O investimento em processo de revisão não diminui quando se adota LLMs — ele aumenta, porque há mais código sendo gerado que precisa ser revisado. O ganho de velocidade aparece no diferencial entre a velocidade de geração (alta) e o custo do processo de revisão (também mais rápido que implementação manual, mas rigoroso).
Comparação por linguagem
<!-- .github/pull_request_template_ai_generated.md -->
## Código gerado por IA
**Modelo usado:** Claude 3.5 / GitHub Copilot / outro: ___
**Prompt resumido:** (ou link para prompt completo em notion/confluence)
### Correção funcional
- [ ] Requisitos satisfeitos: listei os requisitos e verifiquei que o código os satisfaz
- [ ] Casos de borda de domínio: identifiquei e verifiquei os casos específicos do negócio
- [ ] Invariantes preservadas: as invariantes do sistema continuam válidas após esta mudança
- [ ] Contratos externos: a versão da API externa usada está correta para nosso ambiente
### Robustez
- [ ] Tratamento de erro completo: todos os caminhos de falha têm tratamento explícito
- [ ] Timeouts configurados: chamadas externas têm timeout adequado ao contexto
- [ ] Recursos liberados: conexões, streams, locks liberados mesmo em caso de exceção
- [ ] Limites de input: operações em coleções têm limite; sem DoS por input grande
### Segurança
- [ ] Autorização verificada: não apenas autenticação, mas permissão para o recurso específico
- [ ] PII não exposta: dados sensíveis não aparecem em logs ou respostas desnecessariamente
- [ ] Criptografia correta: algoritmo e modo adequados (não MD5 para senha, não AES-ECB)
- [ ] Dependências verificadas: novas dependências sem CVE conhecida
### Qualidade
- [ ] Consistente com a base: naming, padrões, convenções do projeto
- [ ] Dependências justificadas: nova dependência é necessária e não duplica algo existente
- [ ] Testes de domínio: testes cobrem casos de negócio (não apenas happy path gerado por LLM)
### Rastreabilidade
- [ ] Commit marcado: mensagem inclui [gen: claude] ou Co-authored-by conforme convenção do time
# scripts/validate_ai_tests.py
# Valida testes gerados por LLM usando mutation testing.
# Uso: python scripts/validate_ai_tests.py src/payments/ tests/test_payments.py
import subprocess
import sys
import json
from pathlib import Path
def run_mutmut(source_dir: str, test_file: str) -> dict:
"""Roda mutation testing e retorna relatório estruturado."""
result = subprocess.run(
["mutmut", "run", "--paths-to-mutate", source_dir,
"--tests-dir", str(Path(test_file).parent)],
capture_output=True, text=True, timeout=300
)
# Coleta resultados
results_result = subprocess.run(
["mutmut", "results"],
capture_output=True, text=True
)
return parse_mutmut_output(results_result.stdout)
def parse_mutmut_output(output: str) -> dict:
survived = []
killed = []
current_section = None
for line in output.splitlines():
if "Survived" in line:
current_section = "survived"
elif "Killed" in line:
current_section = "killed"
elif line.strip().startswith("#"):
mutant_id = line.strip()
if current_section == "survived":
survived.append(mutant_id)
elif current_section == "killed":
killed.append(mutant_id)
total = len(survived) + len(killed)
kill_rate = len(killed) / total if total > 0 else 0
return {
"total_mutants": total,
"killed": len(killed),
"survived": len(survived),
"kill_rate": round(kill_rate * 100, 1),
"survived_ids": survived
}
def generate_review(results: dict, threshold: float = 70.0) -> str:
lines = [
f"=== Mutation Testing Report ===",
f"Total mutants: {results['total_mutants']}",
f"Killed: {results['killed']}",
f"Survived: {results['survived']}",
f"Kill rate: {results['kill_rate']}%",
f"Threshold: {threshold}%",
""
]
if results['kill_rate'] >= threshold:
lines.append("✅ PASS: Kill rate acima do threshold.")
lines.append(" Testes gerados detectam a maioria das mutações.")
else:
lines.append("❌ FAIL: Kill rate abaixo do threshold.")
lines.append(" Testes gerados por LLM provavelmente estão incompletos.")
lines.append(" Adicione testes de domínio manualmente para:")
for mutant_id in results['survived_ids'][:10]:
lines.append(f" - Mutante sobrevivente: {mutant_id}")
if len(results['survived_ids']) > 10:
lines.append(f" ... e mais {len(results['survived_ids']) - 10} mutantes")
return "\n".join(lines)
if __name__ == "__main__":
source = sys.argv[1] if len(sys.argv) > 1 else "src/"
tests = sys.argv[2] if len(sys.argv) > 2 else "tests/"
threshold = float(sys.argv[3]) if len(sys.argv) > 3 else 70.0
print(f"Rodando mutation testing em {source} com testes de {tests}...")
results = run_mutmut(source, tests)
print(generate_review(results, threshold))
// cmd/ai-review/main.go
// Coleta código gerado e contexto do repositório para revisão arquitetural.
package main
import (
"context"
"flag"
"fmt"
"os"
"os/exec"
"strings"
"github.com/anthropics/anthropic-sdk-go"
"github.com/anthropics/anthropic-sdk-go/option"
)
type ReviewRequest struct {
GeneratedCode string
ProjectPatterns string
ExistingTests string
Invariants string
ReviewFocus string
}
func collectProjectContext() (string, error) {
// Coleta padrões existentes de error handling no projeto
out, err := exec.Command("grep", "-r", "--include=*.go", "-l",
"fmt.Errorf", ".", "--", "internal/").Output()
if err != nil {
return "", err
}
// Seleciona uma amostra representativa de arquivos para contexto
files := strings.Split(strings.TrimSpace(string(out)), "\n")
var patterns strings.Builder
for i, f := range files {
if i >= 3 { // limita contexto a 3 arquivos exemplo
break
}
content, err := os.ReadFile(f)
if err != nil {
continue
}
patterns.WriteString(fmt.Sprintf("=== %s ===\n%s\n\n", f, string(content)))
}
return patterns.String(), nil
}
func buildReviewPrompt(req ReviewRequest) string {
return fmt.Sprintf(`Você é um revisor de código Go sênior. Revise o código gerado por IA abaixo.
CONTEXTO DO PROJETO (padrões existentes):
%s
CÓDIGO GERADO PARA REVISÃO:
%s
FOCO DA REVISÃO:
%s
Para cada problema encontrado, forneça:
1. Categoria: [funcional|robustez|segurança|consistência|dependência]
2. Severidade: [crítico|importante|sugestão]
3. Localização: linha ou função específica
4. Problema: descrição clara e objetiva
5. Correção: o que deve ser mudado
Importante: não faça revisão genérica de boas práticas Go que o código claramente já segue.
Foque em: (a) inconsistências com os padrões do projeto, (b) assunções sobre o sistema que podem estar erradas,
(c) casos de borda de domínio que o código não cobre, (d) problemas de segurança específicos do contexto.
Finalize com: RECOMENDAÇÃO: [aprovar|aprovar com mudanças menores|rejeitar e regenerar]`,
req.ProjectPatterns, req.GeneratedCode, req.ReviewFocus)
}
func reviewCode(ctx context.Context, req ReviewRequest) (string, error) {
client := anthropic.NewClient(
option.WithAPIKey(os.Getenv("ANTHROPIC_API_KEY")),
)
msg, err := client.Messages.New(ctx, anthropic.MessageNewParams{
Model: anthropic.F(anthropic.ModelClaude3_5SonnetLatest),
MaxTokens: anthropic.F(int64(2000)),
Messages: anthropic.F([]anthropic.MessageParam{
anthropic.NewUserMessage(anthropic.NewTextBlock(buildReviewPrompt(req))),
}),
})
if err != nil {
return "", fmt.Errorf("chamada API falhou: %w", err)
}
if len(msg.Content) == 0 {
return "", fmt.Errorf("resposta vazia do modelo")
}
return msg.Content[0].Text, nil
}
func main() {
file := flag.String("file", "", "arquivo com código gerado por LLM")
focus := flag.String("focus", "consistência com o projeto, robustez, segurança",
"foco específico da revisão")
flag.Parse()
if *file == "" {
fmt.Fprintln(os.Stderr, "uso: ai-review -file [-focus ]")
os.Exit(1)
}
code, err := os.ReadFile(*file)
if err != nil {
fmt.Fprintf(os.Stderr, "erro lendo arquivo: %v\n", err)
os.Exit(1)
}
patterns, err := collectProjectContext()
if err != nil {
fmt.Fprintf(os.Stderr, "aviso: erro coletando contexto: %v\n", err)
}
req := ReviewRequest{
GeneratedCode: string(code),
ProjectPatterns: patterns,
ReviewFocus: *focus,
}
result, err := reviewCode(context.Background(), req)
if err != nil {
fmt.Fprintf(os.Stderr, "erro na revisão: %v\n", err)
os.Exit(1)
}
fmt.Println(result)
}
Síntese: a revisão como investimento
O valor de LLMs para geração de código só se realiza se a revisão é adequada. Código gerado rapidamente e revisado superficialmente não é mais rápido do que código gerado manualmente e revisado cuidadosamente — é mais rápido de gerar e mais lento de manter, mais lento de debugar, e potencialmente mais perigoso em produção. O cálculo correto não é "geração rápida menos tempo de revisão igual ganho total" — é "geração rápida com processo de revisão adequado igual a ganho real e sustentável".
O processo estruturado de revisão apresentado neste conceito — começar pelos requisitos, verificar categorias específicas, usar mutation testing para validar a suíte, rastrear o que foi gerado — é um investimento que protege o ganho de velocidade. Sem ele, o ganho de velocidade é ilusório: aparece no curto prazo e desaparece quando os bugs gerados a alta velocidade chegam em produção.
Um engenheiro sênior que usa LLMs com processo de revisão robusto entrega mais, mais rápido, com qualidade sustentável. Um engenheiro que usa LLMs sem processo entrega mais, mais rápido, com qualidade decrescente — e eventualmente passa mais tempo corrigindo problemas do que teria gastado gerando corretamente desde o início. A diferença está no investimento no processo de revisão.
Exercícios práticos
- Auditoria de código existente: Identifique um arquivo de código no seu projeto que foi substancialmente gerado por LLM. Aplique o checklist estruturado deste conceito sobre ele. Para cada categoria, documente: o que você verificou, o que encontrou (ou não encontrou), e o que adicionaria ao processo de revisão da sua equipe com base no que encontrou.
- Mutation testing na sua suíte: Escolha um módulo de negócio crítico do seu projeto (não utilitários, não infra — código que implementa regras de negócio). Configure e rode mutation testing (Stryker, mutmut, ou go-mutesting). Meça o kill rate. Se for abaixo de 70%, identifique quais mutantes sobrevivem e escreva os testes humanos que os matariam — especificamente os testes de domínio que um LLM não teria escrito.
- PR template de rastreabilidade: Crie um PR template específico para código gerado por LLM para o seu projeto. Inclua: identificação do modelo, referência ao prompt, e checklist de revisão adaptado ao contexto do seu domínio (com casos de borda específicos, não genéricos). Aplique em uma próxima PR que use geração por LLM e itere o template com base no que o processo revelou.
Referências e leitura complementar
- livro Software Engineering at Google — Winters, Manshreck e Wright (2020).
- artigo An Analysis of the Plausibility and Correctness of Code Generated by LLMs — Zheng et al. (2023).
- artigo Mutation Testing in the Wild — Petrović e Ivanković (Google, 2018).
- docs Stryker Mutator — Stryker (2024).
- docs mutmut — Python Mutation Testing — mutmut (2024).
- artigo Security Vulnerabilities in AI-Generated Code — Pearce et al. (2022).
- artigo Do Users Write More Insecure Code with AI Assistants? — Sandoval et al. (2022).
- artigo The "Thoughtful" AI Assistant — Attribution and Responsibility in AI-Assisted Coding — ACM FAccT (2024).
- livro Accelerate: The Science of Lean Software and DevOps — Forsgren, Humble e Kim (2018).
- artigo Code Review Quality: How Authors and Reviewers Agree to Disagree — Bosu e Carver (2013).
- docs OWASP Code Review Guide — OWASP.
- artigo ChatGPT Incorrectness Detection Is Hard: How Well Do LLMs Detect Their Own Mistakes? — Huang et al. (2023).