02 - A-B testing de prompts

TL;DR

A/B testing de prompt é o experimento que valida o diff: mesmo input, dois prompts (control = champion, treatment = challenger), métrica primária pré-declarada, sample size suficiente, gate de decisão objetivo. Especificidade do contexto LLM: variância é alta (mesmo temperature=0 varia entre snapshots do modelo), N pequeno mente (200 por arm é piso razoável pra eval score; mais pra feedback do usuário), e bayesiano costuma ser mais adequado que frequentista (small N, prior de eval offline, decisão sequencial). Anti-padrão central: peeking — parar o experimento cedo porque o resultado “tá bom”. Tools 2026: Statsig, GrowthBook, Eppo pra A/B com estatística pronta; muitas equipes rodam custom em cima do registry de prompt (Langfuse Prompts, Braintrust) com cálculo bayesiano caseiro.

Você merge uma nova versão do prompt — v3.1 substituindo v3.0 — depois de revisar manualmente uns 20 exemplos e achar que “ficou mais claro”. Duas semanas depois de promover pra 100% do tráfego, o NPS cai 3 pontos. Foi o prompt? Foi uma mudança de produto? Foi ruído sazonal? Ninguém sabe, porque não havia um baseline controlado rodando ao lado — só a impressão de quem leu as amostras escolhidas.

É esse buraco que o A/B testing de prompt fecha: em vez de decidir “parece melhor” por leitura manual, você roda control (champion) e treatment (challenger) lado a lado, sob as mesmas condições, e deixa uma métrica pré-declarada decidir — não o julgamento subjetivo de quem revisou os exemplos “bons”.

A unidade de teste — o que é “control” e “treatment”

A/B de prompt mantém tudo igual menos o prompt:

VariávelControlTreatment
Promptv3.0 (champion)v3.1 (challenger)
Modeloclaude-sonnet-4-6claude-sonnet-4-6
Parâmetros de inferênciatemperature=0.2, top_p=0.9temperature=0.2, top_p=0.9
Inputmesmos casos do golden set OR mesma amostra de tráfego(idem)
Métrica primária(escolhida antes)(idem)

Se mudar mais de uma coisa por experimento (prompt + modelo, ou prompt + temperatura), você não consegue atribuir o ganho a uma causa específica. Uma variável por experimento é a regra que vale aqui.

Onde rodar — offline vs online

ModoO que medeQuando
Offline (eval set)Performance no golden setAntes do canary; gate de “vale ir pra prod?”
Online (canary em prod)Performance em tráfego realApós offline; valida em distribuição real
ShadowRoda treatment em paralelo sem servir ao usuárioQuando treatment é arriscado (ex: modelo novo) e quer comparar sem expor

Fluxo canônico:

offline (golden set, 100% treatment)
    ↓ passou no gate
canary online (5-10% tráfego treatment)
    ↓ N suficiente, métricas no verde
ramp up (50% treatment)
    ↓ confirma em volume
promoção (100% treatment vira novo champion)

Offline-only é tentador (rápido, barato) mas tem dois problemas: distribuição de input em prod difere do golden set (especialmente cauda longa), e métricas de negócio (conversão, retenção, NPS) só medem em prod.

Métrica primária — escolher antes de rodar

A regra de ouro: declarar a métrica antes do experimento. Sem isso, você (involuntariamente) escolhe a métrica que ganha — viés de seleção.

Opções comuns por tipo de produto:

Tipo de produtoMétrica primária candidata
Resposta a perguntasEval score (golden set + judge)
Sumário/extraçãoEval score por dim (faithfulness, completeness)
Geração estruturada% de output válido no schema
Agente com toolsTask completion rate
Chatbot com humanoRe-prompt rate, abandonment, thumbs feedback
RecomendaçãoClick-through, conversion downstream

Métricas secundárias (guardrails que não podem regredir) também declaradas antes:

  • Custo médio por request (não pode subir > X%)
  • Latência p95 (não pode subir > Y%)
  • Refusal rate (não pode mudar > Z pontos)
  • Safety violations (zero tolerância)

Decisão final precisa olhar a primária e as secundárias. Treatment ganhar 5% no primary mas duplicar custo = não vale.

