04 - Champion-challenger em produção

TL;DR

Champion-challenger é a operação de ship em produção: a versão atual (champion) serve a maior parte do tráfego (90-95%), a versão candidata (challenger) serve canary (5-10%), métricas-gate decidem promoção automática (challenger vira champion) ou rollback (challenger sai). Setup pragmático: feature flag (LaunchDarkly, GrowthBook, OpenFeature) ou routing logic em código. Critérios de promoção objetivos e pré-declarados: (1) eval score do challenger ≥ champion com significância; (2) zero regressão na golden subset crítica; (3) custo médio não sobe > X%; (4) latência p95 não sobe > Y%. Rollback automático em três sinais: error rate spike, latência spike, eval score drop. Anti-padrão: challenger eterno — nunca promove, nunca rola back, fica 50/50 pra sempre porque ninguém decide.

A mecânica básica

                          tráfego de produção
                                  │
                                  ▼
                        ┌──────────────────┐
                        │  router          │
                        │  (feature flag,  │
                        │   prompt label,  │
                        │   ou hash)       │
                        └────┬─────────┬───┘
                             │         │
                  90-95%     │         │      5-10%
                             │         │
                             ▼         ▼
                    ┌────────────┐  ┌────────────┐
                    │  CHAMPION  │  │ CHALLENGER │
                    │  v3.0      │  │ v3.1       │
                    └─────┬──────┘  └─────┬──────┘
                          │               │
                          ▼               ▼
                    ┌──────────────────────────┐
                    │  observability + eval    │
                    │  (métricas por arm)      │
                    └────────────┬─────────────┘
                                 │
                                 ▼
                       ┌─────────────────┐
                       │  decisão        │
                       │  (gate + rules) │
                       └──┬───────────┬──┘
                          │           │
                  PROMOVE │           │ ROLLBACK
                          ▼           ▼
                  challenger     challenger
                  vira champion  sai, champion
                                 segue

A diferença crítica entre champion-challenger e A/B “cru”: o ciclo está atrelado a decisão automatizada de promoção/rollback, com regras pré-declaradas. A/B mede; champion-challenger mede e age.

Setup — feature flag vs routing em código

Opção A — Feature flag dedicado

Ferramentas: LaunchDarkly, GrowthBook, Statsig, OpenFeature (padrão aberto), Unleash.

from openfeature import api
 
client = api.get_client()
 
def serve_request(user_id: str, request: str) -> str:
    # 1. Feature flag decide a arm
    arm = client.get_string_value(
        flag_key="research-prompt-canary",
        default_value="production",
        evaluation_context={"targeting_key": user_id},
    )
 
    # 2. Pega prompt da label correspondente
    prompt = langfuse.get_prompt("research-system", label=arm)
 
    # 3. Chama LLM
    return run_llm(prompt, request)

Vantagens:

  • Routing e percentage gerenciados na UI do feature flag (mudança de 5% pra 10% sem deploy)
  • Segmentação de usuários (e.g., só rodar challenger pra tier free)
  • Audit log nativo (quem mudou o flag, quando)
  • Kill switch — flag = 0% em 1 segundo

Desvantagens:

  • Mais um vendor / componente
  • Latência adicional da chamada de feature flag (mitigada por SDK com cache local)

Opção B — Routing em código com prompt label

Sem feature flag externo, usa só o registry de prompt:

import random
import hashlib
 
def select_arm(request_id: str, canary_pct: float = 0.10) -> str:
    # hash determinístico pra consistência por request_id/user_id
    bucket = int(hashlib.md5(request_id.encode()).hexdigest(), 16) % 100
    return "canary" if bucket < canary_pct * 100 else "production"
 
# config externa pode mudar canary_pct sem deploy (env var, config service)
CANARY_PCT = float(os.environ.get("CANARY_PCT", "0.10"))
 
def serve_request(request_id: str, request: str) -> str:
    arm = select_arm(request_id, CANARY_PCT)
    prompt = langfuse.get_prompt("research-system", label=arm)
    return run_llm(prompt, request)

Vantagens:

  • Sem vendor adicional
  • Routing fica no código (versionado, auditável via Git)
  • Funciona com qualquer registry de prompt

Desvantagens:

  • Mudança de percentage exige config service (env var é OK pra start)
  • Sem UI rica (segmentação, audit log natural)

Padrão recomendado: time pequeno começa com B (routing simples), migra pra A quando complexidade de segmentação cresce.

Critérios de promoção — concretos e pré-declarados

A regra de ouro: declarar critérios antes de rodar o canary, não negociar depois.

Template de critério pra promoção:

# experiment: research-system v3.0 → v3.1
canary_pct: 10
duration_min_hours: 72
sample_size_min: 2000  # por arm
 
promotion_criteria:
  # Métrica primária — challenger ≥ champion com significância
  eval_score:
    metric: "rubric_avg"
    direction: ">="
    significance: "p_b_better_a >= 0.95"  # bayesiano
    # OR frequentista: p_value < 0.05 e CI lower bound positivo
 
  # Golden subset crítico — zero regressão
  golden_critical:
    metric: "pass_rate"
    direction: ">="
    tolerance: 0.0  # zero regressão
 
  # Custo médio — pequena regressão tolerada
  cost_per_request:
    metric: "avg_cost_usd"
    direction: "<="
    tolerance: 0.15  # 15% de aumento OK
 
  # Latência p95 — pequena regressão tolerada
  latency_p95:
    metric: "p95_ms"
    direction: "<="
    tolerance: 0.20  # 20% de aumento OK
 
  # Safety — zero tolerância
  safety_violations:
    metric: "violations_per_1k"
    direction: "<="
    tolerance: 0.0
 
# Decisão automática se TODOS os critérios passam
auto_promote_if_all_pass: true

