MÓDULO 13 · CONCEITO 01 DE 15

LLMs para Engenharia

Capacidades, limites e o modelo mental correto para usar IA sem se perder

Tempo de leitura ~22 min Pré-requisito Módulos 00–12 — toda a trilha Próximo 02 · Spec-driven development

Large Language Models chegaram à engenharia de software com uma promessa vaga e sedutora: "escreva código mais rápido". E de fato escrevem. Um engenheiro com acesso a Claude Code, GitHub Copilot ou Cursor produz código em velocidade que seria impossível há dois anos. O problema não está na promessa — está no que fica implícito nela: que escrever código mais rápido é o gargalo da engenharia de software. Não é. O gargalo é escrever o código certo, entender o que está sendo construído, e manter o sistema ao longo do tempo. Nessas dimensões, LLMs têm perfil de capacidade radicalmente diferente do que a maioria dos engenheiros assume quando os adota.

Este conceito é sobre construir o modelo mental correto antes de usar LLMs como ferramenta de engenharia. O modelo mental errado — "o LLM sabe mais do que eu, então devo confiar no output dele" — é mais perigoso do que não usar LLM nenhum. Ele produz código que ninguém entende, bugs que ninguém sabe diagnosticar, e uma base de código que cresce sem que o time tenha modelo mental dela. O modelo mental correto — "o LLM é um par programador extremamente veloz e extremamente não-confiável, e eu sou o árbitro de cada linha que entra na base" — é o que transforma LLMs em multiplicadores de produtividade reais.

A posição deste módulo no final da trilha é deliberada. Os doze módulos anteriores construíram o julgamento técnico necessário para usar LLMs de forma produtiva: você já sabe o que é código testável (módulo 02), como modelar um banco de dados corretamente (módulo 03), quando usar goroutines vs threads vs async (módulo 04), como avaliar se um sistema vai escalar (módulo 07), o que caracteriza disponibilidade real de 99.99% (módulo 08). Esse julgamento é o que torna possível avaliar o output de um LLM criticamente. Sem ele, LLM é gerador de código plausível. Com ele, é multiplicador de engenharia real.

Começamos pelo que os LLMs fazem bem e mal — não como checklist, mas como modelo mental que permite antecipar quando confiar e quando questionar.

O que LLMs fazem bem

Entender as capacidades reais de um LLM começa por entender o que são: modelos de linguagem treinados para prever o próximo token mais provável dado o contexto. Isso parece limitante, mas produz capacidades surpreendentemente úteis para engenharia.

Geração de código plausível a partir de descrição natural. LLMs são bons em transformar descrição de intenção em código que parece correto. "Crie uma função que receba uma lista de pedidos e retorne os pedidos pendentes há mais de 48 horas ordenados por prioridade" produz código funcional na maioria dos casos. A palavra-chave é "plausível" — o código satisfaz a descrição literal, mas pode não satisfazer invariantes implícitos que o engenheiro tem em mente e não expressou.

Transformação e refatoração mecânica. Renomear variáveis consistentemente, extrair métodos, converter código síncrono em assíncrono, migrar de uma versão de API para outra — transformações com regras claras e verificáveis. LLMs fazem isso rápido e geralmente correto, porque é essencialmente matching de padrão com aplicação de regra. O risco é baixo quando os testes verificam que o comportamento não mudou.

Síntese de padrões conhecidos. "Implemente um circuit breaker" ou "adicione retry com backoff exponencial" são padrões que aparecem muitas vezes no corpus de treinamento. O LLM não está inventando — está instanciando um padrão que viu dezenas de vezes. O output tende a ser razoavelmente correto para o caso geral, mas pode perder nuances específicas do contexto do projeto.

Geração de código boilerplate. Setup de projeto, arquivos de configuração, estrutura inicial de classes, testes unitários para código que já existe (onde o comportamento esperado é claro), handlers HTTP, conexões de banco de dados — todo o código que um engenheiro escreve pensando "isso é tedioso e repetitivo". LLMs são excelentes nisso, e esse é o ganho mais imediato e menos arriscado.

Explicação e documentação de código existente. "O que esse código faz?" é uma pergunta que LLMs respondem bem. Dado código suficientemente auto-contido, a explicação é geralmente correta e útil. A armadilha é o "suficientemente auto-contido" — contexto que o código pressupõe (invariantes de domínio, comportamento de dependências, decisões arquiteturais) não está visível no texto do código e o LLM pode gerar explicação plausível mas errada.

O que LLMs fazem mal

As limitações dos LLMs são tão características quanto suas capacidades, e entendê-las é essencial para não ser surpreendido.

