Code review por LLM é uma das aplicações mais imediatamente úteis e uma das mais fáceis de usar incorretamente. A utilidade vem da velocidade: um LLM revisita código em segundos, identifica padrões problemáticos conhecidos, e produz feedback estruturado sem custo de tempo de um engenheiro sênior. O erro de uso vem de confundir essa capacidade com substituição da revisão por pares — que resolve categorias de problemas radicalmente diferentes e que o LLM não alcança.
O engenheiro que entende a divisão correta de trabalho entre revisão por LLM e revisão humana ganha em dois eixos: usa LLM para uma primeira passagem rápida que captura o que captura bem (style, bugs óbvios, segurança comum), e libera a atenção do par humano para o que só ele pode fazer (impacto sistêmico, alinhamento com arquitetura do sistema, invariantes de domínio, legibilidade em contexto do time). A soma é mais forte do que qualquer um dos dois sozinho.
Este conceito mapeia com precisão o que cada tipo de revisão cobre — não como lista abstrata, mas com exemplos concretos de quais problemas cada um encontra e qual não encontra. E cobre como estruturar prompts de revisão por LLM para que o output seja acionável e focado no que importa, não uma lista genérica de observações sem ordem de prioridade.
O que LLMs encontram em code review
Bugs óbvios e casos de borda não tratados. Off-by-one errors em loops, null reference sem verificação, divisão por zero potencial, comparação de strings com == em vez de .equals() em Java, early return que pula lógica importante — bugs estruturais que são visíveis no texto do código sem precisar de contexto de runtime. LLMs são bons nisso porque esses padrões aparecem frequentemente no corpus de treinamento com identificação explícita de que são bugs.
Violações de estilo e convenção. Naming inconsistente, funções longas demais, duplicação de código, comentários desatualizados, imports não-utilizados, trailing whitespace — o tipo de feedback que linters automatizados cobrem, mais casos onde o problema é semântico (nome que não comunica a intenção) em vez de sintático. Para convenções específicas do projeto, o LLM precisa que o CLAUDE.md ou o prompt especifique — sem isso, usa convenções genéricas da linguagem.
Vulnerabilidades de segurança conhecidas. SQL injection via concatenação de string, XSS em output não-escaped, secrets hardcoded no código, uso de algoritmos de hash inseguros (MD5 para senhas), configuração de CORS permissiva demais, deserialização de input não-validado. O LLM reconhece esses padrões do OWASP Top 10 e similares que aparecem extensivamente documentados no corpus de treinamento.
Problemas de performance local. N+1 queries visíveis no código (loop com chamada de banco dentro), alocação desnecessária em hot path, regex compilada dentro de loop, conversão de tipo redundante. "Local" é a palavra-chave — o LLM vê o código do arquivo, não o sistema inteiro. Performance não-local (o comportamento desse código quando chamado concorrentemente por 1000 threads) não é visível no texto.
Falta de tratamento de erro. Exceptions capturadas e silenciadas, erros ignorados (em Go, _, err := func() sem verificar err), recursos não fechados em caso de falha, timeout não configurado em chamada de rede. Esses padrões são identificáveis no código e o LLM os captura razoavelmente bem.
O que LLMs não encontram — e por que importa saber
Violações de invariante de negócio. "Um pedido cancelado não pode ser reaberto" é uma regra de negócio. Se o código de um endpoint permite reabrir pedidos cancelados, o LLM não vai identificar isso como problema — a menos que a invariante esteja explícita no contexto do prompt. Invariantes de negócio vivem no conhecimento do time, não no texto do código. Essa é a categoria de bug mais cara — chega à produção exatamente porque parece correto para quem não conhece a regra.
Impacto sistêmico de mudanças locais. Uma mudança em como um campo é serializado parece inócua localmente. Se outros três serviços consumem esse campo e esperam o formato anterior, a mudança quebra a integração. O LLM só vê o arquivo sendo revisado, não os consumidores. Code review humano com conhecimento de quem usa esse contrato é insubstituível para mudanças de interface.
Problemas de concorrência não-locais. Race conditions que emergem quando múltiplas instâncias do serviço rodam simultaneamente, problemas de ordering de eventos em sistemas assíncronos, deadlocks que só aparecem sob carga específica — são comportamentos emergentes do sistema, não visíveis no texto de um único arquivo.
Legibilidade para o time específico. Um LLM avalia legibilidade em termos genéricos da linguagem. O que "legível" significa para um time específico — quais abstrações são conhecidas, qual vocabulário de domínio é compartilhado, quais padrões são idiomáticos nessa base — o LLM não sabe sem contexto explícito extensivo. O par humano que conhece a base sabe imediatamente o que vai confundir um colega que lê o código às 2h da manhã durante um incidente.
Alinhamento com decisões arquiteturais não documentadas. Times tomam decisões arquiteturais que nem sempre estão em ADRs: "usamos sempre Mediator para handlers, não injeção direta de repositório", "queries analíticas vão para a réplica, nunca para o primary", "erros de validação de entrada são sempre 422, não 400". Código que viola essas decisões parece correto isoladamente mas diverge do sistema. O LLM não tem acesso a essas decisões implícitas.
LLM como primeira passagem de revisão: captura bugs óbvios, style, segurança comum, missing error handling. Par humano como segunda passagem: invariantes de domínio, impacto sistêmico, concorrência, legibilidade para o time, alinhamento arquitetural. Usar LLM na segunda passagem (depois de humano revisar) também tem valor: o humano pode ter perdido um bug de segurança que o LLM teria capturado. As passagens são complementares, não alternativas.
Como estruturar prompts de revisão
A diferença entre um prompt de revisão que produz feedback útil e um que produz lista genérica está na especificidade do que é pedido para verificar. "Revise esse código" produz observações da mesma qualidade que um linter automático. "Revise esse handler de pagamentos especificamente para os riscos listados abaixo" produz análise focada.
A estrutura eficaz de prompt de revisão tem três partes: o contexto de execução (em que ambiente esse código vai rodar, quais são as condições de stress, quais são as dependências externas), as dimensões específicas de análise (o que você quer que o LLM procure especificamente), e o formato de output desejado (lista priorizada por severidade, agrupada por categoria, ou narrativa).
Para contexto de execução: "esse handler vai rodar em 10 instâncias simultâneas com Postgres em READ COMMITTED, Redis para cache com TTL de 60s, e chamada para serviço externo de pagamentos com latência variável entre 50ms e 2s" é informação que muda completamente o que o LLM vai procurar em relação a "revise esse handler".
Para dimensões específicas: em vez de pedir revisão genérica, especifique as categorias: correção (o código faz o que deveria?), segurança (há riscos de exposição de dados ou vulnerabilidade?), concorrência (há race conditions visíveis no código?), tratamento de erro (todos os erros de dependência externa são tratados explicitamente?), e legibilidade (o naming e a estrutura comunicam a intenção claramente?). Categorias separadas permitem ao LLM focar em uma dimensão de cada vez em vez de produzir output misturado.
Para formato de output: pedir que o LLM priorize os problemas encontrados por severidade (crítico, alto, médio, baixo) e separe fatos de opiniões ("esse código tem race condition em X" vs "consideraria extrair Y em método separado") torna o output acionável. Sem essa instrução, o LLM frequentemente lista observações de design na mesma formatação que bugs críticos, o que dificulta a triagem.
// Prompt estruturado para revisão de handler de pagamentos:
//
// Revise o handler abaixo. Contexto de execução:
// - 5 instâncias do serviço em paralelo
// - Postgres 16, READ COMMITTED por padrão
// - IPaymentGateway: latência 100ms-3s, retorna erro ou sucesso
// - IOrderRepository: EF Core 9, sem retry automático
//
// Analise em ordem de prioridade:
//
// [CRÍTICO] Consistency: se o gateway retornar sucesso mas
// o SaveChanges falhar, o cliente foi cobrado mas o pedido
// não foi atualizado. Como isso é tratado?
//
// [ALTO] Concorrência: dois pagamentos simultâneos do mesmo
// pedido podem criar double-charge? Há lock ou idempotência?
//
// [ALTO] Segurança: algum dado sensível (número de cartão,
// CVV, valores internos) pode aparecer em logs ou resposta?
//
// [MÉDIO] Tratamento de erro: há caminhos onde exception
// não-tratada pode vazar para o cliente com stack trace?
//
// [BAIXO] Style: naming e estrutura seguem as convenções
// vistas em outros handlers do projeto?
//
// Para cada problema encontrado: linha aproximada,
// descrição do risco, e sugestão de correção.
// Se não encontrar problema em alguma categoria, diga
// explicitamente "não encontrado" — não omita a categoria.
Pedir "não encontrado" explicitamente quando a categoria não tem problemas evita que o LLM pule categorias onde encontrou algo sutil — a ausência de menção é ambígua.
# Passagem 1 — Revisão de correção:
"""
Revise o código abaixo APENAS para problemas de correção.
Não comente sobre style, naming, ou estrutura ainda.
Foco exclusivo:
- O código faz o que o docstring diz que deve fazer?
- Há casos de entrada válida que retornam resultado errado?
- Há casos de entrada inválida que não levantam o erro correto?
- Há condição de corrida se chamado concorrentemente?
- Há recurso que pode não ser liberado em caso de erro?
Contexto: função chamada por múltiplos workers simultâneos,
inputs vêm de fila SQS (podem ter formato inesperado apesar
de validação upstream).
Liste problemas de correção encontrados, se houver.
Se não encontrar nenhum, confirme explicitamente.
"""
# Passagem 2 — Revisão de qualidade (depois de correção aprovada):
"""
Assumindo que a lógica está correta (passagem anterior confirmou),
revise agora para qualidade de implementação:
- O código é legível para alguém que não o escreveu?
- Há duplicação que prejudica manutenibilidade?
- O naming comunica a intenção claramente?
- Há abstração faltando ou abstração prematura?
- Os comentários (se houver) adicionam informação além do código?
Contexto: base de código usa snake_case, type hints em
todas as funções públicas, docstrings no estilo Google.
"""
Separar as duas passagens em turnos distintos evita que observações de qualidade (que são opiniões) contaminem a análise de correção (que são fatos). O output de cada passagem é mais focado e mais fácil de priorizar.
// Para projetos Go com padrões específicos, o prompt
// de revisão pode referenciar as convenções locais:
//
// Revise o handler abaixo para os seguintes riscos,
// em ordem de severidade:
//
// 1. Context propagation: o ctx é propagado para todas as
// chamadas downstream? Alguma goroutine ignorando ctx.Done()?
//
// 2. Error wrapping: erros são wrapped com fmt.Errorf("...: %w", err)?
// Erros internos estão sendo mascarados?
//
// 3. Resource cleanup: todos os recursos (DB conn, HTTP response body,
// file handles) têm defer close/cancel na inicialização?
// defer está sendo chamado ANTES de verificar err?
// (armadilha: defer conn.Close() antes de if err != nil)
//
// 4. Panic recovery: o handler tem recover? Sem recover, panic
// de qualquer goroutine derruba o processo inteiro.
//
// 5. Data races: há acesso a variável compartilhada sem mutex?
// Closures em goroutines capturando variável de loop?
// (armadilha clássica em Go pré-1.22)
//
// Nota sobre Go 1.22: loop variable semantics mudou —
// o armadilha de closure capturando variável de loop
// não se aplica mais para loops range. Levar em conta
// a versão do go.mod ao avaliar esse ponto.
Incluir a versão da linguagem no prompt é importante para Go: a semântica de loop variable mudou no 1.22, e um LLM sem esse contexto pode reportar falso positivo para código correto em versões recentes.
Revisão de PRs grandes: dividir para conquistar
PRs grandes são onde a revisão por LLM tem mais potencial de valor — e onde mais frequentemente o output é genérico e inútil. Um PR com 800 linhas de diff passado inteiro ao LLM produz análise superficial: o contexto é grande demais para análise profunda de cada seção, e o LLM vai produzir observações de alto nível sem a especificidade que torna feedback acionável.
A estratégia para PRs grandes: dividir o diff em seções funcionais (uma seção por arquivo ou grupo de arquivos relacionados) e revisar cada seção separadamente com prompt específico para aquela seção. O modelo de domínio que mudou recebe prompt de revisão de correção de invariantes. O endpoint novo recebe prompt de revisão de segurança e tratamento de erro. Os testes novos recebem prompt de revisão de cobertura de casos. A migração de banco recebe prompt de revisão de zero-downtime e rollback.
Essa fragmentação produz análise mais profunda por seção e feedback mais acionável. O engenheiro revisa o feedback de cada seção, descarta o que não é relevante, e consolida o que é. O custo é mais turnos de interação; o benefício é feedback de qualidade em vez de análise genérica.
Usando LLM para preparar o próprio PR para revisão
Além de revisar código, LLMs são úteis para preparar o PR para revisão humana: gerar a descrição do PR a partir do diff, identificar as partes que precisam de atenção especial do revisor, e checar se há algo no diff que contradiz a descrição. Esse uso é de baixo risco — o output é texto de descrição, não código — e de alto valor para times onde a qualidade da descrição do PR correlaciona com a qualidade da revisão.
A instrução eficaz: "dado o diff abaixo, escreva uma descrição de PR que explique o quê mudou, por quê, e o que o revisor deve prestar atenção especial. Identifique se há alguma mudança no diff que não está coberta pela descrição que forneci". O LLM frequentemente identifica mudanças incidentais (um arquivo que foi editado mas não é mencionado, uma mudança de comportamento que parece não-intencional) que o autor do PR não percebeu — porque o autor está próximo demais do código.
Como praticar
- Comparar revisão por LLM com revisão humana em PR real. Pegue um PR que já foi revisado por um colega humano. Passe o mesmo diff para um LLM com prompt estruturado. Compare os dois feedbacks: o que o LLM encontrou que o humano não encontrou? O que o humano encontrou que o LLM não encontrou? Essa comparação calibra exatamente onde cada tipo de revisão adiciona valor para o seu contexto específico.
- Construir um prompt de revisão padrão para o projeto. Baseado nas categorias de bugs mais comuns que aparecem em PRs do projeto (dados de postmortems ou retrospectivas), construa um prompt de revisão que verifica especificamente essas categorias. Refine ao longo de um mês com base em falsos positivos (o LLM apontou algo que não era problema) e falsos negativos (um bug passou que o prompt deveria ter capturado). Esse prompt é um ativo do time, não só uma ferramenta individual.
- Exercício de revisão de segundo olhar. Após revisar um PR você mesmo, passe-o ao LLM com prompt de revisão de segurança específico (OWASP Top 10, validação de input, gerenciamento de secrets). Verifique se o LLM encontrou algo que você não encontrou. Se encontrou, é aprendizado — qual padrão de segurança você tende a não notar em revisão? Se não encontrou nada novo, confirma que sua revisão de segurança está cobrindo os padrões básicos.
Referências para aprofundar
- livro The Art of Readable Code — Boswell e Foucher (2011).
- artigo Code Review Best Practices — Trisha Gee (2022).
- artigo How to Review Code Effectively: A GitHub Staff Engineer's Philosophy — Gergely Orosz (2022).
- paper Using LLMs for Code Review: Capabilities and Limitations — Guo et al. (2024).
- artigo OWASP Code Review Guide — OWASP Foundation.
- docs CodeRabbit Documentation — CodeRabbit.
- artigo What Can LLMs Find in Code That Humans Miss? — Simon Willison (2024).
- livro Secure By Design — Johnsson, Deogun e Sawano (2019).
- artigo Conventional Comments — conventionalcomments.org.
- vídeo The Science of Code Review — Jason Cohen, SmartBear (2013).
- artigo A Practical Guide to Using AI for Code Review — Birgitta Böckeler (2024).
- livro Software Engineering at Google — Winters, Manshreck e Wright (2020).