A/B Testing¶
Compare two RAG configurations on live traffic: split users deterministically between variants, tag monitor traces with the assignment, and let the analyzer tell you which variant wins — with statistical significance, not gut feeling.
Builds on Production Monitoring: traces collected by the monitor daemon are the input.
Quick start¶
1. Declare the experiment and route traffic:
from ragnarok_ai import Experiment, MonitorClient
exp = Experiment(name="reranker-test", variants=["control", "reranker"])
client = MonitorClient()
pipelines = {"control": rag_baseline, "reranker": rag_with_reranker}
async def handle_query(user_id: str, query: str) -> str:
with client.trace(query) as trace:
variant = exp.tag(trace, user_id) # assigns + tags the trace
return await pipelines[variant].query(query)
Assignment is deterministic and stateless: assign(key) hashes the key with the experiment name, so the same user always sees the same variant — across processes and restarts, with no shared storage. Use whatever key should stay consistent: a user id, session id, or query hash.
2. Analyze once traffic has accumulated:
from ragnarok_ai import ABAnalyzer
from ragnarok_ai.monitor.store import MonitorStore
store = MonitorStore()
analyzer = ABAnalyzer(exp)
report = analyzer.analyze(store.get_traces(since=experiment_start))
print(report.winner) # "reranker", "control", or None
for verdict in report.verdicts:
print(f"{verdict.metric}: {verdict.a_value:.3f} vs {verdict.b_value:.3f} "
f"(p={verdict.p_value:.4f}, winner={verdict.winner})")
What is compared¶
| Metric | Test | Better |
|---|---|---|
| Success rate | Two-proportion z-test | Higher |
| Mean total latency | Welch's t-test | Lower |
Each variant also gets descriptive stats (count, p50/p95 latency) in report.stats.
Overall winner rule: a variant wins the experiment only if it is significantly better on at least one metric and significantly worse on none. A variant that is faster but fails more often produces winner=None — the trade-off is yours to arbitrate, and both verdicts are in the report.
Options¶
from ragnarok_ai import ABTestConfig
# Uneven split: send 10% of traffic to the candidate
exp = Experiment(name="test", variants=["control", "candidate"], weights=[0.9, 0.1])
# Stricter significance, larger minimum sample
analyzer = ABAnalyzer(exp, ABTestConfig(alpha=0.01, min_samples=200))
- Windows where either variant has fewer than
min_samplestraces are flaggedreport.insufficient_datainstead of producing unstable verdicts. - Changing
saltreshuffles all assignments (useful to re-run an experiment with fresh buckets). - More than two variants are supported at assignment time;
analyze(traces, variant_a=..., variant_b=...)compares any pair.
Design notes¶
- p-values use the normal approximation, which is accurate at the sample sizes
min_samplesenforces — the module needs no scipy. - Peeking caveat: checking significance repeatedly as data accumulates inflates false positives. Decide the analysis window in advance, or apply a stricter
alpha. - Pair with drift detection: drift detection watches the whole traffic; A/B testing compares deliberate configuration changes within it.