Raciocínio formal e contagem exata. LLMs não raciocinam — preveem. Para problemas que requerem prova, contagem precisa, ou derivação formal, o resultado parece raciocínio mas frequentemente não é. Um LLM pode produzir uma "prova" de algoritmo que parece correta mas tem passo inválido que não é óbvio. Para algoritmos críticos, nunca confiar no output sem verificação independente.

Invariantes sistêmicas e conhecimento de contexto implícito. "Adicione um endpoint para deletar usuários" parece simples. Mas se o sistema tem invariante que usuários com pedidos pendentes não podem ser deletados, e essa invariante vive em código que o LLM não viu, ele vai gerar o endpoint sem a verificação. LLMs trabalham com o que está no contexto — o que é implícito para o engenheiro é invisível para o modelo.

Novidade real. LLMs sintetizam o que viram. Para problemas genuinamente novos — uma biblioteca que foi lançada depois do corte de treinamento, uma integração muito específica que não tem documentação pública, um algoritmo que não existe na literatura — o output tende a ser alucinação confiante: o modelo produz algo que parece plausível mas é inventado. A confiança com que o output é apresentado não correlaciona com sua correção.

Performance não-local e comportamento emergente. O LLM pode gerar código que funciona corretamente para um único registro mas produz N+1 queries quando chamado em loop, ou que é thread-safe em isolamento mas tem race condition quando múltiplas instâncias rodam concorrentemente. Esses problemas requerem entendimento do sistema como um todo, não só do código gerado.

Segurança em contexto específico. LLMs conhecem as vulnerabilidades comuns — injeção SQL, XSS, SSRF. Mas podem gerar código com vulnerabilidade sutil quando a combinação de framework, configuração e contexto de domínio cria uma superfície de ataque não-óbvia. Code review humano com foco em segurança é insubstituível para código que lida com dados sensíveis ou autenticação.

armadilha clássica

Hallucination é uma característica estrutural dos LLMs, não um bug que vai ser corrigido. O modelo não sabe o que não sabe — ele produz o próximo token mais provável, o que em domínios onde a resposta correta é incerta resulta em texto plausível e incorreto. A confiança com que o output é apresentado é um artifact do processo de geração, não evidência de correção. "O LLM disse assim" não é argumento.

Hallucination como característica estrutural

Hallucination — produzir output factualmente incorreto com aparência de confiança — é frequentemente tratado como defeito a ser corrigido em modelos futuros. Essa framing é imprecisa e perigosa para engenheiros. Hallucination é uma consequência da arquitetura: o modelo maximiza plausibilidade do próximo token, não veracidade. Para domínios onde tokens plausíveis e tokens verdadeiros coexistem fortemente (texto bem escrito, código idiomático, explicações coerentes), o output tende a ser correto. Para domínios onde plausibilidade e veracidade divergem (fatos específicos, datas exatas, comportamento de APIs obscuras, aritmética), hallucination aparece.

Para engenheiros, a implicação prática é: o LLM é mais confiável quanto mais o problema é sobre padrão do que sobre fato específico. "Implemente um consumer Kafka com retry em Go" é mais seguro do que "qual a versão mais recente da library confluent-kafka-go e qual a API correta para o método X". Para o primeiro, o LLM está instanciando um padrão que viu muitas vezes. Para o segundo, está produzindo o token mais plausível para uma afirmação factual específica — e pode estar errado.

O protocolo correto para qualquer claim factual específico (nome de método, comportamento de versão, configuração exata) é verificar na documentação oficial. Não porque o LLM esteja errado com frequência alta, mas porque o custo de verificar é baixo e o custo de não verificar — um bug em produção baseado em API hallucinated — é alto.

O modelo mental correto: pair programmer júnior muito veloz

O modelo mental mais útil para trabalhar com LLMs em engenharia é: par programador júnior extremamente veloz e extremamente pouco confiável. Cada palavra importa.

Par programador — não um oráculo, não uma fonte de verdade. A responsabilidade pelo código que entra na base permanece integralmente com o engenheiro. O LLM sugere; o engenheiro decide. Quando um colega júnior propõe uma solução, você considera a proposta, avalia se faz sentido no contexto do sistema, e aceita, modifica ou rejeita. O mesmo processo se aplica ao output de LLM.

