Objetivo da Aula
Construir templates de prompt parametrizados e reutilizáveis que seguem as mesmas boas práticas de código de produção
Implementar chaining de prompts onde a saída de um estágio alimenta o próximo, com gestão de contexto
Aplicar estratégias concretas de anti-alucinação que reduzem outputs inventados em produção
Medir e comparar qualidade de prompts com zero-shot, few-shot e chain-of-thought
Estruturar um pipeline de prompt que seja testável, versionável e maintainable
Por que isso importa
Prompt engineering é onde a maioria dos produtos de IA acaba sendo ganha ou perdida — não no modelo, não na infraestrutura. Dois produtos usando o mesmo Claude, com prompts diferentes, podem ter qualidades radicalmente diferentes.
Um exemplo real: um sistema de suporte ao cliente com prompt genérico (“Você é um assistente útil”) vai inventar políticas que não existem quando o usuário perguntar sobre reembolso. Com um prompt bem estruturado (“Você é um agente de suporte. Se não tiver certeza sobre uma política, diga ‘Deixe-me verificar isso para você’ e NÃO invente informações”), o mesmo modelo se comporta de forma radicalmente diferente.
O custo de alucinações em produção é real: - Jurídico: sistema de IA citou lei inexistente em briefing para advogados — empresa processada - Financeiro: chatbot de banco ofereceu taxa de juros que não existia — empresa teve que honrar centenas de contratos - Reputação: assistente de saúde deu orientação médica incorreta com confiança total
Prompt engineering não é “magia” — é engenharia de software. E como código ruim, prompts ruins têm custo em produção.
Conceitos Fundamentais
Anatomia de um prompt eficaz
Um prompt completo tem quatro componentes:
# 1. System prompt: define o papel, tom e regras absolutas
system = """Você é um analista técnico especializado em segurança.
Responda sempre em português.
Se não tiver certeza sobre algo, diga explicitamente.
NUNCA invente especificações técnicas ou números."""
# 2. Contexto: informações relevantes para a tarefa
# (dados do usuário, conteúdo de documento, histórico relevante)
contexto = f"""
Produto em análise: {nome_produto}
Versão: {versao}
Logs relevantes: {logs}
"""
# 3. Instrução: o que fazer com o contexto
instrucao = "Identifique vulnerabilidades de segurança nos logs acima."
# 4. Formato de saída: como estruturar a resposta
formato = """Responda no formato:
VULNERABILIDADE: [nome]
SEVERIDADE: [Alta/Média/Baixa]
EVIDÊNCIA: [trecho específico dos logs]
MITIGAÇÃO: [ação concreta]"""
prompt_completo = f"{contexto}\n\n{instrucao}\n\n{formato}"Zero-shot, Few-shot e Chain-of-Thought
Zero-shot: instrução sem exemplos. Funciona bem para tarefas simples e bem definidas.
# Zero-shot: direto ao ponto
prompt = "Classifique o sentimento deste tweet como POSITIVO, NEGATIVO ou NEUTRO: '{tweet}'"Few-shot: exemplos dentro do prompt orientam o formato e padrão esperados.
# Few-shot: exemplos ensinam o formato
prompt = """Classifique o sentimento:
Tweet: "Adorei o atendimento, muito rápido!"
Sentimento: POSITIVO
Tweet: "Travou pela décima vez hoje."
Sentimento: NEGATIVO
Tweet: "O app foi atualizado."
Sentimento: NEUTRO
Tweet: "{tweet_novo}"
Sentimento:"""Few-shot é especialmente útil quando: - O formato da saída é complexo ou específico - A tarefa tem nuances que não são óbvias na instrução - Você quer consistência estrita no formato
Chain-of-Thought (CoT): pede ao modelo para “pensar passo a passo” antes da resposta final.
# CoT: raciocínio antes da conclusão
prompt = """Problema: {problema}
Pense passo a passo:
1. Quais informações relevantes temos?
2. Quais são as possíveis abordagens?
3. Qual abordagem é mais adequada e por quê?
Conclusão final:"""CoT melhora significativamente em tarefas de raciocínio multi-passo (matemática, planejamento, debugging). Para classificações simples, adiciona tokens desnecessários.
Templates: prompts como código
Um PromptTemplate é um prompt com variáveis substituíveis — o equivalente de uma função em código de prompt:
# Sem template: string hardcoded, difícil de manter
prompt = f"Resuma o seguinte texto em 3 pontos: {texto_longo_do_usuario}"
# Com template: parametrizado, testável, versionável
template = PromptTemplate(
template="Resuma o seguinte texto em {num_pontos} pontos:\n\n{texto}",
system="Você é especialista em síntese de textos técnicos."
)
# Reutilizável para qualquer número de pontos ou texto
resumo_3 = template.executar(num_pontos=3, texto=texto1)
resumo_5 = template.executar(num_pontos=5, texto=texto2)Vantagens de templates: - Testabilidade: você pode testar com inputs diferentes sistematicamente - Versionamento: mudanças no template são rastreáveis no git como mudanças de código - Reutilização: o mesmo template serve para múltiplos casos de uso - Separação de concerns: lógica de negócio separada da formulação do prompt
Chaining: pipeline de prompts
Chaining conecta prompts em sequência, onde cada saída alimenta o próximo:
Decisão de Arquitetura: Quando Decompor em Prompt Chain
Um prompt único para tarefas complexas produz qualidade inferior porque o modelo tenta fazer tudo ao mesmo tempo. Decomponha em chain quando: a tarefa tem etapas naturalmente sequenciais com saídas intermediárias verificáveis; cada etapa pode ser cacheada (reduz custo em execuções repetidas); você precisa debugar uma etapa sem reexecutar as anteriores. Custos da decomposição: latência em série (etapas não paralelizáveis somam latências), custo somado de tokens (cada estágio tem input e output separados), e propagação de erro (um estágio com saída ruim contamina os seguintes). Use chain quando qualidade importa mais que latência ou custo; use prompt único quando latência é crítica e a tarefa cabe numa instrução clara.
Entrada → [Prompt 1: Extrair] → [Prompt 2: Analisar] → [Prompt 3: Formatar] → SaídaPor que isso é melhor que um prompt gigante? 1. Qualidade: cada etapa pode ser otimizada independentemente 2. Debugabilidade: você vê o que cada etapa produziu 3. Reutilização: etapas podem ser recombinadas 4. Custo: etapas anteriores podem ser cacheadas
Exemplo de chain:
Descrição de produto
→ Prompt 1: "Extraia requisitos técnicos"
→ "- Latência < 100ms\n- Uptime 99.9%\n- LGPD compliant"
→ Prompt 2: "Com esses requisitos, gere tasks de desenvolvimento"
→ "1. [Alta] Implementar circuit breaker..."
→ Saída final: lista de tasks priorizadaAprofundamento Técnico
Estratégias anti-alucinação
Alucinação ocorre quando o modelo gera informações plausíveis mas incorretas com aparente confiança. As estratégias a seguir reduzem (mas não eliminam) esse problema:
Estratégia 1: Instrução explícita de incerteza
system = """Regras absolutas:
- Se não tiver certeza, diga "Não tenho informação sobre isso"
- NUNCA invente números, datas ou nomes
- Prefira "Não sei" a dar informação incorreta"""Decisão de Arquitetura: Limites das Técnicas Anti-Alucinação
LLMs alucinam porque geram o token mais provável, não o token verdadeiro — são máquinas de probabilidade, não de fato. As técnicas do capítulo reduzem alucinação mas não a eliminam. Grounding (ancorar no contexto fornecido) ajuda mais quando o contexto é suficiente e claro; self-consistency (múltiplas gerações, voto majoritário) dobra custo e melhora consistência, não acurácia absoluta. Para sistemas regulados (saúde, jurídico, financeiro), essas técnicas não substituem: aprovação humana em saídas de alto impacto, logging completo de input/output para auditoria, e canal explícito para contestação. A combinação correta é: técnicas de prompt para reduzir frequência + controles organizacionais para capturar as que passam.
Estratégia 2: Grounding no contexto fornecido
# Em vez de perguntar ao modelo "o que você sabe sobre X":
prompt = """Com base APENAS nas informações a seguir:
{documento_de_referencia}
Responda: {pergunta}
Se a resposta não estiver no texto acima, diga "Essa informação não está no documento fornecido"."""Estratégia 3: Verificação de confiança (self-consistency)
# Primeira chamada: gera resposta
resposta = modelo.perguntar(pergunta)
# Segunda chamada: pede para verificar a própria resposta
verificacao = modelo.perguntar(f"""
A seguinte resposta está correta? Identifique qualquer afirmação que possa ser incorreta:
RESPOSTA: {resposta}
Liste as afirmações problemáticas ou confirme que a resposta está correta.""")Self-consistency dobra o custo de tokens, mas para decisões de alto impacto (diagnóstico, análise jurídica, decisões financeiras) o custo é justificado.
Estratégia 4: Output estruturado com campo de confiança
prompt = """Responda em JSON:
{
"resposta": "...",
"confianca": "alta|média|baixa",
"motivo_incerteza": "null ou explicação de por que pode estar errado"
}"""Retry com exponential backoff
APIs de LLM têm rate limits e falhas transitórias. Implementar retry robusto é essencial em produção:
import time
import anthropic
def chamar_com_retry(client, max_tentativas=3, **kwargs):
for tentativa in range(max_tentativas):
try:
return client.messages.create(**kwargs)
except anthropic.RateLimitError:
# Rate limit: espera exponencial (2^tentativa segundos)
espera = 2 ** tentativa
print(f"Rate limit. Aguardando {espera}s...")
time.sleep(espera)
except anthropic.APIError as e:
if e.status_code >= 500: # Erro de servidor: pode tentar novamente
time.sleep(2 ** tentativa)
else:
raise # Erro de cliente (400): não vai melhorar com retry
raise Exception(f"Falhou após {max_tentativas} tentativas")Cache de system prompt: redução de custo de 90%+
Anthropic tem prompt caching: se o system prompt é idêntico entre chamadas, você paga apenas 10% do custo de tokens cacheados:
Fundamento: Prompt Caching — Prefixo Contíguo
O cache de prompt funciona por prefixo de tokens byte-idêntico: todos os tokens antes do marcador de cache devem ser idênticos ao da requisição anterior. Qualquer alteração — incluindo um espaço — invalida o cache a partir daquele ponto. Por isso o padrão de design é estático primeiro, dinâmico depois: system prompt, documentos de referência e exemplos ficam no início (cacheados); a pergunta do usuário fica no fim (nunca cacheada). O TTL é curto (minutos). Em múltiplas requisições concorrentes ao mesmo prefixo, o primeiro cria o cache; os demais leem. O hit rate real em produção é menor que 100% — leve isso em conta ao projetar economia de custo.
# Sem cache (paga system prompt inteiro a cada chamada):
# 10 chamadas × 5.000 tokens de system prompt × $0.000003 = $0.15
# Com cache (paga 100% apenas na primeira, 10% nas demais):
# 1ª chamada: 5.000 tokens × $0.000003 = $0.015
# 9 chamadas: 5.000 tokens × $0.00000030 = $0.0135
# Total: $0.0285 — 80% de economia!
params = {
"model": "claude-haiku-4-5-20251001",
"system": [
{
"type": "text",
"text": system_prompt_longo,
"cache_control": {"type": "ephemeral"} # habilita caching
}
],
"messages": [{"role": "user", "content": pergunta}]
}FinOps: Hit Rate Realista e Métricas de Cache
A economia de 80-90% é o cenário de hit rate máximo. Na prática, o hit rate é afetado por: (a) variação no prefixo entre requisições do mesmo usuário; (b) TTL curto — requisições espaçadas mais de ~5 minutos perdem o cache; (c) concorrência — múltiplos usuários com prefixos diferentes competem pelo mesmo slot. A API retorna cache_creation_input_tokens e cache_read_input_tokens separados no campo usage. Monitore esses campos em produção para medir hit rate real. Economia projetada = (cache_read_tokens × 0.9 × preço_input) — (cache_creation_tokens × 0.25 × preço_input).
Exemplos Anotados
Exemplo 1: PromptTemplate com validação e logging
import anthropic
import os
client = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
class PromptTemplate:
def __init__(self, template: str, system: str = ""):
self.template = template
self.system = system
# Extraímos as variáveis esperadas do template para validação
# {nome} e {texto} viram {'nome', 'texto'}
import string
formatter = string.Formatter()
self.variaveis_esperadas = {
v for _, v, _, _ in formatter.parse(template) if v
}
def render(self, **kwargs) -> str:
# Validamos que todas as variáveis foram fornecidas
# Isso evita KeyError silencioso no .format()
faltando = self.variaveis_esperadas - set(kwargs.keys())
if faltando:
raise ValueError(f"Variáveis obrigatórias ausentes: {faltando}")
return self.template.format(**kwargs)
def executar(self, modelo="claude-haiku-4-5-20251001", max_tokens=500, **kwargs) -> str:
prompt_renderizado = self.render(**kwargs)
params = {
"model": modelo,
"max_tokens": max_tokens,
"messages": [{"role": "user", "content": prompt_renderizado}]
}
if self.system:
params["system"] = self.system
resposta = client.messages.create(**params)
# Logamos uso para monitoramento de custo
print(f" Tokens: {resposta.usage.input_tokens} in, {resposta.usage.output_tokens} out")
return resposta.content[0].text
# Uso: template de resumo reutilizável
resumidor = PromptTemplate(
template="Resuma em {num_pontos} pontos:\n\n{texto}",
system="Especialista em síntese técnica. Seja objetivo e preciso."
)
resultado = resumidor.executar(num_pontos=3, texto="texto longo aqui...")
print(resultado)Como o arquiteto lê este código
A classe PromptTemplate valida que você passou todas as variáveis que o template precisa antes de renderizar. __init__ é o construtor — executa quando você cria um objeto. self é a referência ao objeto atual, como this em Java ou JavaScript. **kwargs recebe argumentos nomeados em quantidade variável — template.formatar(nome="João", data="2024") chega como {"nome": "João", "data": "2024"}. A operação self.variaveis_esperadas - set(kwargs.keys()) usa subtração de conjuntos para encontrar variáveis esperadas que não foram passadas. Se o resultado não for vazio, lança erro antes de renderizar. O padrão é: defina o contrato no construtor, valide na chamada, execute só se válido.
Exemplo 2: Chain de prompts com contexto acumulado
class ChainPrompt:
def __init__(self):
self.etapas = []
def adicionar_etapa(self, nome: str, template: PromptTemplate):
self.etapas.append((nome, template))
return self # permite encadeamento: chain.adicionar_etapa(...).adicionar_etapa(...)
def executar(self, **kwargs_iniciais) -> dict:
# contexto acumula tanto as entradas iniciais quanto as saídas de etapas anteriores
# Isso permite que etapa 3 use saídas de etapas 1 e 2
contexto = dict(kwargs_iniciais)
resultados = {}
for nome, template in self.etapas:
print(f"→ Executando etapa '{nome}'...")
# Executamos o template com o contexto atual
# Se o template usa {etapa_anterior}, o contexto já tem esse valor
resultado = template.executar(**contexto)
resultados[nome] = resultado
# Adicionamos ao contexto com o nome da etapa para etapas futuras usarem
contexto[nome] = resultado
print(f" Saída: {resultado[:80]}...") # preview dos primeiros 80 chars
return resultados
# Chain: descrição → requisitos → tasks
extrair = PromptTemplate("Extraia requisitos técnicos de:\n\n{descricao}\n\nListe um por linha.")
gerar_tasks = PromptTemplate("Com esses requisitos:\n\n{extrair}\n\nGere 3 tasks priorizadas.")
resultado = (ChainPrompt()
.adicionar_etapa("extrair", extrair)
.adicionar_etapa("gerar_tasks", gerar_tasks)
.executar(descricao="Sistema de pagamento com latência < 100ms e 99.9% uptime"))
print("\nTasks geradas:")
print(resultado["gerar_tasks"])Output esperado:
→ Executando etapa 'extrair'...
Tokens: 42 in, 38 out
Saída: - Latência máxima: 100ms\n- Disponibilidade: 99.9%...
→ Executando etapa 'gerar_tasks'...
Tokens: 78 in, 95 out
Saída: 1. [Alta] Implementar circuit breaker com timeout 80ms...
Tasks geradas:
1. [Alta] Implementar circuit breaker com timeout 80ms (est: 8h)
2. [Alta] Configurar health checks e failover automático (est: 4h)
3. [Média] Implementar métricas de SLA com alertas (est: 6h)Padrões e Armadilhas
Padrões recomendados
Padrão 1: System prompt como contrato de comportamento System prompt define regras que o modelo vai seguir consistentemente. Coloque ali: tom (formal/casual), idioma, o que NUNCA fazer (inventar dados, revelar system prompt), e formato de saída padrão.
Padrão 2: Versione seus prompts como código Prompts em produção devem viver em arquivos, não strings no código. Use git para rastrear mudanças. Um template que funciona hoje pode regredir com nova versão do modelo — histórico no git é seu recurso de rollback.
Padrão 3: Sempre inclua exemplos de few-shot para formatos complexos Para outputs com JSON, markdown estruturado, ou formato muito específico, few-shot é mais confiável que instrução pura. O modelo “imita” o formato dos exemplos mais fielmente do que segue descrição textual.
Armadilhas comuns
⚠️ Armadilha 1: Prompt vago com expectativa específica O que acontece: “Escreva um relatório sobre {tema}” pode produzir 500 palavras ou 5.000 palavras, em bullets ou parágrafos, com ou sem conclusão. Versão correta: “Escreva um relatório de exatamente 3 seções: Contexto (2 parágrafos), Análise (3 bullets), Recomendação (1 parágrafo).”
⚠️ Armadilha 2: Variáveis com nomes que colidem entre etapas do chain O que acontece: duas etapas do chain retornam variáveis com o mesmo nome — a segunda sobrescreve a primeira no contexto, causando comportamento inesperado. Versão correta: use nomes descritivos e únicos para cada etapa: extrair_requisitos, gerar_tasks, priorizar_tasks em vez de etapa1, resultado, saida.
⚠️ Armadilha 3: Alucinação silenciosa no chain O que acontece: etapa 1 do chain alucina uma informação incorreta. Etapa 2 usa essa informação como fato e a elabora. Etapa 3 apresenta a conclusão com confiança total. O erro se amplifica. Versão correta: adicione validação entre etapas críticas — um prompt de “revisão” que verifica se o output da etapa anterior é consistente com os dados de entrada originais.
Se não for realizar o laboratório, pule para o próximo capítulo.
Ponte para o Lab
O starter tem 1 TODO principal:
TODO 1 — ChainPrompt.executar() Seção de referência: “Conceitos Fundamentais → Chaining: pipeline de prompts” e “Exemplos Anotados → Exemplo 2”.
Implemente o corpo do loop for nome, template in self.etapas::
resultado = template.executar(**contexto)
resultados[nome] = resultado
contexto[nome] = resultadoO ponto crítico é contexto[nome] = resultado — adicionar ao contexto com o NOME da etapa (não um nome fixo como “resultado”). Isso é o que permite que etapas futuras referenciem saídas de etapas anteriores como variáveis de template.
Dica para o TODO mais difícil: se o template da etapa 2 usa {nome_etapa_1} como variável, e você adicionou ao contexto como contexto["nome_etapa_1"] = ..., então template.executar(**contexto) vai encontrar a variável. Se não encontrar, o PromptTemplate.render() vai lançar KeyError — use esse erro como debug para descobrir qual nome está sendo esperado vs qual nome você usou.
Agora você está pronto para o lab.