Online A/B Testing for LLM Features

AILLMMachine LearningArchitecture
Share on LinkedIn

Offline eval said the new prompt was 8% better. Online, users clicked thumbs-down 12% more often and average session cost went up 40%. LLM quality doesn't fully compress into a benchmark score — online A/B testing catches the gap between eval wins and user experience. It's slower and noisier than offline eval, but it's the only test that counts when real users with real money interact with your product.

Experiment architecture

async def handle_request(request: Request) -> Response:
    variant = experiment.assign(
        experiment="prompt_v3_test",
        unit_id=request.tenant_id,  # or user_id
    )
    prompt = PROMPTS[variant]  # "control" | "treatment"
    response = await orchestrator.run(request, prompt_version=variant)
    await analytics.track("llm_response", {
        "experiment": "prompt_v3_test",
        "variant": variant,
        "tenant_id": request.tenant_id,
        "latency_ms": response.latency,
        "cost_usd": response.cost,
        "thumbs": None,  # filled async on feedback
    })
    return response

Use consistent hashing on tenant_id or user_id — not request_id — so the same user always sees the same variant.

Choosing the unit of randomization

Unit Use when
User Consumer apps, personal experience
Tenant B2B SaaS, team-level behavior
Session Short experiments, high traffic
Request Only for stateless, low-stakes (risky — inconsistent UX)

Tenant-level randomization prevents one team seeing two different bot personalities in the same workflow.

Primary vs guardrail metrics

Primary (what you're trying to improve):

Guardrails (don't regress):

GUARDRAILS = {
    "cost_per_session": {"max_lift": 0.15},      # no >15% cost increase
    "latency_p99": {"max_lift": 0.20},
    "safety_violations": {"max_absolute": 0.001}, # hard ceiling
}

Auto-stop treatment if guardrails breach for 24 hours.

What to test

High-value experiments:

  1. Model downgrade — can mini handle this feature? (usually biggest cost win)
  2. Retrieval config — top-5 vs top-10 chunks, reranker on/off
  3. Prompt structure — citation format, tone, verbosity
  4. Caching — semantic cache on/off for FAQ feature

Low-value: testing punctuation in system prompt on low traffic — you'll never reach significance.

Statistical pitfalls

Use sequential testing or Bayesian methods if your experimentation platform supports them — they handle peeking better than fixed-horizon tests.

Connecting online to offline

Before launching online:

  1. Run offline eval on both variants (golden dataset)
  2. Ensure offline direction matches hypothesis
  3. Launch online with 5–10% traffic to treatment
  4. Ramp if guardrails hold

After online:

Feature flag integration

Use your existing flag system (LaunchDarkly, Unleash, custom):

experiment:
  name: model_routing_v2
  variants:
    control: { model: "gpt-4o", weight: 50 }
    treatment: { model: "gpt-4o-mini", weight: 50 }
  targeting:
    include_tenants: ["internal", "beta-cohort"]

Start with internal/beta tenants. Expand after guardrails pass.

Document everything

Experiment log per test:

Future you won't remember why prompt_v2 was reverted.

Statistical significance for LLM experiments

Don't ship on gut feel — compute significance:

from scipy.stats import chi2_contingency

def ab_test_significance(control_successes, control_total,
                          treatment_successes, treatment_total,
                          alpha=0.05):
    table = [
        [control_successes, control_total - control_successes],
        [treatment_successes, treatment_total - treatment_successes],
    ]
    chi2, p_value, _, _ = chi2_contingency(table)
    return {
        "p_value": p_value,
        "significant": p_value < alpha,
        "control_rate": control_successes / control_total,
        "treatment_rate": treatment_successes / treatment_total,
    }

# Example: thumbs-up rate
result = ab_test_significance(450, 1000, 520, 1000)
# p=0.003 → significant improvement in treatment

Minimum sample size: 500 users per variant for 5% effect detection at 80% power. Run experiments for at least 1 week to capture day-of-week effects.

Guardrail metrics

Primary metric alone can hide regressions — monitor guardrails:

GUARDRAILS = {
    "latency_p99_ms": {"max": 3000, "action": "pause_experiment"},
    "error_rate": {"max": 0.02, "action": "pause_experiment"},
    "safety_violation_rate": {"max": 0.001, "action": "kill_experiment"},
    "cost_per_request_usd": {"max": 0.05, "action": "alert"},
}

def check_guardrails(experiment_metrics):
    for metric, threshold in GUARDRAILS.items():
        if experiment_metrics[metric] > threshold["max"]:
            trigger_action(threshold["action"], metric)

Auto-pause experiment if safety violation rate exceeds threshold — don't wait for manual review.

Sequential testing and early stopping

Fixed-horizon tests waste time when effect is obvious early. Sequential testing allows early stop:

Day 1: 200 users/variant → guardrails pass, no significance
Day 3: 600 users/variant → p=0.04, significant → ship treatment
Day 7: (skipped — already decided)

Use sequential testing frameworks (Statsig, Optimizely) that adjust significance thresholds for multiple looks. Naive early stopping inflates false positive rate.

Failure modes

Production checklist

Run online A/B with guardrail metrics (latency, error rate) not just quality score — better model that doubles latency loses users.

Resources

Frequently asked questions

What should I A/B test in an LLM product?

Model tier (GPT-4 vs mini), prompt versions, retrieval parameters (chunk count, reranker on/off), response format (with/without citations), and UX changes (streaming vs batch). Don't A/B test safety filters without strong guardrails — the losing variant might violate policy before you detect it.

How long should an LLM A/B test run?

Long enough for your primary metric to reach statistical significance — usually 1–2 weeks minimum for B2B with moderate traffic. LLM metrics have high variance; avoid stopping early on a good day. Pre-register sample size and run duration before launching.

What guardrail metrics prevent shipping harmful variants?

Track alongside primary metrics: error rate, latency p99, cost per session, thumbs-down rate, safety violation rate, and human escalation rate. Auto-kill a variant if guardrails breach thresholds even if the primary metric looks good.

Hiring a senior Android / Flutter engineer?

I architect and ship production mobile software — Kotlin, Jetpack Compose, Flutter — for robotics, EV infrastructure, fintech, and real-time systems. Open to remote roles in Europe and the US.

Get in touch →