Skip to content
Blog

A/B Testing Agent Variants: Prompts, Models, and Context

Set up A/B testing for agent variants using LiteLLM to compare prompts, models, and context strategies with statistical rigor and production-ready infrastructure.

Published on September 15, 2026

AI Assistant

A/B testing agents isn’t like A/B testing web pages. You can’t just swap a button color—your variants might use different models, different prompts, or different context strategies. LiteLLM’s gateway makes this practical by routing traffic, tracking costs, and collecting metrics across variants without changing your application code.

Why Agent A/B Testing Is Hard

Traditional A/B testing assumes deterministic outputs. Agent A/B testing faces unique challenges:

  • Non-deterministic: Same prompt can produce different results
  • Multi-turn: Variants affect entire conversation trajectories
  • Cost asymmetry: GPT-4o costs 20x more than GPT-4o-mini
  • Latency variance: Different models have different response times
  • Tool-calling differences: Variants may choose different tools

Architecture with LiteLLM

┌─────────────┐     ┌──────────────────┐     ┌─────────────────┐
│   Client     │────▶│  LiteLLM Proxy   │────▶│  Model A (GPT4o)│
│              │     │                  │     └─────────────────┘
│              │     │  ┌────────────┐  │     ┌─────────────────┐
│              │────▶│  │  Router    │────▶│  Model B (Claude) │
│              │     │  │  + Logger  │  │     └─────────────────┘
│              │     │  └────────────┘  │
└─────────────┘     └──────────────────┘

Setting Up the Gateway

# litellm_config.yaml
model_list:
  - model_name: "variant-a"
    litellm_params:
      model: "openai/gpt-4o"
      api_key: os.environ/OPENAI_API_KEY
  - model_name: "variant-b"
    litellm_params:
      model: "anthropic/claude-sonnet-4-20250514"
      api_key: os.environ/ANTHROPIC_API_KEY
  - model_name: "variant-c"
    litellm_params:
      model: "openai/gpt-4o-mini"
      api_key: os.environ/OPENAI_API_KEY

router_settings:
  routing_strategy: "simple-shuffle"  # Or custom AB test routing
  num_retries: 3
  timeout: 30

general_settings:
  master_key: os.environ/LITELLM_MASTER_KEY
  database_url: os.environ/DATABASE_URL

Custom A/B Routing

import hashlib
import json
from fastapi import FastAPI, Request
from litellm.proxy.proxy_server import app as litellm_app

# Define variants
VARIANTS = {
    "prompt_v1": {"model": "variant-a", "system_prompt": "You are a helpful assistant."},
    "prompt_v2": {"model": "variant-a", "system_prompt": "You are a concise, expert assistant. Answer in under 100 words."},
    "model_swap": {"model": "variant-b", "system_prompt": "You are a helpful assistant."},
    "cheap_model": {"model": "variant-c", "system_prompt": "You are a helpful assistant."},
}

def assign_variant(user_id: str, experiment: str) -> str:
    """Deterministic variant assignment using consistent hashing."""
    hash_input = f"{user_id}:{experiment}"
    hash_val = int(hashlib.md5(hash_input.encode()).hexdigest(), 16)
    variant_names = list(VARIANTS.keys())
    return variant_names[hash_val % len(variant_names)]

Tracking Metrics Per Variant

import time
from dataclasses import dataclass, field
from collections import defaultdict

@dataclass
class VariantMetrics:
    requests: int = 0
    total_tokens: int = 0
    total_cost: float = 0.0
    latencies: list = field(default_factory=list)
    successes: int = 0
    failures: int = 0
    tool_calls: int = 0

class ABTestTracker:
    def __init__(self):
        self.metrics = defaultdict(VariantMetrics)
    
    def record(self, variant: str, response: dict, latency: float, cost: float):
        m = self.metrics[variant]
        m.requests += 1
        m.total_tokens += response.get("usage", {}).get("total_tokens", 0)
        m.total_cost += cost
        m.latencies.append(latency)
        if response.get("choices", [{}])[0].get("finish_reason") == "stop":
            m.successes += 1
        else:
            m.failures += 1
    
    def summary(self) -> dict:
        result = {}
        for variant, m in self.metrics.items():
            p50 = sorted(m.latencies)[len(m.latencies) // 2] if m.latencies else 0
            p99 = sorted(m.latencies)[int(len(m.latencies) * 0.99)] if m.latencies else 0
            result[variant] = {
                "requests": m.requests,
                "avg_cost": m.total_cost / max(m.requests, 1),
                "total_cost": m.total_cost,
                "avg_tokens": m.total_tokens / max(m.requests, 1),
                "p50_latency_ms": p50 * 1000,
                "p99_latency_ms": p99 * 1000,
                "success_rate": m.successes / max(m.requests, 1),
            }
        return result

Statistical Significance

Don’t call a winner too early. Use proper statistical tests:

from scipy import stats
import numpy as np

def ab_test_significance(
    variant_a_scores: list[float],
    variant_b_scores: list[float],
    alpha: float = 0.05
) -> dict:
    """Two-sample t-test for A/B comparison."""
    t_stat, p_value = stats.ttest_ind(variant_a_scores, variant_b_scores)
    
    mean_a = np.mean(variant_a_scores)
    mean_b = np.mean(variant_b_scores)
    lift = (mean_b - mean_a) / mean_a * 100
    
    # Minimum sample size calculation
    effect_size = abs(mean_a - mean_b) / np.std(variant_a_scores + variant_b_scores)
    min_sample = max(30, int((2.8 / effect_size) ** 2)) if effect_size > 0 else 1000
    
    return {
        "variant_a_mean": mean_a,
        "variant_b_mean": mean_b,
        "lift_pct": lift,
        "p_value": p_value,
        "significant": p_value < alpha,
        "min_sample_size": min_sample,
        "current_sample": len(variant_a_scores) + len(variant_b_scores),
    }

What to A/B Test

Prompt Variations

prompts = {
    "concise": "Answer concisely. Maximum 2 sentences.",
    "detailed": "Provide a thorough, detailed explanation with examples.",
    "structured": "Answer using markdown with headers and bullet points.",
    "chain_of_thought": "Think step by step before answering."
}

Context Window Strategies

context_strategies = {
    "full_history": "Include all previous messages",
    "sliding_window": "Last 10 messages only",
    "summarized": "Summarize history every 5 turns",
    "rag_augmented": "Retrieve relevant past conversations"
}

Model Variants

model_variants = {
    "fast_cheap": "openai/gpt-4o-mini",
    "balanced": "openai/gpt-4o",
    "premium": "anthropic/claude-sonnet-4-20250514",
    "local": "ollama/llama3.1"
}

Production Dashboard

# Grafana/Prometheus metrics
from prometheus_client import Counter, Histogram, Gauge

VARIANT_REQUESTS = Counter(
    "agent_ab_requests_total",
    "Total requests per variant",
    ["variant"]
)

VARIANT_LATENCY = Histogram(
    "agent_ab_latency_seconds",
    "Response latency per variant",
    ["variant"],
    buckets=[0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0]
)

VARIANT_COST = Gauge(
    "agent_ab_cost_dollars",
    "Cumulative cost per variant",
    ["variant"]
)

VARIANT_SATISFACTION = Gauge(
    "agent_ab_user_satisfaction",
    "Average user satisfaction score",
    ["variant"]
)

A/B testing agents requires patience—agent quality metrics are noisy and effects are small. Run tests for at least 1000 conversations per variant before drawing conclusions, and always monitor cost alongside quality.