BlogA/B Testing Models in Production Without Shipping a RegressionEval · Output Quality

A/B Testing Models in Production Without Shipping a Regression

BC
Ben Carter · December 2025 · 9 min read

TL;DR

The new model benchmarks better, so you swap it in. Two weeks later, refunds are up and nobody connected the dots. A blind model swap is a bet that an offline benchmark predicts your production reality, and it usually does not. The disciplined alternative is an online A/B test with guardrail metrics: split live traffic, measure the change you want and the things you must not break (cost, latency, safety, satisfaction), and respect statistical significance instead of “peeking” until you see the number you want. Done right, you ship improvements with evidence and catch regressions before they reach everyone.

Why the benchmark lied to you

Every model swap starts with a number that looks like permission: the new model scores higher on MMLU, or on your offline eval, or in the vendor’s announcement. That number is real, but it answers a question you did not ask. It tells you how the model performs on that benchmark’s distribution. It does not tell you how it performs on your traffic, with your prompts, your retrieval context, and your users’ idiosyncratic phrasings, the only distribution that pays your bills.

The gap between benchmark and production is where blind swaps go to die. A model that is better on average can be worse on the specific slice that drives your revenue. It can change output format and break a downstream parser. It can be more verbose, quietly raising cost and latency. None of this shows up in the headline benchmark, and all of it shows up in your support queue, usually after enough time has passed that nobody connects the regression to the swap.

A model swap is a production change, and production changes get A/B tested. You would not deploy a checkout-flow rewrite to 100% of users on the strength of a designer’s opinion. Treat a model swap with the same discipline: measure it on real traffic, against a control, with guardrails, before it owns all your users.

Guardrail metrics: the regression early-warning system

The single most important concept in testing model changes safely is the guardrail metric. Your primary metric is what you are trying to improve, resolution rate, conversion, quality score. Your guardrail metrics are the things that must not get worse even if the primary improves. As the experimentation literature puts it, a regression in a guardrail metric can be grounds to reject a change even when it improves the goal metric.

For an LLM model swap, the essential guardrails are:

  • Cost per request. A “better” model that is 3x the price or 2x as verbose can wreck unit economics while looking like a quality win.
  • Latency (TTFT and total). Users feel speed; a slower model can lose more in abandonment than it gains in quality.
  • Safety / policy violation rate. A more capable model can also be more willing to do things it should refuse.
  • Format / schema adherence. The silent parser-breaker. Track it explicitly.
  • User-visible outcome. Escalation rate, thumbs-down rate, refund rate, the business signal the benchmark cannot see.
# Online A/B test: split traffic, compare primary metric AND guardrails
def route_request(user_id, request):
    arm = 'treatment' if hash_bucket(user_id) < 0.50 else 'control'
    model = NEW_MODEL if arm == 'treatment' else CURRENT_MODEL
    out   = model(request)
    log_experiment(
        arm=arm, user_id=user_id,
        primary=resolution_signal(out, request),
        guardrails={
            'cost':       out.cost_usd,
            'latency_ms': out.latency_ms,
            'safe':       safety_score(out) > 0.9,
            'format_ok':  validates_schema(out, request.schema),
        })
    return out

def decide(experiment):
    primary = compare(experiment, 'primary')          # is treatment better?
    if not primary.significant:                        # respect significance, don't peek
        return 'KEEP RUNNING'
    for g in ('cost', 'latency_ms', 'safe', 'format_ok'):
        if guardrail_breached(experiment, g):
            return f'DO NOT SHIP: guardrail {g} regressed'
    return 'SHIP' if primary.treatment_better else 'KEEP CONTROL'

The peeking trap: how teams fake their own results

The most common way A/B tests lie is peeking: checking results repeatedly and stopping the moment you see significance. This is statistically invalid, every additional look inflates the false-positive rate, so a test that “reached significance” after you refreshed the dashboard twenty times probably reached nothing. The classic treatment of this, Evan Miller’s “How Not To Run an A/B Test, ” shows how dramatically peeking corrupts your error rate. The problem is worse for LLMs, where outputs are noisy and stochastic, so early spikes and small samples mislead even more readily than in classic web experiments.