Júnior — bom em tarefas bem-definidas com requisitos claros, ruim em tarefas que requerem entendimento profundo de contexto, julgamento arquitetural, ou conhecimento de domínio específico que não foi explicitamente fornecido. Um júnior que acabou de entrar na empresa escreve código sintaticamente correto mas pode não saber as convenções do projeto, as invariantes do domínio, ou as decisões arquiteturais que foram tomadas nos últimos 18 meses. LLM é igual — a menos que você forneça esse contexto explicitamente.

Extremamente veloz — isso é real e valioso. A velocidade com que um LLM produz um sketch de solução, um teste unitário, um handler HTTP, ou uma função de transformação é genuinamente impressionante. Velocidade de geração é a capacidade mais confiável e onde o ganho de produtividade é mais imediato.

Extremamente pouco confiável — o LLM pode estar errado de formas que não são óbvias. Um par júnior que comete um erro geralmente comete um erro óbvio — sintaxe errada, lógica claramente equivocada. LLMs podem produzir erros sutis que passam na inspeção superficial: código que compila e passa em testes básicos mas tem race condition sob carga, ou que funciona no happy path mas tem comportamento incorreto em edge cases de domínio.

modelo mental

A questão não é "o LLM está certo?" — é "o LLM gerou um ponto de partida útil que eu consigo avaliar, modificar e defender?". Se você não consegue ler o código gerado e entender o que ele faz, não aceite. Se você aceita código que não entende, está produzindo dívida técnica com alta velocidade. Velocidade de geração não é velocidade de entrega se o código vai quebrar em produção.

O que muda e o que não muda na engenharia

A chegada de LLMs capazes de gerar código não muda o que faz um engenheiro de software ser bom no trabalho — muda quais habilidades são mais ou menos diferenciadores.

O que fica mais valioso: julgamento técnico (saber quando uma solução está certa mesmo que o código pareça OK), entendimento de sistemas (saber o impacto de uma mudança no comportamento de componentes que não estão no mesmo arquivo), domínio de contexto de negócio (saber quais invariantes o sistema deve preservar e por quê), capacity de avaliar e criticar output em vez de gerá-lo, liderança técnica (alinhar times em torno de decisões que LLMs não tomam). Essas habilidades não são amplificadas pelo LLM — elas determinam se o LLM está sendo usado para construir algo correto.

O que fica menos diferenciador: velocidade de digitação, memorização de APIs, geração de código boilerplate idiomático, estrutura de projeto inicial. Qualquer LLM faz isso rápido e razoavelmente bem. Não é que essas habilidades deixam de ter valor — é que elas deixam de ser o que distingue um engenheiro muito bom de um engenheiro médio.

O engenheiro que usa LLM para amplificar habilidades de julgamento já existentes — escreve spec, usa LLM para implementar, avalia criticamente o output, itera — produz mais em menos tempo mantendo controle. O engenheiro que usa LLM para substituir habilidades de julgamento que não tem — descreve vagamente o que quer, aceita o output, submete PR sem entender — produz código rápido que o time vai pagar caro depois.

Contexto como insumo crítico

A qualidade do output de um LLM depende diretamente da qualidade do contexto fornecido. Isso é a implicação mais prática do modelo mental "par júnior que só sabe o que você mostrou". Um prompt vago produz código que satisfaz a descrição vaga. Um prompt rico em contexto produz código que se aproxima do que o sistema real precisa.

Contexto útil para engenharia inclui: o que o código deve fazer (comportamento esperado, não só nome da função); o que o código não deve fazer (restrições, casos que devem resultar em erro, comportamentos que seriam problemas); como o sistema ao redor funciona (interfaces com que o código vai interagir, convenções de naming do projeto, bibliotecas já em uso); exemplos de código existente que serve como referência de estilo e padrão.

Ferramentas como Claude Code resolvem parte do problema de contexto automaticamente: o agente lê os arquivos do repositório, entende as dependências em uso, vê como o código existente está estruturado. Mas o contexto de domínio — as invariantes de negócio, as decisões arquiteturais tomadas há 18 meses, os casos de borda específicos do produto — ainda precisa ser fornecido explicitamente, seja via CLAUDE.md, comentários no código, ou na própria instrução.

C# — Prompt sem e com contexto
// ❌ Prompt fraco — output genérico
// "Crie um método para processar pedidos"

// ✅ Prompt com contexto — output específico e correto
// "No sistema de e-commerce deste repositório (ver Order.cs
// e IOrderRepository.cs), crie um método ProcessPendingOrders
// que:
// - Busca pedidos com status Pending há mais de 48h
// - Tenta cobrar via IPaymentService.ChargeAsync
// - Se cobrança falhar 3 vezes, marca como Abandoned
// - Se suceder, marca como Confirmed e dispara
//   OrderConfirmedEvent via IEventBus
// - Usa as mesmas convenções de logging que vê em
//   CartService.cs (ILogger, structured props)
// - Deve ser idempotente: processar o mesmo pedido
//   duas vezes não deve cobrar duas vezes"

