Do prompt frágil ao sistema confiável: pipeline de eval contínuo para prompts em produção
Sexta de manhã. Você troca claude-opus-4-6 por claude-opus-4-7 num system prompt de classificação de tickets. Sobe para produção, abre o Slack e vai tomar café. Segunda, o time de suporte avisa: 30% dos tickets de "reembolso" estão indo para a fila de "logística". O prompt está igual. O modelo é supostamente melhor. Quem regrediu?
Esse tipo de bug não aparece no phpunit. Não aparece no review do PR. Aparece no cliente. E a maior parte dos times que coloca LLM em produção descobre isso do mesmo jeito: pelo prejuízo.
A solução não é mais um framework. É tratar prompt como código. Dataset de teste versionado, eval rodando em cada PR, threshold barrando merge quando a qualidade cai. Neste post vamos montar essa pipeline ponta a ponta com Promptfoo e GitHub Actions, integrando com o que a Anthropic chama de eval-driven development.
TL;DR
- O que é: pipeline de avaliação contínua que trata prompts como código versionado, com gate de qualidade no CI.
- Stack: Promptfoo (open-source, MIT), GitHub Actions, golden dataset em YAML, judge code-based + LLM-as-judge.
- Custo/Acesso: Promptfoo é gratuito. Pago = chamadas de API do modelo gerador e do modelo grader.
- Repositório: github.com/promptfoo/promptfoo — usado por OpenAI e Anthropic, e agora parte da OpenAI mantendo licença MIT.
Por que prompt em produção precisa de CI
Prompt é código. Mas a maioria dos times trata como copy-paste de um Notion.
Mudou uma frase no system. Acrescentou um exemplo no few-shot. Trocou o modelo no fornecedor. Cada uma dessas três ações pode regredir 5%, 20%, 40% dos casos sem nenhum erro de runtime. O try/catch não pega. O linter não pega. O type checker não pega.
A Anthropic é direta sobre isso: evals devem rodar "em cada commit", e a distinção entre capability evals (medem o que o agente consegue fazer, começam com pass rate baixo) e regression evals (mantêm pass rate de ~100%, qualquer queda significa breakage) é o que torna isso operacional. Quando uma capability eval satura, ela "se gradua" para a suíte de regressão e passa a rodar continuamente. Tudo isso está descrito no guia Demystifying evals for AI agents.
Isso não é teoria. É a diferença entre detectar a regressão no PR e detectar via cliente irritado.
Arquitetura do pipeline
A pipeline tem cinco peças. Cada uma com responsabilidade clara.
- Golden dataset versionado — casos de teste em YAML/CSV, dentro do repositório do projeto. Vive no
git, evolui com PRs. - Runner de eval — Promptfoo executa cada caso contra um ou mais modelos.
- Grader — code-based para casos com resposta exata, LLM-as-judge para casos abertos.
- Quality gate — GitHub Actions calcula pass rate, falha o build se cair abaixo do threshold definido.
- Dashboard de regressão — relatório HTML/JUnit por execução + histórico para cruzar com mudanças no código.
O golden dataset é o coração. Sem dataset bom, todo o resto é teatro. A Anthropic recomenda começar com 20 a 50 casos vindos de falhas reais e bugs reportados, não com sets sintéticos genéricos. Cada caso precisa ter critério de pass/fail "tão claro que dois experts independentes chegariam ao mesmo veredito" (fonte).
Pré-requisitos
- Node 20+ instalado (Promptfoo roda via
npx). - Repositório no GitHub com Actions habilitado.
- Chave de API do modelo que você usa em produção (Claude, OpenAI, Gemini, etc).
- Chave de API de um modelo "judge" — geralmente um modelo diferente do gerador, conforme recomendação da Anthropic na docs de Define success criteria and build evaluations.
Mão na massa — montando o pipeline
Passo 1: estruturar o golden dataset
Crie eval/promptfooconfig.yaml na raiz do projeto. Esse arquivo é o contrato: prompts, providers, casos de teste e asserts. Versionado no git, igual qualquer outro código.
prompts:
- file://prompts/classifier-tickets.txt
providers:
- id: anthropic:messages:claude-opus-4-7
config:
temperature: 0
max_tokens: 50
tests:
- description: "reembolso explícito"
vars:
input: "Pedido chegou quebrado, preciso de reembolso"
assert:
- type: equals
value: "reembolso"
- description: "rastreamento de entrega"
vars:
input: "Quando chega meu pedido?"
assert:
- type: equals
value: "logistica"
- description: "reembolso implícito (caso que regrediu em prod)"
vars:
input: "Não quero mais o produto, comprei errado"
assert:
- type: equals
value: "reembolso"
Repare no terceiro caso. Esse é o tipo de teste que vale ouro: o caso real que quebrou em produção, congelado no dataset, vacinando o sistema contra a mesma regressão futura.
Passo 2: adicionar LLM-as-judge para casos abertos
Classificação tem resposta exata. Geração não. Para um endpoint que escreve resposta de atendimento, equals não serve. Aqui entra o LLM-as-judge com rubric.
- description: "resposta a cliente irritado"
vars:
input: "Pela terceira vez vocês erram meu pedido. QUERO MEU DINHEIRO DE VOLTA AGORA!"
assert:
- type: llm-rubric
provider: anthropic:messages:claude-opus-4-7
value: |
A resposta deve:
- reconhecer a frustração do cliente em uma frase, sem se desculpar 5 vezes
- explicar o próximo passo concreto para o reembolso
- não prometer prazo que não pode cumprir
- não usar emoji
Promptfoo suporta vários tipos de assert além do equals e llm-rubric: contains, contains-json, javascript (executa código arbitrário) e similar (similaridade semântica com threshold). Lista completa no guia de configuração.
Detalhe importante: o modelo grader idealmente é diferente do gerador. A própria docs da Anthropic reforça isso na seção de Build evaluations: "Generally best practice to use a different model to evaluate than the model used to generate the evaluated output". Se você gera com Claude, julgue com GPT, e vice-versa. Isso reduz o viés do grader.
Passo 3: integrar com GitHub Actions
Crie .github/workflows/prompt-eval.yml. Esse workflow só roda quando algo relevante muda — prompt, dataset ou config.
name: Prompt Eval
on:
pull_request:
paths:
- 'prompts/**'
- 'eval/**'
jobs:
eval:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Run eval
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
run: |
npx promptfoo@latest eval \
-c eval/promptfooconfig.yaml \
-o results.json \
-o report.html
- name: Quality gate
run: |
PASS_RATE=$(jq '.results.stats.successes / (.results.stats.successes + .results.stats.failures) * 100' results.json)
THRESHOLD=95
echo "Pass rate: ${PASS_RATE}% (threshold: ${THRESHOLD}%)"
if (( $(echo "$PASS_RATE < $THRESHOLD" | bc -l) )); then
echo "Quality gate failed"
exit 1
fi
- name: Upload report
if: always()
uses: actions/upload-artifact@v4
with:
name: eval-report
path: |
results.json
report.html
Pronto. Daqui em diante, qualquer PR que mexa em prompt vai rodar a eval. Se o pass rate cair de 95% para 88%, o merge é bloqueado, igual teste unitário falhando. O padrão completo de gate via jq está documentado nas integrações CI/CD do Promptfoo.
Passo 4: dashboard de regressão e alerta
O artefato report.html resolve o "o que aconteceu neste PR". Mas você quer também responder: "como estamos versus a semana passada?". Para isso, dois caminhos:
- Histórico no Promptfoo Cloud / Self-hosted: o
promptfoo viewagrega execuções e mostra evolução por caso. Bom para times pequenos. - Push para observabilidade existente: parsear o
results.jsone mandar métricas para Grafana, Langfuse ou W&B. Permite alerta padrão (Slack, PagerDuty) quando o pass rate de uma categoria de caso cai abaixo de N% em rolling window.
Padrão simples que funciona bem: além do eval no PR, agendar uma execução nightly contra o dataset completo (que pode ter centenas de casos), com um post para o canal de engenharia se algo regredir. O PR usa um sample (20-30 casos), o nightly usa o universo todo. Custo controlado, cobertura mantida.
A Evidently mostra um padrão parecido com a action própria deles para quem prefere outro stack.
Limitações e pontos de atenção
LLM-as-judge não é oráculo. Tem variação entre execuções, especialmente em rubrics vagos. Calibre periodicamente: pegue 50 casos, classifique manualmente, compare com o que o judge disse. Concordância abaixo de 80% significa que o rubric precisa ser reescrito ou o judge trocado. A Anthropic detalha isso na seção de grading do develop-tests: "encourage reasoning, have detailed clear rubrics, be empirical or specific".
Custo de eval cresce linearmente com dataset. Dataset de 500 casos x 2 modelos (gerador + judge) x cada PR = boleto no fim do mês. Estratégias para conter:
- Sample no PR (20-30 casos), full no nightly.
- Cache de respostas do judge para casos que não mudaram (Promptfoo tem flag
--no-cachepara desligar quando precisa). - Só rodar eval quando arquivos relevantes mudaram (
pathsno workflow).
Dataset estagnado vira falso negativo. Se faz seis meses que ninguém adiciona caso, é provável que sua produção tenha drift que o teste não vê. Cadência saudável: revisão semanal, todo bug que vira ticket vira caso novo no dataset.
Por fim: não confie em pass rate agregado. Pass rate de 96% pode esconder uma categoria inteira (ex: "perguntas com sarcasmo") com 40%. Quebre o relatório por tag/categoria de caso. O Promptfoo suporta metadata em cada teste justamente pra isso.
FAQ rápido
Quantos casos preciso para começar? Vinte. A Anthropic recomenda 20-50 tasks vindas de falhas reais e bugs reportados. Não pare para construir um dataset perfeito antes de subir o pipeline. Comece com os 20 piores casos que você já viu em produção e cresça a partir daí.
Posso rodar eval em cada PR sem estourar custo?
Sim, com sampling. Sample de 20-30 casos no PR para feedback rápido, full dataset (centenas de casos) no nightly via schedule no GitHub Actions. Custo previsível, cobertura mantida.
Como lidar com caso que o LLM-as-judge erra?
Reescreva o rubric mais específico ("não usar mais que 3 frases" em vez de "ser conciso"), ou troque o modelo grader, ou converta o caso para equals/contains se a resposta esperada for objetiva. Se nada disso resolve, marca como "human-graded" e tira do gate automático até calibrar.
Funciona com OpenAI, Claude, Gemini juntos? Sim. Promptfoo é multi-provider. Você pode rodar a mesma eval contra três modelos em paralelo e comparar pass rate side-by-side, exatamente o tipo de comparação que o Eval tool do Console da Anthropic também faz.
Conclusão
O salto de "prompt no Notion" para "prompt em produção com confiança" não é uma ferramenta nova. É um pipeline. Dataset versionado, runner em cada PR, grader honesto, threshold que barra merge, dashboard que mostra regressão.
Não precisa ser elegante na primeira versão. Precisa existir. Vinte casos no YAML, um workflow no GitHub Actions, um gate de pass rate. Em uma tarde está rodando. A partir daí, cada bug que vira caso novo no dataset é uma regressão que não vai voltar.
O próximo passo é tratar o próprio prompt como artefato versionado: hash no system, registro de qual versão atendeu cada request, rollback rápido quando uma versão regredir em produção mesmo passando no eval. PromptOps é a evolução natural do que vimos aqui, e fica para outro post.
Enquanto isso: se a sua aplicação já chama LLM em produção e você não tem isso rodando, você não tem engenharia. Tem sorte. E sorte não escala.
{AI Engineer} — apaixonado por Laravel, arquitetura de software e construir produtos com impacto. Compartilho aqui tutoriais, descobertas e reflexões sobre o dia a dia de engenharia.