Sample size — variância manda

LLM é estocástico mesmo com temperature=0 (snapshot do modelo varia, infra cache varia). Variância nas métricas é maior que em A/B clássico de produto. Regras de bolso:

MétricaN mínimo por armPor quê
Eval score (golden set, judge automático)~200Variância de judge + amostragem
User feedback (thumbs up/down)~1.000Sinal binário esparso
Conversão downstream~10.000Efeito de tamanho pequeno + ruído
Métrica contínua (NPS, satisfação)~500Variância média

Estes números são piso de bolso, não derivação estatística. Cálculo formal de power depende do baseline da métrica, do efeito mínimo detectável (MDE), e da variância observada. Ferramentas como Statsig e GrowthBook fazem o cálculo automático; cálculo manual usa fórmulas clássicas (e.g., n ≈ 16σ²/Δ² pra MDE Δ em métrica contínua).

Se o experimento precisa de 10k por arm e produto só faz 100 requests/dia por arm = 200 dias. Trade-off real: ou aumenta tráfego do canary (mais risco) ou aceita decisão com confiança menor.

Frequentista vs bayesiano — por que bayesiano costuma encaixar melhor

Frequentista clássico (t-test, chi-squared):

  • Decisão binária no fim: rejeita H0 ou não
  • Precisa de N pré-declarado (ou correção forte pra peeking)
  • p-value não responde “qual a probabilidade de B ser melhor”

Bayesiano (cálculo de posterior, e.g., beta-binomial pra conversão):

  • Decisão pode ser sequencial sem inflar erro tipo I
  • Dá probabilidade direta: “P(B > A) = 0.87”
  • Acomoda prior — eval offline vira informação genuína no posterior
  • Funciona melhor com N pequeno

Por que prompt A/B encaixa melhor com bayesiano:

  1. N é tipicamente pequeno (centenas a milhares, não milhões)
  2. Você tem prior forte do offline (vai entrar no experimento já com a hipótese de que treatment ≥ control)
  3. Decisão é sequencial — você quer parar cedo se o sinal é claro, sem pagar correção forte de peeking
  4. Decisão tem múltiplas dimensões (primary + guardrails) — Bayesiano lida melhor com múltiplas posteriors

Quando frequentista ainda faz sentido:

  • Time já tem A/B framework frequentista maduro
  • Métrica é conversão simples com volume alto
  • Auditoria/regulação exige p-value clássico

Tools como Statsig, GrowthBook, Eppo oferecem ambos os modos; a escolha é mais de cultura do time que de teoria.

Peeking — o anti-padrão central

Peeking é parar o experimento cedo porque “parece bom” — sem ajustar pra múltiplas comparações ou paradas sequenciais. Resultado: você infla a taxa de falso positivo. Experimento que parece significativo a 50% do N não é o mesmo experimento que seria significativo no N completo.

Como mitigar:

  • Pré-declarar N e duração (frequentista) — só olha no fim
  • Always-valid p-values (mSPRT, e-values) — permitem olhar quando quiser, mas com correção
  • Bayesian decision threshold (e.g., parar se P(B > A) > 0.95 e expected loss < ε) — natural pra sequential

Sinal de que o time peeking-a: “olha, B já tá 8% acima depois de 3 dias, vamos promover”. Resposta correta: o N declarado era 14 dias por uma razão.

Tools 2026

ToolTipoForte emTradeoff
StatsigSaaSA/B com estatística bayesiana built-in, free tier generosoMais um vendor; integração com prompt registry é custom
GrowthBookOSS + CloudOSS forte, frequentista + bayesiano, SDK em várias linguagensSetup precisa mais cuidado que SaaS puro
EppoSaaSMétricas com warehouse direto, governança forteMais caro; alvo enterprise
Langfuse + customOSSPrompt versionado já no Langfuse; A/B via label canary/production + cálculo de posterior caseiroVocê implementa a estatística
BraintrustSaaSEval offline + comparison view nativa; A/B online via integração com feature flagCombina bem com Braintrust como eval framework