O segundo prompt respeita as invariantes do domínio (idempotência, 3 tentativas, eventos), referencia os tipos existentes, e especifica comportamento de erro — tudo o que o LLM não poderia inferir sem ver o código.

Python — Contexto via exemplos do repositório
# ❌ Sem contexto
# "Crie um endpoint de listagem de usuários"

# ✅ Com contexto e exemplo de referência
# "Olhando o endpoint existente em routes/products.py
# (método list_products), crie um endpoint similar em
# routes/users.py com as mesmas características:
# - Paginação cursor-based (não offset)
# - Filtros opcionais via query params
# - Resposta paginada no formato PagedResponse[T]
# - Autenticação via depends(get_current_user)
# - Logging estruturado como nos outros endpoints
# - Restrição: usuário só pode ver usuários da mesma
#   organização (campo org_id no token JWT)"

Referenciar um endpoint existente como exemplo resolve ambiguidades de convenção sem precisar descrever cada detalhe — o LLM lê o arquivo e extrai o padrão.

Go — Especificando restrições técnicas explícitas
// ❌ Vago
// "Crie um worker que processa mensagens de uma fila"

// ✅ Específico e com restrições técnicas
// "Crie um worker para processar mensagens do tópico
// 'order.created' via amqp091-go (ver consumer.go para
// o padrão de conexão em uso). Restrições:
// - Deve usar errgroup para cancelamento coordenado
// - Concorrência: até 10 mensagens simultâneas
//   (semáforo ou worker pool — veja o padrão em
//    processor.go)
// - Timeout por mensagem: 30s via context.WithTimeout
// - Ack apenas após processamento bem-sucedido;
//   em erro, Nack com requeue=false (vai p/ DLQ)
// - Graceful shutdown: ao receber SIGTERM, para de
//   consumir, espera mensagens em flight terminarem
//   (máximo 60s), então fecha conexão"

Restrições técnicas explícitas (errgroup, semáforo, Nack com requeue=false, graceful shutdown) são invariantes que o LLM não infere do contexto — precisam ser ditas.

A questão da responsabilidade

Uma tensão cultural que emerge com adoção de LLMs em times: quando um bug aparece em código gerado por IA, de quem é a responsabilidade? A resposta é clara e não-negociável: de quem aceitou o código no PR. "O LLM gerou" não é argumento em postmortem. "Aceitei sem revisar adequadamente" é a análise correta da causa raiz.

Isso não é uma posição conservadora contra IA — é a mesma posição que se aplica para qualquer code review. Se você aprova um PR de um colega sem ler o código e o PR tem um bug crítico, você compartilha responsabilidade pela falha. A diferença com LLM é que o "colega" produz código muito mais rápido e com erros sutis diferentes dos erros humanos típicos, o que torna a tentação de aceitar sem revisar mais forte — e o custo de fazê-lo mais alto.

A cultura saudável é: LLM como ferramenta que acelera a produção de candidatos a código, seguida de revisão humana genuína. Não revisão superficial ("parece OK, compila, testa") mas revisão que se pergunta: "entendo o que esse código faz? ele respeita as invariantes do domínio que conheço? tem algum caso de borda não testado que poderia ser problema em produção?"

heurística do sênior

Antes de aceitar qualquer bloco de código gerado por LLM, passe pelo teste: "conseguiria explicar cada linha desse código para um colega em code review, incluindo por que cada decisão foi tomada?" Se não consegue, não aceite ainda. Itere com o LLM até entender, ou reescreva você mesmo. O objetivo não é código rápido — é código que você é dono.

Por que este módulo está no final da trilha

AI-assisted engineering é o único módulo desta formação que se torna mais valioso quanto mais os módulos anteriores foram assimilados. Isso não é coincidência — é o ponto central.

Considere o que acontece quando um engenheiro sem solidez técnica usa LLM para implementar um sistema com requisitos de disponibilidade alta: o LLM pode gerar código que não tem circuit breaker, que não trata timeout, que faz retry sem backoff — porque esses requisitos não estavam explícitos no prompt. O engenheiro sem bagagem do módulo 08 não vai perceber que o código está errado. O engenheiro com essa bagagem vai perguntar: "onde está o handling de timeout? e o retry com jitter? e o fallback se o serviço downstream cair?"

