Revisão de Código
Um pull request no order muda o checkout para que ele confirme a reserva do assento com um único comando SQL, em vez de uma chamada HTTP ao inventory — "é mais rápido". O código compila, os testes passam e o revisor, às 18h40 de uma sexta-feira, aprova. Três semanas depois, durante uma abertura de vendas, o inventory muda sua tabela de reservas e o checkout quebra. A regra violada — um schema por serviço — estava escrita numa página da wiki que ninguém abria. Um revisor que tivesse lido essa página, toda vez, em todo PR, teria pegado o problema.
Essa é a promessa da revisão de código por IA: um revisor que lê cada diff em minutos, nunca se cansa e pode ser informado das regras do time. Esta página trata de transformar essa promessa em uma ferramenta em que as pessoas realmente confiem.
Por que revisão de código, e para que ela serve
A revisão de código moderna — uma revisão leve, apoiada em ferramentas, de cada mudança antes do merge — é praticamente universal na indústria. Estudos na Microsoft e no Google concluíram que seus principais resultados não são apenas encontrar defeitos, mas transferência de conhecimento, propriedade coletiva do código e consistência; no Google, a latência mediana da revisão era menor que um dia, e a maioria das mudanças era pequena12. Duas consequências para a revisão por IA:
- Um revisor de IA pode assumir parte do trabalho de encontrar defeitos e de consistência, mas não a transferência de conhecimento: um humano ainda precisa ler o código para aprendê-lo.
- A latência da revisão importa. O primeiro olhar humano costuma esperar horas; uma revisão por IA em dois minutos dá ao autor algo para corrigir antes de o humano chegar, e a revisão humana fica mais curta.
Em que a revisão por IA é boa e em que é ruim
| Boa em (alta precisão quando recebe contexto) | Ruim em (baixa precisão ou cega) |
|---|---|
| Bugs locais visíveis no diff: tratamento de null, off-by-one, comparação errada, vazamento de recursos | Bugs que exigem conhecimento de execução: desempenho sob carga, condições de corrida entre serviços |
| Padrões de segurança: SQL por concatenação, segredos no código, verificação de autorização ausente, desserialização insegura | Se a funcionalidade é a funcionalidade certa: a intenção de produto não está no diff |
| Violações de convenções e ADRs escritos | Convenções que ninguém escreveu |
| Testes ausentes para lógica arriscada; testes que não verificam nada | Julgar se um teste verifica o valor esperado correto (o problema do oráculo) |
| Explicar código desconhecido ao revisor humano | Grandes refatorações que abrangem centenas de arquivos (limites de contexto, diluição da atenção) |
| Consistência: formatos de erro, códigos de status, logging, nomes | Trade-offs de arquitetura que exigem pesar o contexto de negócio |
Duas fraquezas sistemáticas merecem nome. Credulidade: o modelo lê texto escrito pelo autor — descrição do PR, comentários no código — e pode ser conduzido por ele ("isto foi aprovado pela segurança"); veja prompt injection. Ruído plausível: um modelo que recebe o pedido de revisar vai encontrar alguma coisa; sem permissão explícita para não retornar nada, ele enche a revisão com observações de estilo e repetições do código.
Anatomia de um revisor de IA
sequenceDiagram
autonumber
participant GH as Host Git
participant R as Job do revisor
participant Ctx as Montador de contexto
participant GW as LLM gateway
participant Post as Publicador
GH->>R: PR aberto / novo push
R->>Ctx: diff + metadados do PR
Ctx->>Ctx: numera linhas, filtra arquivos, orça tokens
Ctx->>Ctx: adiciona convenções, ADRs, responsáveis, código relacionado
Ctx->>GW: system (estável, em cache) + user (dados não confiáveis)
GW-->>R: achados em JSON (validados pelo schema)
R->>R: filtro: linhas alteradas, severidade, confiança, dedup, teto
R->>Post: achados a publicar
Post->>GH: uma revisão com comentários inline
Post->>GH: métricas: tokens, US$, mantidos vs. descartados O modelo é uma caixa entre oito. A maior parte da engenharia — e a maior parte da qualidade — está no montador de contexto e no filtro.
Engenharia de contexto
Um modelo só consegue revisar contra aquilo que recebe. Engenharia de contexto é decidir o que entra na janela de contexto, limitada e paga, e em que ordem.
| Contexto | Por quê | Custo / risco | BoxOffice |
|---|---|---|---|
| O diff, com números de linha | O objeto da revisão; linhas numeradas permitem que o modelo cite linhas exatas | Cresce com o tamanho do PR; não confiável | git diff base...head, cada linha nova como L21 + … |
| Convenções do repositório | Transforma conselhos genéricos nas suas regras | Precisam ser curtas e atuais; lidas em toda execução | CONVENTIONS.md: schema por serviço, timeouts, dinheiro em centavos |
| Decisões de arquitetura (ADRs) | O porquê das regras; permite que o modelo aponte decisões sendo revertidas | Longos; resuma ou recupere os relevantes | 0001-schema-per-service.md, 0004-outbox-for-events.md |
| Código ao redor | A função ou classe inteira em torno de um trecho; quem chama um método alterado | Caro em tokens; exige recuperação em repositórios grandes | O HoldService.java inteiro quando um trecho altera expire() |
| Responsáveis e mapa de serviços | Qual serviço é dono de quais dados e APIs | Pequeno, de alto valor | "inventory é dono de inventory.*; order é dono de orders.*" |
| Histórico | Comentários de revisões anteriores, incidentes relacionados | Pode enviesar para problemas antigos | "Incidente 2026-08: expiração da reserva com 2 min de diferença" |
| Título e descrição do PR | A intenção da mudança | Não confiável — canal de injeção | Envolvido em <untrusted_pr_description> |
Três regras práticas:
- Ordene pensando no cache. Coloque o conteúdo estável primeiro (instruções, convenções, ADRs) e o conteúdo de cada execução por último (diff). O prompt caching é uma correspondência de prefixo: se a parte estável for idêntica byte a byte, as revisões repetidas a leem por uma fração do preço (LLM gateway).
- Orce, não trunque. Um PR de 5.000 linhas não cabe — e não deveria ser revisado como um único bloco. Mantenha arquivos inteiros por prioridade (código de produção antes de testes, antes de configuração), liste na revisão os arquivos pulados, ou divida: uma chamada por arquivo ou por serviço (map) e depois uma chamada para juntar e deduplicar (reduce). Cortar um diff no meio produz absurdos confiantes sobre código que o modelo nunca viu.
- Escreva as convenções primeiro para humanos. Se uma regra não é clara para um engenheiro novo, não é clara para o modelo. Uma regra que precisa de um parágrafo de nuances costuma funcionar melhor como verificação determinística (abaixo).
Pense primeiro: recuperação em um monorepo
A BoxOffice vira um monorepo de 40 serviços e 2 milhões de linhas. Um PR muda a assinatura de InventoryClient.confirmHold. Que contexto extra mais melhoraria a revisão, e como encontrá-lo sem enviar o repositório inteiro?
Os pontos de chamada do método alterado e o contrato da API que ele encapsula. Encontre-os de forma determinística — um índice de código (language server, ctags, um serviço de busca de código) ou um simples git grep confirmHold — e adicione os 5–10 trechos mais relevantes. A recuperação baseada em embeddings sobre o repositório (RAG) ajuda em perguntas vagas ("onde mais calculamos preços de assentos?"), mas, para "quem chama este método", um índice de símbolos é exato e mais barato.
Saída estruturada, publicada como comentários de revisão
Texto livre é para humanos; o pipeline precisa de dados. Peça um JSON que seja validado contra um schema — no Claude, as structured outputs restringem a resposta ao schema; em outros provedores, modo JSON mais validação e uma tentativa de reparo:
{
"summary": "Adds direct SQL access from order to the inventory schema.",
"findings": [
{
"file": "services/order/src/main/java/boxoffice/order/OrderService.java",
"line": 21,
"severity": "blocker",
"category": "architecture",
"confidence": "high",
"title": "order writes to the inventory schema",
"explanation": "CONVENTIONS.md: a service reads and writes only its own schema. This UPDATE couples order to inventory's table layout and bypasses inventory's locking, so a hold can be confirmed after it expired.",
"suggestion": "Call InventoryClient.confirmHold(holdId) and handle 409 Conflict."
}
]
}
| Campo | Usado para |
|---|---|
file, line | Posicionamento inline; validado contra o diff (comentários em linhas fora do diff são rejeitados pelo host Git) |
severity (nit … blocker) | Limiares e ordenação; decisões de gate |
category | Precisão por categoria; roteamento (achados de segurança para o security champion) |
confidence | Controle de ruído; calibração ao longo do tempo |
title | Impressão digital para deduplicação; a linha que as pessoas leem de relance |
explanation, suggestion | O corpo do comentário; uma correção concreta torna o comentário acionável |
No GitHub, todos os achados de uma execução vão para uma revisão (POST /repos/{owner}/{repo}/pulls/{number}/reviews com um array comments de path, line, side), de modo que o autor recebe uma notificação, e não doze3. Um evento de revisão COMMENT mantém o bot consultivo; é o código — não o modelo — que escolhe esse evento.
Controle de ruído
Cada comentário gasta a atenção de um desenvolvedor. Um revisor é julgado pelos seus piores comentários: depois de alguns inúteis, as pessoas param de ler — inclusive os úteis. Os controles, na ordem em que atuam:
| Controle | O que ele remove |
|---|---|
| Gatilhos e filtros de caminho | Rascunhos, PRs só de documentação, arquivos gerados, lockfiles, código vendorizado |
| Prompt: "uma lista vazia é uma boa revisão; sem elogios; sem repetir o código" | Enchimento |
| Só linhas alteradas | Comentários em código que o autor não tocou |
| Severidade mínima | Detalhes de estilo que um formatador deveria resolver |
| Confiança mínima | Especulação |
| Deduplicação por impressão digital entre pushes | O mesmo comentário republicado a cada push |
| Teto por PR | Enxurradas em PRs grandes — mantenha os mais graves |
| Uma revisão por push + um resumo fixo | Tempestades de notificações |
Os limiares trocam precisão (a fração dos comentários publicados que são úteis) por recall (a fração dos problemas reais encontrados). A tabela usa uma amostra rotulada ilustrativa de 100 PRs — o modelo emitiu 60 achados de confiança alta, 90 de média e 150 de baixa, dos quais 48, 45 e 30 eram problemas reais, de um total de 150 problemas reais:
| Post findings with confidence ≥ | Comments per PR | Precision | Recall |
|---|---|---|---|
| high | 0.6 | 80% | 32% |
| medium | 1.5 | 62% | 62% |
| low | 3.0 | 41% | 82% |
Publicar tudo encontra a maior parte dos problemas e é ignorado por todo mundo (3 comentários por PR, 4 em cada 10 úteis). Publicar só os de confiança alta gera confiança e deixa passar dois terços dos problemas. A maioria dos times começa com alta + média para severidades ≥ minor, mede e ajusta por categoria: segredos e SQL injection podem ficar em média; estilo nunca é publicado.
No código do BoxOffice: o filtro do revisor (lab 13)
O Hands-on 13 — O revisor constrói a anatomia acima num único script. O título, a descrição e o diff do PR vão ao modelo embrulhados como dados não confiáveis; o modelo responde JSON contra um schema; e o código — não o modelo — mantém só os achados em linhas adicionadas, com severidade ≥ minor, sem confiança baixa, no máximo 6 por PR, os mais graves primeiro. Uma impressão digital de cada achado impede que ele seja publicado de novo no próximo push, e um erro do modelo encerra o job com exit code 0: fail open, porque uma etapa consultiva nunca pode bloquear merges. Uma execução de teste com o modelo local publicou um achado, o SQL injection plantado em EventSearch.java; com o Claude, uma revisão do golden set do lab custou cerca de US$ 0,012.
def review(diff: str, title: str, body: str, root: Path = Path(".")):
"""Review a diff: (summary, kept findings, dropped [(finding, why)], usage, skipped files)."""
files = diffutil.split_files(diff)
files, skipped = diffutil.budget(files, MAX_DIFF_LINES)
if not files:
return "No reviewable changes.", [], [], Usage(model()), skipped
numbered = "\n\n".join(diffutil.numbered(p, lines) for p, lines in files.items())
user = (f"<untrusted_pr_title>\n{title}\n</untrusted_pr_title>\n"
f"<untrusted_pr_description>\n{body[:4000]}\n</untrusted_pr_description>\n"
f"<untrusted_diff>\n{numbered}\n</untrusted_diff>")
data, usage = complete_json(load_context(root), user, SCHEMA, effort="medium")
kept, dropped = [], []
added = {p: diffutil.added_lines(lines) for p, lines in files.items()}
for f in data["findings"]:
if f["line"] not in added.get(f["file"], set()):
dropped.append((f, "not an added line"))
elif SEVERITIES.index(f["severity"]) < SEVERITIES.index(MIN_SEVERITY):
dropped.append((f, f"below {MIN_SEVERITY}"))
elif f["confidence"] == "low":
dropped.append((f, "low confidence"))
else:
f["fp"] = fingerprint(f)
kept.append(f)
kept.sort(key=lambda f: (-SEVERITIES.index(f["severity"]), f["confidence"] != "high"))
for f in kept[MAX_COMMENTS:]:
dropped.append((f, f"over the limit of {MAX_COMMENTS} comments"))
return data["summary"], kept[:MAX_COMMENTS], dropped, usage, skipped
try:
summary, kept, dropped, usage, skipped = review(diff, title, body)
except LLMError as err:
step_summary(f"### AI review skipped\n\n{err}")
return 0 # fail open: an AI outage must not block merges
Consultivo ou gate?
| Consultivo | Gate (check obrigatório) | |
|---|---|---|
| Efeito | Comentários; humanos decidem | Merge bloqueado até o achado ser corrigido ou ignorado com justificativa |
| Custo de um falso positivo | Um minuto de leitura | Um PR bloqueado, uma liberação manual, frustração — e pressão para desligar o bot |
| Custo de um falso negativo | O mesmo de hoje | Falsa sensação de segurança se os humanos relaxarem |
| Requisitos | Nada de especial | Alta precisão medida naquela categoria; um caminho para liberação manual; confiabilidade com fail closed |
| Bom para | Tudo, no começo | Categorias estreitas e de alta precisão: segredos no código, SQL por concatenação, acesso proibido entre schemas |
Uma implantação segura: sombra (achados registrados, não publicados; compare com o que os humanos encontraram) → consultivo para times voluntários → gate por categoria quando a precisão estiver comprovada. Mesmo como gate, uma verificação de IA é um status check obrigatório — nunca uma aprovação. A aprovação continua humana, imposta pela proteção de branch.
Medindo o revisor
Não dá para ajustar o que não se mede, e "o time gosta" não é uma medida.
| Métrica | Como | Observações |
|---|---|---|
| Precisão | Comentários aceitos ÷ (aceitos + descartados); reações ou um botão de resolver com motivo | Exige que as pessoas avaliem; use amostragem se necessário |
| Recall | Encontrados ÷ plantados, em um golden set de PRs com problemas conhecidos | Rode a cada mudança de prompt ou de modelo, no CI |
| Defeitos escapados | Bugs encontrados depois em código que o bot revisou | Lenta, mas é a métrica que interessa ao negócio |
| Ruído | Comentários por PR; % de PRs sem nenhum comentário | Zero muitas vezes é a resposta certa |
| Custo e latência | US$ e minutos por revisão | A partir do uso de tokens do job |
| Comportamentais | Tempo até a primeira revisão, rodadas de revisão, tempo até o merge | Exigem um período de referência |
Um LLM também pode julgar comentários ("este comentário estava correto?"), o que é mais barato que rótulos humanos — mas o LLM como juiz tem vieses (tende a concordar com textos confiantes, e consigo mesmo). Use-o para triagem e calibre-o contra uma amostra rotulada por humanos.
Conformidade com a arquitetura
A história do início — o order escrevendo no schema de inventory — é uma regra de arquitetura. Essas regras são o que de mais valioso um revisor automatizado pode verificar, porque humanos as esquecem e as violações custam caro depois. Mas muitas delas não precisam de modelo algum.
| Regra | Verificação determinística (preferida) | Onde a IA agrega valor |
|---|---|---|
"O order não deve acessar o schema de inventory" | Lint de SQL: grep por inventory. em services/order/**; usuários de banco separados por serviço, com grants (o banco recusa) | Explicar o porquê, sugerir a chamada de API no lugar |
| "Controllers não devem chamar repositories diretamente" | Teste com ArchUnit no build | — |
| "Todo cliente HTTP de saída tem timeouts" | Lint customizado, ou uma biblioteca de cliente compartilhada que os exige | Identificar um novo cliente feito à mão que contorna a biblioteca |
| "Eventos passam pelo outbox" | ArchUnit: nenhum uso de KafkaTemplate fora do relay do outbox | Reconhecer uma publicação direta disfarçada |
| "Prefira operações idempotentes para pagamento" | Difícil de expressar estaticamente | Bom encaixe: vago, semântico, dependente de contexto |
@AnalyzeClasses(packages = "boxoffice.order")
class ArchitectureTest {
@ArchTest
static final ArchRule no_direct_kafka_outside_outbox =
noClasses().that().resideOutsideOfPackage("..outbox..")
.should().dependOnClassesThat().haveSimpleName("KafkaTemplate")
.because("ADR-0004: events are published through the transactional outbox");
}
O melhor projeto combina os dois: regras determinísticas bloqueiam, porque são exatas; o revisor de IA explica e pega o que as regras não conseguem expressar, e seus achados recorrentes viram candidatos a novas regras determinísticas. De novo, defesa em profundidade — e o usuário de banco do order sem grants em inventory.* é a última camada, que não depende de revisor nenhum.
BoxOffice: o revisor pega o atalho
O UPDATE inventory.hold no OrderService — o achado do JSON acima — é o caso 02-cross-schema do golden set do revisor no hands-on 13. Com o CONVENTIONS.md no contexto, o revisor o encontrou nas execuções que o lab mostra, com o modelo local e com o Claude; mas uma execução posterior do pequeno modelo local o deixou passar. Um revisor é uma probabilidade, então o time adiciona a verificação determinística: um papel (role) do PostgreSQL separado por serviço, para que o mesmo erro falhe nos testes de integração mesmo que todos os revisores deixem passar.
Na entrevista
Quando pedirem para você projetar um revisor de código com IA, diga cedo que precisão vale mais que recall para tudo o que fala com humanos, e mostre como você vai medi-la (golden set para o recall, aceitos/avaliados para a precisão). Descreva o contexto (diff + convenções + ADRs + código relacionado por busca de símbolos), os achados estruturados, o filtro (linhas alteradas, severidade, confiança, dedup, teto) e a implantação (sombra → consultivo → gate por categoria). Mencione que regras de arquitetura devem ser verificações determinísticas primeiro, com a IA como quem explica.
Boas práticas
- Comece em modo sombra — registre os achados por duas semanas e compare-os com as revisões humanas antes de publicar qualquer coisa.
- Versione o contexto do revisor junto com o código — o
CONVENTIONS.mde os ADRs vivem no repositório e mudam por pull request; o revisor do lab 13 os carrega como o seu prefixo estável, que vai para o cache. - Colete uma reação em todo comentário — um 👍/👎 por achado dá a precisão por categoria sem custo; todo comentário do lab 13 pede um.
- Limite os comentários por PR e publique primeiro os mais graves — o lab 13 publica no máximo 6 (
AI_REVIEW_MAX_COMMENTS). - Elimine achados duplicados entre pushes — o mesmo problema não pode ser publicado de novo a cada push; o lab 13 guarda uma impressão digital de cada achado no comentário.
Armadilhas comuns
- Sem permissão para ficar em silêncio: o modelo inventa trabalho. Diga explicitamente que uma revisão vazia é boa.
- Revisar o repositório inteiro em vez da mudança: caro e fora do assunto.
- Tratar o silêncio do bot como aprovação: os humanos relaxam, os defeitos escapados aumentam.
- Usar como gate antes de medir: uma semana de bloqueios falsos e o time desliga o bot.
- Regras só no prompt: uma regra que importa também deve existir como teste, lint ou grant no banco.
Como se conecta
flowchart LR
PR["pull request event"] --> CTX["context: diff, conventions,<br/>ADRs, symbol search"]
CTX --> RET["retrieval<br/>→ AI systems RAG & Memory"]
CTX --> LLM["model call<br/>→ LLM gateway, prefix cache"]
LLM --> OUT["structured findings<br/>(JSON schema)"]
OUT --> NOISE["noise control: changed lines,<br/>confidence, dedup, cap"]
NOISE --> GATE{"advisory<br/>or gate?"}
GATE --> PREC["precision + golden set<br/>→ AI systems safety & evals"]
ARCH["architecture rules:<br/>deterministic checks first"] --> OUT
PR --> INJ["PR text is untrusted<br/>→ Security & governance"] Componentes da paleta: orchestrator (o job do revisor em volta do modelo) · llmgw (roteamento, cotas, prefixo em cache com convenções e ADRs) · memory (o contexto montado para cada diff) · safety (texto do PR não confiável, saída validada) · metrics (precisão e comentários por PR). Cenários: nenhum específico. Continua em: Sistemas de IA — RAG e memória, Sistemas de IA — LLM Gateway, Sistemas de IA — segurança e avaliações, Segurança e governança, Hands-on 13 — o revisor.
-
BACCHELLI, A.; BIRD, C. Expectations, outcomes, and challenges of modern code review. ICSE 2013. ↩
-
SADOWSKI, C. et al. Modern code review: a case study at Google. ICSE-SEIP 2018. ↩
-
GitHub Docs. REST API endpoints for pull request reviews. ↩