MÓDULO 13 · CONCEITO 10 DE 15

Avaliando Código Gerado por IA

O checklist do revisor: o que testar, o que inspecionar manualmente, e por que a responsabilidade nunca migra para o modelo

Tempo de leitura ~22 min Pré-requisito 09 · IA em Pipelines de CI/CD · 06 · Code Review com IA Próximo 11 · Riscos e Limites da IA em Engenharia

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.

princípio

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

Categoria 2: Robustez

Categoria 3: Segurança

Categoria 4: Manutenibilidade

Categoria 5: Testabilidade

anti-padrão

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:

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.

decisão de processo

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.

responsabilidade

"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

C# — Checklist como PR Template
Um PR template específico para código gerado por LLM, como checklist estruturado que o autor deve preencher antes de solicitar review.
<!-- .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
Python — Mutation Testing para Validar Testes Gerados por LLM
Script que roda mutation testing em um módulo específico e gera relatório sobre a qualidade dos testes gerados — identifica quais mutantes "sobrevivem" (não são detectados pelos testes).
# 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))
Go — Revisão Arquitetural com Prompt Estruturado
Script Go que coleta contexto arquitetural do repositório e submete código gerado por LLM para revisão focada em consistência com o projeto, não revisão genérica.
// 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

  1. 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.
  2. 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.
  3. 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

  1. livro Software Engineering at Google — Winters, Manshreck e Wright (2020). O capítulo sobre code review é a base do processo descrito neste conceito. A abordagem de revisar a partir dos requisitos, não do código, é derivada dos princípios de review do Google.
  2. artigo An Analysis of the Plausibility and Correctness of Code Generated by LLMs — Zheng et al. (2023). Arxiv. O paper que formaliza a distinção entre plausibilidade sintática e correção semântica em código gerado por LLM. Base empírica para o "problema plausível mas errado" deste conceito.
  3. artigo Mutation Testing in the Wild — Petrović e Ivanković (Google, 2018). A experiência do Google com mutation testing em larga escala. Contextualiza quando mutation testing tem ROI positivo e como usá-lo de forma seletiva (não em todo PR).
  4. docs Stryker Mutator — Stryker (2024). stryker-mutator.io — A ferramenta de mutation testing usada no exemplo C# do conceito. Suporta C#, JavaScript, e Scala. Documentação inclui configuração de thresholds e integração com CI.
  5. docs mutmut — Python Mutation Testing — mutmut (2024). mutmut.readthedocs.io — A ferramenta Python usada no script de validação. Mais simples que mutpy, com melhor integração com pytest.
  6. artigo Security Vulnerabilities in AI-Generated Code — Pearce et al. (2022). IEEE Symposium on Security and Privacy. Estudo sistemático de vulnerabilidades de segurança em código gerado por Copilot. Documenta que ~40% dos completions em contextos de segurança contêm vulnerabilidades — a base empírica para a seção de revisão de segurança deste conceito.
  7. artigo Do Users Write More Insecure Code with AI Assistants? — Sandoval et al. (2022). ACM CCS. Experimento controlado comparando código de segurança escrito com e sem assistência de LLM. Resultado: usuários com LLM produzem código de segurança de qualidade similar ou ligeiramente pior, mas se sentem mais confiantes. A combinação de confiança aumentada e qualidade similar-ou-pior é especialmente perigosa.
  8. artigo The "Thoughtful" AI Assistant — Attribution and Responsibility in AI-Assisted Coding — ACM FAccT (2024). Análise legal e ética de responsabilidade em código gerado por IA. Contextualiza a posição "a responsabilidade não migra" deste conceito dentro de frameworks de responsabilidade de engenharia de software.
  9. livro Accelerate: The Science of Lean Software and DevOps — Forsgren, Humble e Kim (2018). As métricas DORA para medir se a adoção de LLMs está realmente acelerando delivery ou apenas criando ilusão de velocidade com dívida técnica crescente.
  10. artigo Code Review Quality: How Authors and Reviewers Agree to Disagree — Bosu e Carver (2013). Pesquisa sobre o que torna code review efetivo. Contextualiza por que review estruturado (checklist) é mais consistente do que review informal — e por que isso é especialmente importante com código gerado por LLM onde os padrões de falha são menos familiares.
  11. docs OWASP Code Review Guide — OWASP. owasp.org/www-project-code-review-guide — O guia de referência para revisão de segurança. As categorias de segurança do checklist deste conceito são derivadas das categorias OWASP.
  12. artigo ChatGPT Incorrectness Detection Is Hard: How Well Do LLMs Detect Their Own Mistakes? — Huang et al. (2023). Arxiv. O paper empírico por trás do anti-padrão "usar o mesmo LLM para gerar e revisar". LLMs são sistematicamente piores em detectar seus próprios erros do que erros de outros — contexto para a regra de não usar auto-revisão sem humano no loop.