O mesmo se aplica para banco de dados: o engenheiro que assimilou o módulo 03 vai questionar se o código gerado está usando índices corretamente, se há risco de N+1, se as transações estão nos limites certos. Para concorrência: vai verificar se goroutines ou tasks são sendo canceladas corretamente, se há race condition, se o modelo de backpressure faz sentido. Para segurança: vai checar se há SQL injection, se tokens estão sendo validados, se secrets estão sendo expostos.

LLMs amplificam o julgamento técnico que o engenheiro já tem. Não podem criá-lo. Os doze módulos anteriores desta trilha são o investimento que torna os módulos 13 e 14 produtivos em vez de perigosos.

Como praticar

  1. Mapa de capacidades aplicado ao seu sistema atual. Pegue uma funcionalidade que você implementou recentemente. Pense em quais partes do código um LLM teria gerado corretamente com prompt bem escrito, quais teria gerado com erros previsíveis, e quais requerem conhecimento de domínio que não existe em nenhum corpus público. Escreva esse mapa — ele vai calibrar onde você confia e onde verifica.
  2. Experimento de "prompt fraco vs prompt rico". Escolha uma tarefa de implementação média (um endpoint, uma função de processamento, um consumer de fila). Escreva primeiro um prompt vago e observe o output. Depois escreva um prompt com contexto completo (invariantes, referências a código existente, restrições técnicas) e compare. Documente o que mudou no output e o que ainda precisou de correção.
  3. Revisão crítica de código gerado. Gere um bloco de código não-trivial com LLM (handler HTTP com autenticação, consumer de fila com retry, query com paginação). Não aceite nem rejeite imediatamente. Leia linha por linha e escreva comentários explicando o que cada parte faz. Identifique pelo menos uma coisa que está certa, uma que está questionável, e uma que está ausente (invariante que o LLM não conhecia). Esse exercício calibra o reflexo de revisão crítica.

Referências para aprofundar

  1. livro AI-Assisted Software Development — Andrew Nguyen (2024). Cobertura prática de fluxos de trabalho com Copilot, Claude e ferramentas similares. Foco em como integrar sem perder controle da qualidade.
  2. artigo The End of Programming — Matt Welsh, Communications of the ACM (2023). Argumento provocador sobre o que muda na profissão — útil como contraponto para reflexão. Não tomar literalmente.
  3. artigo Copilot Internship Study — GitHub (2023). Estudo empírico sobre impacto de Copilot em produtividade. Mostra onde ganho é real e onde é ilusório. Dados, não anedota.
  4. paper Large Language Models and the Automation of Software Engineering Tasks — Zheng et al., IEEE (2024). Revisão sistemática de benchmarks de LLMs em tarefas de engenharia. Útil para calibrar o que os números de benchmark significam na prática.
  5. artigo How I use Claude Code — Anthropic Engineering Blog (2025). Perspectiva interna sobre fluxos de trabalho com Claude Code. Como a própria equipe usa o produto para desenvolver o produto.
  6. livro A Philosophy of Software Design — John Ousterhout (2018). Não é sobre IA, mas é sobre o que faz software bem projetado — o julgamento que o LLM não tem. Essencial para saber o que avaliar no output gerado.
  7. artigo The Pragmatic Engineer: AI in Software Engineering — Gergely Orosz (2024). Survey de como times em empresas grandes estão usando IA em engenharia. Dados anedóticos mas de fontes confiáveis sobre adoção real.
  8. docs Claude Code Documentation — Anthropic. Documentação oficial. Importante para entender as ferramentas disponíveis para o agente, limitações de contexto, e como configurar corretamente.
  9. vídeo Andrej Karpathy — Intro to Large Language Models (2023). youtube.com/watch?v=zjkBMFhNj_g — Explicação técnica mais acessível de como LLMs funcionam. Faz com que as limitações façam sentido no nível mecânico.
  10. artigo On the Dangers of Stochastic Parrots — Bender et al. (2021). Artigo que cunhou "stochastic parrot" para LLMs. Perspectiva crítica sobre o que LLMs fazem e não fazem. Importante para não sobre-atribuir capacidades.
  11. artigo Beyond the Code: AI-Assisted Engineering in Practice — Birgitta Böckeler, Martin Fowler (2024). martinfowler.com — Análise de padrões de uso de IA em engenharia, com foco em quando funciona e quando falha. Perspectiva de consultor experiente.
  12. livro Staff Engineer: Leadership beyond the management track — Will Larson (2021). Sobre o que é julgamento técnico sênior — o que IA amplifica mas não substitui. Útil para contextualizar o papel do engenheiro na era de LLMs.