There are two valid ways out. Either pre-compute a sample size and test duration via power analysis and only look once at the end, or use a sequential testing method (designed for continuous monitoring) that controls error rates while you watch. What you cannot do is eyeball a running dashboard and ship when it crosses the line, that is how a model swap that is actually neutral or negative gets blessed as a win.

Significance is a stopping rule, not a vibe. Decide before you start how you will decide. Either fix the sample size and look once, or use a sequential test that is mathematically allowed to peek. Anything else is theater that will eventually approve a regression.

Combine offline and online, they catch different things

Offline eval against a golden set and online A/B testing are not competitors; they are layers. The golden set is your pre-flight check: it catches obvious regressions cheaply and instantly, with no user exposure, and it can probe edge cases your live traffic rarely hits. The A/B test is the real-world verdict: it measures what actually happens on your distribution and your business metrics, which no offline set can fully simulate.

The right sequence is: gate the new model on the offline golden set first (cheap, fast, blocks the obvious disasters), then run the online A/B with guardrails for the changes that only reality reveals. Shipping a model that fails offline is reckless; shipping a model that only passed offline is naive.

# Two-stage gate: offline pre-flight, then online A/B with guardrails
def promote_model(candidate):
    offline = run_golden_set(candidate)               # stage 1: cheap, no users
    if offline['quality'] < BASELINE['quality'] - 0.02:
        return 'BLOCKED offline: quality regressed before any user saw it'
    if offline['format_ok'] < 0.99:
        return 'BLOCKED offline: format adherence regressed'

    start_ab_test(candidate, split=0.5,               # stage 2: real traffic
                  primary='resolution_rate',
                  guardrails=['cost', 'latency_ms', 'safety', 'format_ok'],
                  min_sample=computed_sample_size(mde=0.02, power=0.8))
    return 'IN EXPERIMENT, decision deferred to significance'

Roll out gradually, keep the rollback warm

Even after an A/B test says ship, the safe pattern is progressive: move through 5% → 20% → 50% → 100% in stages, watching guardrails at each step, because some failures only emerge at full scale (rate limits, cost at volume, long-tail inputs). Automated guardrails should halt the rollout if a critical metric degrades. And keep the rollback instantaneous, the previous model stays one config flag away. A model swap you cannot reverse in seconds is a model swap you should not have made.

Picking a primary metric the model can actually move

The hardest part of A/B testing LLMs is usually not the statistics, it is choosing a primary metric that is both meaningful and sensitive enough to move in a reasonable sample. Teams reach for the ultimate business outcome (revenue, retention) and then discover it is too noisy and too far downstream to show a significant effect before the heat death of the universe. The fix is a metric hierarchy: a primary metric that is proximate and sensitive (resolution rate, task completion, thumbs-up), validated to correlate with the business outcome you actually care about. As practitioners measuring LLM impact with business metrics note, the discipline is to tie a near-term, movable signal to the longer-term value rather than testing directly on the slow, noisy outcome.

The trap on the other side is optimizing a proxy that diverges from value, a model that boosts thumbs-up by being sycophantic while resolving fewer real problems. This is why guardrails and the primary metric work as a pair: the primary captures the win you are chasing, the guardrails catch the ways a model can game it. A change that lifts the proxy while breaching a guardrail is exactly the regression A/B testing exists to stop.

Instrument for attribution before you run the test, not after. The reason blind swaps cause undiagnosed regressions is that nobody logged which model served which request. Tag every response with its model version, arm, and the metrics you will judge it on, so when a guardrail moves you can attribute it instantly instead of reconstructing it from a support-ticket trend two weeks later.

The bottom line

Benchmarks measure a distribution that is not yours, so a blind model swap is an untested production change waiting to regress your business metrics. Test model changes the way you test any production change: gate on an offline golden set first, then run an online A/B test on real traffic with guardrail metrics for cost, latency, safety, and format. Respect statistical significance instead of peeking, roll out progressively, and keep rollback one flag away. That is how you ship the improvements the benchmark promised without shipping the regressions it hid.

Ship AI on Evidence, Not Vibes

alt.qa Eval turns "seems fine" into measurable pass/fail, continuous evaluation, regression gates, and groundedness scoring for your AI outputs.

Try alt.qa Free →
Ben Carter Ben Carter writes about AI quality engineering at alt.qa, built by TheWorkCompany.