Padrão pragmático: time pequeno usa Langfuse + cálculo bayesiano caseiro (beta-binomial de 30 linhas Python); time médio/grande adota GrowthBook ou Statsig pra ter estatística e UI prontas; enterprise vai Eppo ou Statsig enterprise.

Esqueleto de A/B em Langfuse — exemplo prático

import random
import langfuse
 
# 1. Routing — define qual arm cada request recebe
def select_prompt_arm(request_id: str, treatment_pct: float = 0.10) -> str:
    # hashing determinístico pra mesmo usuário ver mesma arm
    bucket = hash(request_id) % 100
    return "canary" if bucket < treatment_pct * 100 else "production"
 
# 2. Execução — pega prompt da label e registra arm no trace
arm_label = select_prompt_arm(request.id, treatment_pct=0.10)
prompt = langfuse.get_prompt("research-system", label=arm_label)
 
with langfuse.start_as_current_span(name="llm-call") as span:
    span.update(metadata={
        "experiment_id": "exp-2026-05-rephrase-pt",
        "arm": arm_label,
        "prompt_version": prompt.version,
    })
    response = client.messages.create(
        model="claude-sonnet-4-6",
        system=prompt.compile(),
        messages=[...],
    )
    span.update(output=response, metadata={"cost_usd": cost})
 
# 3. Análise — query por arm, calcula posterior
# (pseudo-código; rode em job offline ou notebook)
def analyze(experiment_id: str):
    traces = langfuse.fetch_traces(metadata={"experiment_id": experiment_id})
    df = build_df(traces)  # arm, eval_score, cost, latency
    posterior = beta_binomial_posterior(
        successes_a=df[df.arm == "production"].pass_count,
        n_a=df[df.arm == "production"].total,
        successes_b=df[df.arm == "canary"].pass_count,
        n_b=df[df.arm == "canary"].total,
    )
    return {
        "p_b_better_than_a": posterior.p_b_better,
        "expected_lift": posterior.expected_lift,
        "decision": "promote" if posterior.p_b_better > 0.95 else "continue",
    }

O fluxo: routing determinístico no entrypoint, atributo arm no span (chave de análise), cálculo offline em job/notebook. Sem precisar de SDK de A/B externo pra começar.

O maior risco nessa abordagem é o cálculo bayesiano caseiro — erros de implementação no beta_binomial_posterior são silenciosos. Valide contra um caso com resultado conhecido antes de usar em decisões reais, e documente as suposições do prior (beta(1,1) = prior plano; beta(α,β) com α,β > 1 = prior informado baseado em eval offline).

Quando NÃO fazer A/B

A/B tem custo: overhead de instrumentação, tempo de coleta de amostra, complexidade de análise. Nem toda mudança justifica:

SituaçãoAbordagem alternativa
Bug crítico em prod (comportamento perigoso, resposta errada a caso grave)Ship direto com rollback pronto; A/B viria tarde demais
Mudança cosmética sem impacto no comportamento (ajuste de tom menor)Eval offline + review humano; sem canary
Mudança de schema obrigatória (novo campo requerido pelo consumer)Coordenação técnica; métrica não é “melhor”, é “correto/incorreto”
Produto sem volume suficienteAvalie se N é atingível em tempo razoável; se não, use eval offline + shadow
Prompt de fallback / caso raramente exercidoEval manual nos casos afetados; não há tráfego pra A/B

Regra de bolso: A/B vale quando o efeito esperado é ≥ 2-5% em métrica importante E o produto tem volume suficiente pra chegar ao N em tempo razoável. Fora disso, eval offline bem feito + review humano é mais honesto que um A/B sub-amostrado.