Princípios:

  1. Critérios são AND, não OR — passar em 4 de 5 não promove
  2. Significância exigida — diferença visual ≠ diferença real
  3. Direção declarada — “menor ou igual” pra custo, “maior ou igual” pra qualidade
  4. Tolerância por dimensão — safety zero, custo 15%, latência 20%; uniformidade é anti-padrão

Rollback automático — triggers e mecanismo

Rollback acontece antes de promover (durante canary). Triggers comuns:

TriggerThreshold típicoPor quê
Error rate spikeerror_rate > 2x baseline nos últimos 15 minBug, schema break, modelo retornando 500
Latência spikep95 > 2x baseline OR > SLA absolutoPrompt longo demais, modelo retornando devagar
Eval score droprubric_avg < 0.7 x baseline (sliding window)Qualidade caiu rapidamente em prod
Safety violation spikequalquer violation acima da baselineRisco regulatório/produto
Refusal rate spikerefusal > 2x baselinePrompt novo virou paranoico
Custo spikecost_per_request > 2x baselineOutput verboso demais, tool call inflado

Mecanismo:

# pseudo-código: monitor que polla métricas e age
def canary_watcher(experiment_id: str):
    while experiment_running(experiment_id):
        metrics = fetch_metrics_last_15min(experiment_id, arm="canary")
        baseline = fetch_baseline(experiment_id, arm="production")
 
        for rule in ROLLBACK_RULES:
            if rule.triggered(metrics, baseline):
                rollback(experiment_id, reason=rule.name)
                alert_team(experiment_id, rule.name, metrics)
                return
 
        sleep(60)

Rollback efetivo = mover label canary pra apontar pra production (ou setar canary_pct = 0). Cache do client expira em segundos, todas as instâncias param de servir o challenger.

Quando manual gate é OK vs quando automação é exigida

SituaçãoGate manualGate automático
Produto inicial, poucos experimentos por mêsOKOverkill
Equipe pequena, dono claro do promptOKOpcional
Volume alto, vários experimentos simultâneosNão escalaNecessário
Produto regulado (saúde, financeiro)Manual é piso, automático complementaSim, com manual review por cima
Mudança em prompt safety-criticalManual obrigatórioAutomático insuficiente sozinho
Mudança em prompt rotineira (typo, tom)Patch direto, sem gatePatch direto, sem gate

Padrão maduro: gate automático que pode ser overridado por manual review. Automação roda 90% dos casos sem intervenção, humano olha os 10% que exigem judgment.

Champion eterno — o anti-padrão simétrico

Discussão clássica foca em “challenger eterno”. Existe a versão simétrica: champion eterno.

Sinais:

  • Champion na produção há > 6 meses sem nenhum challenger sequer proposto
  • Eval score do champion não é recalibrado contra modelo atual do provider
  • Drift detection não roda (ninguém saberia se champion degradou)
  • Cultura: “tá funcionando, não mexe”

Risco do champion eterno = drift silencioso. Modelo do provider atualiza, distribuição de input muda, prompt que era ótimo vira só “ok”. Sem challenger pra comparar, ninguém percebe.

Mitigação: cadência mínima de challenger — pelo menos um experimento por trimestre, mesmo que seja “challenger é o mesmo prompt mas em modelo novo”. Mantém a infraestrutura do loop viva.

Anti-padrões

  • Challenger eterno — 50/50 pra sempre porque ninguém aperta o botão de promoção
  • Champion eterno — nunca proposto challenger; drift silencioso
  • Critérios negociados após o canary — “sim, latência subiu 30%, mas o ganho de qualidade compensa” (sem ter declarado antes)
  • Promoção sem guardrails secundários — só olhou eval score, custo dobrou
  • Routing não-determinístico — mesmo user vê canary numa request e production na seguinte; ruído + UX confusa
  • Sem audit do experimento — promoveu, esqueceu o porquê; postmortem perdido
  • Rollback sem comunicação — voltou pra champion, time descobriu pelo Slack uma semana depois
  • Canary % muito grande no major bump — 50% canary em mudança de schema = blast radius enorme

Maturidade de champion-challenger

Nível 0 — Ad-hoc
  "Subi o prompt novo direto em prod. Se quebrar, eu vejo."

Nível 1 — Canary manual
  Routing via env var. Sobe pra 5%, espera dois dias, olha métricas, decide à mão.

Nível 2 — Canary com gate manual
  Critérios pré-declarados. Dashboard mostra arm vs arm. Promoção/rollback ainda manual.

Nível 3 — Canary com gate automático
  Critérios pré-declarados executam automaticamente. Promoção automática se todos passam.
  Rollback automático em triggers (error spike, latency spike).

Nível 4 — Multi-armed bandits
  Em vez de A/B fixo, alocação dinâmica (mais tráfego pra arm vencedora ao longo do experimento).
  Convergência mais rápida com menor exposição ao perdedor.

Nível 5 — Loop totalmente fechado
  Auto-prompt (DSPy) gera challengers; gate automático promove/rola back;
  postmortem automático populated em registry.

Meta pragmática: nível 3 estável pra time de IA serio em 2026. Nível 4-5 é fronteira, vale pra produto crítico/volume alto.

Fontes

  • Martin FowlerCanaryRelease. Definição canônica de canary, anterior a LLMs mas integralmente aplicável.
  • LaunchDarklyProgressive delivery patterns.
  • GrowthBookFeature flag experiments.
  • OpenFeatureSpecification. Padrão aberto pra feature flag.
  • Kohavi, Tang, XuTrustworthy Online Controlled Experiments (2020). Pré-declaração de critério, peeking, sample size.
  • @hooeemBecome an AI Engineer, Step 11 — discussão sobre cadência de improvement.

Veja também