Engenharia de Prompt para Código: Por Que Sua Instrução Está Gerando Dívida Técnica Silenciosa
Você pede para o modelo “criar uma função que processa pagamentos com retry”, recebe um código que compila, passa no teste feliz que você rodou manualmente e vai para o pull request. Três semanas depois, em produção, um timeout no gateway de pagamento gera cobrança duplicada porque o retry não era idempotente. Ninguém pediu idempotência no prompt — e o modelo, sem essa restrição explícita, escolheu a implementação estatisticamente mais comum nos dados de treino, que raramente é a mais correta para o seu domínio específico. O código estava sintaticamente impecável e semanticamente incompleto. Esse é o tipo de bug que não aparece em nenhum linter, porque não é um erro de sintaxe: é um erro de especificação que o desenvolvedor terceirizou para um modelo sem perceber que estava fazendo isso.
Esse cenário se repete todos os dias em equipes que já dominam a mecânica de “conversar com a IA” mas nunca formalizaram como isso deveria funcionar em código de produção. A discussão pública sobre o tema, como aponta este texto sobre o deslocamento do trabalho do desenvolvedor para a escrita de prompts, tende a tratar o prompt como um substituto direto do código. Não é. Prompt é especificação informal, e especificação informal sempre teve um custo — só que agora esse custo aparece como bug em produção em vez de aparecer como retrabalho na fase de design. Este artigo trata prompt engineering para código como o que ele realmente é: uma disciplina de especificação com suas próprias falhas de modo, seus próprios trade-offs de verbosidade versus liberdade do modelo, e seus próprios limites estruturais que nenhuma técnica de prompt resolve sozinha.
O prompt não é uma pergunta, é um contrato incompleto por padrão
A maioria dos desenvolvedores trata o prompt como uma pergunta em linguagem natural. Isso funciona para tarefas exploratórias — “como funciona o algoritmo de Dijkstra” — mas falha sistematicamente para geração de código de produção, porque código de produção não é definido pelo que ele faz no caso feliz, e sim pelo que ele faz nos casos de borda, nas condições de falha e nas restrições não funcionais que nunca aparecem no enunciado do problema.
Um LLM, na ausência de restrições explícitas, preenche essas lacunas com o padrão estatisticamente mais frequente no seu corpus de treino. Isso significa que ele vai gerar tratamento de erro genérico, vai assumir que strings são UTF-8 bem formado, vai assumir que listas cabem em memória, e vai assumir que a operação pode ser refeita sem efeito colateral — porque essas são as suposições mais comuns em código de exemplo disponível publicamente. Nenhuma dessas suposições é necessariamente errada. O problema é que elas são implícitas, e decisões implícitas em código de produção são decisões que ninguém revisou.
💡 Dica do Mestre: pense no prompt como a especificação que você daria a um desenvolvedor júnior extremamente rápido e sem contexto de negócio. Ele vai produzir exatamente o que você pediu, na leitura mais literal possível, e vai preencher qualquer ambiguidade com a interpretação mais genérica. Se você não especifica idempotência, ele não vai adivinhar que sua operação de pagamento precisa dela.
O framework Função → Tarefa → Detalhes → Contexto → Exemplos
Uma estrutura discutida em uma thread de desenvolvedores sobre prompts para geração de código propõe uma sequência que vale a pena formalizar como checklist de revisão, não como fórmula rígida: definir a função que o modelo deve assumir (revisor sênior, arquiteto, implementador), a tarefa específica, os detalhes técnicos não negociáveis, o contexto do sistema existente e exemplos de código que já seguem o padrão esperado.
Na prática, isso significa que um prompt de produção raramente cabe em uma linha. Compare:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// Prompt fraco "Crie uma função de retry para chamadas HTTP" // Prompt com especificação de contrato "Implemente uma função de retry para chamadas HTTP em TypeScript com estas restrições: - Máximo de 3 tentativas, backoff exponencial com jitter - Retry apenas para status 429, 502, 503, 504 e erros de timeout de rede - NÃO fazer retry em erros 4xx exceto 429 (evitar reenviar requisições inválidas) - A função deve receber um AbortSignal externo e respeitá-lo - Deve ser idempotente: assuma que o chamador garante idempotência da operação, mas documente essa premissa explicitamente no JSDoc - Retornar um Result em vez de lançar exceção genérica - Exemplo de assinatura esperada: retryFetch(url, options, retryConfig): Promise<Result>" |
A diferença de tamanho entre os dois prompts é proposital. O segundo não é “mais educado” — ele elimina graus de liberdade que, se deixados abertos, o modelo vai preencher com a opção mais genérica, não com a opção correta para o seu sistema.
Restrições negativas valem mais que instruções positivas
Um padrão que raramente aparece em tutoriais introdutórios, mas que faz diferença mensurável em código gerado para produção, é a restrição negativa — dizer explicitamente o que o modelo não deve fazer. Isso é contraintuitivo porque a maioria dos guias de prompt foca em descrever o resultado desejado, mas um LLM treinado em bilhões de exemplos de código carrega vieses fortes para padrões específicos que muitas vezes são exatamente o que você quer evitar: uso de bibliotecas desatualizadas, padrões de acesso a dados que não escalam, tratamento de exceção que engole o erro original.
Considere este exemplo em Python:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
# Prompt sem restrição negativa tende a gerar isto: def process_batch(items): try: results = [] for item in items: results.append(transform(item)) return results except Exception as e: print(f"Erro: {e}") return [] # Prompt com restrição negativa explícita: # "Não capture exceções genéricas. Não use print para logging. # Se um item falhar, ele deve ser reportado individualmente, # e o processamento dos demais itens deve continuar." def process_batch(items: list[Item]) -> BatchResult: successes: list[TransformedItem] = [] failures: list[ItemError] = [] for item in items: try: successes.append(transform(item)) except TransformationError as e: logger.warning("Falha ao transformar item %s: %s", item.id, e) failures.append(ItemError(item_id=item.id, reason=str(e))) return BatchResult(successes=successes, failures=failures) |
A primeira versão não está “errada” no sentido sintático — ela compila, roda, e em uma demonstração ao vivo parece funcionar. Mas ela mascara falhas, perde a exceção original e não distingue item que falhou de item que nunca foi processado. Nenhuma dessas falhas aparece em um teste superficial. Elas aparecem em produção, três meses depois, quando alguém precisa descobrir por que 12% dos registros de um lote sumiram silenciosamente.
Contexto de arquitetura como parte obrigatória do prompt, não como extra
Outro erro recorrente em times que adotaram ferramentas como Cursor ou Claude Code é tratar cada prompt como isolado, sem ancorá-lo nas convenções arquiteturais do projeto. Um modelo sem esse ancoramento vai gerar código estilisticamente correto, mas arquiteturalmente inconsistente — uma camada de repositório que acessa o banco diretamente em um projeto que segue rigorosamente Clean Architecture, por exemplo, ou um serviço que lança exceções em um codebase que padronizou Result types.
Isso não é resolvido pedindo “siga os padrões do projeto” de forma genérica — essa instrução é vaga o suficiente para não restringir nada. O que funciona é apontar exemplos concretos do próprio código como referência de padrão:
|
1 2 3 4 5 6 7 8 9 10 11 |
// Prompt ancorado em exemplo real do projeto "Implemente o UserRepository seguindo exatamente o mesmo padrão usado em OrderRepository (arquivo src/repositories/order-repository.ts): - Injeção de dependência do Pool via construtor - Métodos retornam Either usando fp-ts - Erros de constraint violation do Postgres são mapeados para DuplicateEntityError, não propagados como PostgresError bruto - Toda query usa prepared statement, nunca template string" |
Ferramentas com contexto persistente de projeto — arquivos de regras, memória de sessão, indexação de codebase — reduzem parcialmente essa necessidade de repetir contexto a cada prompt, mas não eliminam a necessidade de você, desenvolvedor, decidir explicitamente qual arquivo é a referência canônica de padrão. O modelo não sabe, por conta própria, que order-repository.ts é o exemplo “certo” e não um código legado que está sendo descontinuado.
O prompt como artefato versionado, não como conversa descartável
Uma mudança de mentalidade que separa uso amador de uso profissional de IA generativa é tratar prompts recorrentes como código: versionados, revisados e reutilizados, em vez de reescritos do zero a cada sessão. Isso é especialmente relevante para tarefas repetitivas de geração — criação de endpoints CRUD, migrations, testes de unidade seguindo um padrão específico.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
## prompts/gerar-endpoint-crud.md Você é um engenheiro backend sênior trabalhando neste projeto FastAPI. Ao gerar um endpoint CRUD, siga SEMPRE: 1. Schema Pydantic de entrada separado do schema de saída (nunca reutilize o mesmo model) 2. Validação de autorização via dependency `require_permission(...)`, nunca inline no corpo da função 3. Toda exceção de domínio deve herdar de `DomainError` (ver `core/errors.py`) e ser capturada pelo exception handler global — não trate erro dentro do endpoint 4. Paginação obrigatória em endpoints de listagem, usando `PageParams` já definido em `core/pagination.py` 5. NUNCA exponha o model do SQLAlchemy diretamente na resposta Exemplo de referência completo: `api/routes/products.py` Contexto adicional desta tarefa: {{contexto_especifico}} |
Esse arquivo de prompt, mantido no repositório e referenciado por convenção — seja via arquivo de instruções do Cursor, seja via CLAUDE.md no caso do Claude Code — funciona como um contrato reutilizável. Ele elimina a variância entre sessões diferentes, entre desenvolvedores diferentes e entre dias diferentes do mesmo desenvolvedor. Sem esse artefato, cada pessoa do time reinventa a especificação a cada prompt, e a consistência do código gerado vira função de quem lembrou de mencionar o quê naquele dia.
💡 Dica do Mestre: um material introdutório sobre boas práticas de prompt, disponível em esta mentoria prática sobre prompts claros e seguros, reforça um ponto que vale para times avançados também: comandos claros reduzem retrabalho, mas comandos claros e não versionados reduzem retrabalho apenas uma vez. A economia recorrente só existe quando o prompt vira artefato reutilizável.
Prompt encadeado versus prompt monolítico: quando dividir a tarefa
Um erro de escala é tentar resolver problemas complexos com um único prompt gigante, esperando que o modelo produza, de uma vez, uma solução completa e correta em todas as camadas — arquitetura, implementação, testes e tratamento de erro. Isso funciona para tarefas pequenas e falha de forma proporcional à complexidade da tarefa, porque a probabilidade de erro composto cresce a cada decisão implícita que o modelo precisa tomar sem checkpoint intermediário.
A alternativa é decompor a tarefa em uma cadeia de prompts com verificação humana entre etapas — o que, em ferramentas de agente, se aproxima do que já foi discutido no contexto de como a interação com IA na programação está evoluindo de comandos isolados para fluxos estruturados. Na prática:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
Etapa 1 — Especificação: "Antes de gerar código, liste as decisões de design que precisam ser tomadas para implementar um sistema de filas com reprocessamento de mensagens mortas (DLQ). Não escreva código ainda." Etapa 2 — Revisão humana das decisões listadas (aqui você intervém) Etapa 3 — Implementação: "Com base nas decisões que confirmamos [lista revisada], implemente o consumidor da fila em Go seguindo essas decisões exatamente." Etapa 4 — Testes: "Gere os testes de unidade para este consumidor, cobrindo especificamente os cenários de falha que definimos na etapa 1: mensagem malformada, timeout do processamento, falha ao mover para DLQ." |
Essa decomposição custa mais tempo por interação, mas reduz drasticamente o volume de suposições implícitas empilhadas em uma única resposta. É o mesmo princípio de engenharia de software que já conhecemos de outros contextos: dividir um problema grande em unidades menores e verificáveis reduz a superfície de erro não detectado — só que aqui a unidade de trabalho é uma etapa de prompt em vez de uma função.
Onde a engenharia de prompt encontra seu limite estrutural
É importante ser honesto sobre o que prompt engineering não resolve, porque parte do discurso ao redor do tema — inclusive em materiais introdutórios como os apontados em conteúdos sobre como escrever prompts melhores para programação — foca na técnica de escrita da instrução, mas subestima os limites do próprio modelo diante de contexto real de sistema.
- Conhecimento tácito de negócio não cabe em prompt. Regras de negócio que existem apenas na cabeça de um analista sênior, ou em uma reunião que ninguém documentou, não podem ser especificadas em um prompt porque você mesmo não as tem por escrito. Nesse caso, o gargalo não é a técnica de prompting — é a ausência de especificação, que precede qualquer ferramenta de IA.
- Estado distribuído e efeitos colaterais em sistemas complexos são difíceis de descrever textualmente. Um prompt consegue descrever bem uma função pura. Descrever com precisão as interações entre cinco microsserviços, cache distribuído e um circuit breaker em texto corrido é propenso a omissões, porque a própria linguagem natural é um formato ruim para representar grafos de dependência.
- Prompt não substitui teste, ele apenas move o ponto de falha. Um prompt bem escrito reduz a chance de erro, mas nunca elimina a necessidade de um harness de testes que valide o comportamento gerado contra casos reais — inclusive casos que você não pensou em mencionar no prompt.
- Verbosidade excessiva tem custo de atenção do próprio modelo. Prompts longos demais, com regras genéricas empilhadas sem hierarquia de prioridade, competem entre si por relevância dentro da janela de contexto. Um prompt com 40 regras onde todas têm o mesmo peso textual tende a produzir aderência inconsistente, porque o modelo não tem sinal claro de qual restrição é inegociável e qual é preferência estética.
Priorização explícita de restrições dentro do prompt
Uma técnica pouco discutida, mas eficaz para prompts longos, é hierarquizar explicitamente as restrições por criticidade, em vez de listá-las como uma sequência plana:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
## Restrições CRÍTICAS (violação = código rejeitado) - Nunca fazer commit de credenciais ou secrets hardcoded - Toda query SQL deve usar parâmetros bindados, sem exceção - Operações financeiras devem ser transacionais (BEGIN/COMMIT explícito) ## Restrições IMPORTANTES (desvio precisa de justificativa no comentário) - Preferir composição a herança - Funções com mais de 40 linhas devem ser justificadas ou quebradas ## Preferências (aplicar quando não conflitar com o acima) - Nomes de variáveis em inglês - Preferir arrow functions em TypeScript |
Essa estrutura em camadas dá ao modelo um critério de desempate quando restrições entram em conflito — cenário mais comum do que parece, especialmente em bases de código legadas com convenções inconsistentes entre módulos.
Comunidade Dev’s AI: prática deliberada em vez de tentativa isolada
Tudo o que foi discutido aqui — restrições negativas, prompts versionados, decomposição em cadeia, priorização de regras — é uma disciplina que se desenvolve por repetição e feedback, não por leitura isolada de um artigo. Testar essas técnicas contra código real, discutir com outros desenvolvedores sênior o que funcionou e o que gerou dívida técnica silenciosa, e ajustar seus templates de prompt com base em casos concretos de falha é o tipo de prática que acelera exponencialmente quando feita em grupo.
Se você quer trocar experiências reais sobre engenharia de prompt aplicada a produção — não teoria genérica, mas casos de bugs gerados por prompts mal especificados e como cada um resolveu — participe da Comunidade Dev’s AI. É onde essas técnicas são testadas, quebradas e refinadas coletivamente, com desenvolvedores que já passaram pelos mesmos problemas de especificação implícita que este artigo descreveu.
Conclusão
Escrever prompts que geram código melhor não é uma técnica de retórica aplicada a chatbots — é engenharia de especificação, com as mesmas armadilhas que sempre existiram em requisitos mal escritos, só que agora com um novo executor que preenche ambiguidade com a média estatística do que já viu, em vez de perguntar. A diferença entre um prompt de amador e um prompt de profissional não está na educação da linguagem ou na quantidade de emojis usados para “engajar” o modelo — está na precisão das restrições, na explicitação do que não deve acontecer, no ancoramento em exemplos reais do seu próprio código e na disciplina de tratar esse artefato como parte do processo de engenharia, versionado e revisado como qualquer outro componente crítico do sistema. O código gerado por IA só é tão bom quanto a especificação que você conseguiu tornar explícita — e essa é, no fim, a mesma regra que sempre valeu para qualquer desenvolvedor júnior que você já treinou.