Anti-padrões

  • Peeking — discutido acima; o mais comum e mais danoso
  • Múltiplas variáveis por experimento — mudou prompt + modelo + temperatura; impossível atribuir
  • Métrica escolhida depois — viés de seleção; sempre declare antes
  • Sem guardrails secundários — ganha em quality, regride em custo, ninguém viu
  • Sample size insuficiente — declara vitória com 50 amostras e variância alta
  • Routing não-determinístico — mesmo usuário vê arms diferentes em sessões diferentes; ruído enorme
  • Esquecer offline antes do canary — exposição de usuário a treatment pior por economia de tempo
  • Promover sem postmortem do experimento — perde aprendizado pro próximo ciclo
  • Analisar subgrupos não pré-declarados — “ficou melhor pra usuários em PT-BR” descoberto após o fato; data dredging; declare subgrupos de interesse antes de rodar
  • Atribuir resultado ao challenger sem isolar confounders — horário de pico, evento externo, mudança de UX coincidindo com o experimento; monitore métricas de saúde paralelas durante o A/B
  • Reusar golden set sem atualização — golden set de 6 meses atrás não representa a distribuição atual; revisar periodicamente (trimestral ou após mudança de produto)

Armadilhas comuns

Peeking — parar o experimento cedo porque "parece bom o bastante"

É o erro estatístico mais comum em A/B de produto e é ainda mais perigoso em prompt A/B porque a variância de LLM é alta. Você olha depois de 2 dias, vê treatment +9%, promove. O resultado era ruído — o N ainda era 40% do necessário. Com N completo, o ganho seria 2% — abaixo do threshold de decisão. A correção é simples: declare N e duração antes de rodar e não olhe até chegar lá (frequentista) ou use um threshold de parada bayesiano explícito, como P(B>A) > 0.95 AND expected loss < 0.5%. Se você sente o impulso de “só olhar” antes do N, é o momento de rever a cultura de experimentação do time.

Routing não-determinístico — mesmo usuário vê arms diferentes por sessão

Se o routing usa random.random() sem seed fixo por usuário, o mesmo usuário pode ver a versão production numa sessão e canary na próxima. O resultado: contaminação entre arms (o usuário carrega memória de uma versão pra outra), métricas de comportamento erradas (qual arm causou o click?), e análise de cohort impossível. A solução é routing determinístico: bucket = hash(user_id) % 100 garante que o mesmo usuário sempre caia no mesmo arm durante o experimento. Pra requests sem user_id, use session_id ou request_id como chave — qualquer identificador estável por “unidade de análise”.

Múltiplas variáveis no mesmo experimento — e não saber o que causou o resultado

“Testei prompt + modelo + temperatura juntos porque queria economizar tempo” é a versão A/B do anti-padrão de iteração caótica. O experimento dá resultado — positivo ou negativo — e você não consegue atribuir a nenhuma variável específica. Na próxima iteração, você chuta de novo. O ciclo não aprende. A correção é factorial design ou, mais pragmaticamente, experimentos sequenciais: test A (só prompt), depois test B (só modelo), depois test C (só temperatura). Mais lento, mas o resultado de cada experimento é interpretável e acumula conhecimento real sobre o sistema.

Como explicar em inglês

Interview quote: “Prompt A/B testing is the experiment that validates a diff: same input, two prompts — control being the current champion, treatment being the challenger — with a pre-declared primary metric and objective decision gates. The LLM-specific challenge is high variance even at temperature zero, which means you need more samples than intuition suggests — at least 200 per arm for an eval score, more for user feedback. We use Bayesian testing because our N is small, we have a strong prior from offline eval, and we need sequential decision-making without heavy peeking corrections.”

PortuguêsInglês
Braço de controle (champion)Control arm (champion)
Braço de tratamento (challenger)Treatment arm (challenger)
Olhar antes do tempo (peeking)Peeking
Tamanho de amostraSample size
Efeito mínimo detectávelMinimum detectable effect (MDE)
Gate de decisãoDecision gate
Lançamento gradual (canary)Canary / traffic ramp
Inferência sempre-válidaAlways-valid inference
Probabilidade de B ser melhor que AP(B > A) / probability of outperformance
Análise de subgrupoSubgroup analysis
Tamanho de efeito mínimoMinimum detectable effect
Poder estatísticoStatistical power
Intervalo de confiançaConfidence interval

O que vem a seguir

A/B determina se o challenger é melhor que o champion com dados suficientes. A nota 03 sobe um nível: como versionar os prompts que entram nesses experimentos, com semver adaptado ao comportamento de LLM — quando é patch, quando é minor, quando é major, e como o versioning se integra ao registry e ao CI.

Fontes

Veja também