๐Ÿงช
Layer 6 of 6 ยท Agent Evaluation Stack
Experiments

Make Every Change
a Data-Driven Decision.

The top of the stack. Treat each prompt tweak or model swap as a controlled experiment: define a hypothesis, run variants over the same dataset, and decide with statistical confidence โ€” not vibes.

Hypothesis
State it first
A/B
Compare variants
p < 0.05
Significance
Layer 6
Decisions, not guesses

What Is an Experiment?

An experiment is a controlled comparison. You hold everything constant โ€” the dataset, the evaluators, the harness โ€” and change exactly one thing (a prompt, a model, a retrieval setting) to isolate its effect.

Then you measure whether the difference is real or noise. LLM outputs vary, and eval sets are finite, so a 2% score bump might be pure chance. Statistical testing tells you whether to ship.

This layer sits on top of everything below it: it consumes golden sets, coverage, reproducible replays, and rubric scores to turn "I think this prompt is better" into "this prompt is better, p = 0.01".

In One Sentence

"Change one thing, measure against a fixed baseline, and decide with statistical confidence."


Analogy
  • A controlled lab trial
  • A product A/B test
  • A dependency upgrade benchmark

Why Experiments Matter

๐Ÿงฎ

Signal vs. Noise

Statistical tests separate a genuine improvement from random variation in a small sample.

๐ŸŽ›๏ธ

Isolate Variables

Change one thing at a time so you know exactly what caused a score to move.

๐Ÿ“œ

Auditable History

A logged trail of experiments explains why the system is configured the way it is.

๐Ÿ’ธ

Justify Trade-offs

Quantify quality-vs-cost-vs-latency so model choices are defensible, not intuition.

๐Ÿ›‘

Prevent Regressions

A ship gate on the baseline stops "improvements" that quietly break other cases.

๐Ÿ”ฌ

Compounding Learning

Each experiment sharpens intuition and feeds the next hypothesis.

Anatomy of an Experiment

A good experiment record is a pre-registered plan: hypothesis, the single variable, the fixed dataset, and the metric that decides success โ€” all written before you run it.

experiments/exp_017.yaml
id: exp_017
hypothesis: >
  Adding the refund window to the system prompt reduces
  hallucinated_fact failures without hurting tone.
variable: system_prompt        # the ONE thing that changes
control: prompts/v3.txt
treatment: prompts/v4.txt
dataset: golden/support.jsonl  # fixed across both arms
primary_metric: accuracy       # decides ship / no-ship
guardrail_metrics: [tone, cost_usd, p95_latency_ms]
decision_rule: "ship if accuracy +โ‰ฅ3% at p < 0.05 and no guardrail regresses"

Hypothesis

A specific, falsifiable prediction โ€” written first to avoid rationalizing the result.

Single Variable

Exactly one change between control and treatment, so any effect is attributable.

Primary Metric

One metric decides the outcome; declaring it up front prevents cherry-picking.

Guardrails

Metrics that must not regress โ€” a win on accuracy can't come at the cost of safety or latency.

How Experiments Work

Experiment Lifecycle
flowchart LR H["๐Ÿ’ก Hypothesis"] --> D["๐Ÿ—‚๏ธ Fix dataset & metrics"] D --> A["๐Ÿ…ฐ๏ธ Run control"] D --> B["๐Ÿ…ฑ๏ธ Run treatment"] A --> S["๐Ÿ“Š Compare scores"] B --> S S --> T["๐Ÿงฎ Significance test"] T --> R{"Ship?"} R -- "Yes" --> Ship["๐Ÿš€ Promote"] R -- "No" --> Learn["๐Ÿ““ Log & iterate"] Learn --> H
Result Readout (example)
MetricControl (v3)Treatment (v4)ฮ”Verdict
Accuracy (primary)0.780.86+0.08Significant (p=0.01)
Tone0.910.90โˆ’0.01No change (n.s.)
Cost / 1k$1.20$1.22+$0.02Within guardrail

Decision: Ship v4 โ€” accuracy up significantly, no guardrail regressed.

Step-by-Step Implementation

1

Define the experiment

Model an experiment as two arms over one fixed dataset with a declared primary metric.

experiments/models.py
from __future__ import annotations
from dataclasses import dataclass
from typing import Callable


@dataclass
class Arm:
    """One side of a comparison (control or treatment).

    Args:
        name: Human-readable label, e.g. "v3" or "gpt-4o".
        agent: Callable mapping an input to the arm's output.
    """
    name: str
    agent: Callable[[str], str]


@dataclass
class Experiment:
    """A controlled comparison between two arms on a shared dataset."""
    id: str
    control: Arm
    treatment: Arm
    dataset: list
    metric: Callable[[str, object], float]
    primary: str = "score"
2

Run both arms on the same data

Score each example under both arms so the comparison is paired โ€” the most sensitive design.

experiments/run.py
from .models import Experiment


def run_experiment(exp: Experiment) -> dict:
    """Evaluate control and treatment over the same examples.

    Args:
        exp: The experiment definition to execute.

    Returns:
        Paired per-example scores for each arm, ready for a paired test.
    """
    control_scores, treatment_scores = [], []
    for example in exp.dataset:
        control_scores.append(exp.metric(exp.control.agent(example.input), example))
        treatment_scores.append(exp.metric(exp.treatment.agent(example.input), example))
    return {
        "control": control_scores,
        "treatment": treatment_scores,
        "n": len(exp.dataset),
    }
3

Test for significance

A paired t-test tells you whether the mean difference is unlikely to be chance.

experiments/stats.py
from statistics import mean
from scipy import stats


def compare(control: list[float], treatment: list[float], alpha: float = 0.05) -> dict:
    """Run a paired significance test between two arms.

    Args:
        control: Per-example scores for the control arm.
        treatment: Per-example scores for the treatment arm.
        alpha: Significance threshold for declaring a real effect.

    Returns:
        A dict with the mean delta, p-value, and a ``significant`` flag.

    Raises:
        ValueError: If the two arms have different lengths.
    """
    if len(control) != len(treatment):
        raise ValueError("Paired test requires equal-length arms")
    result = stats.ttest_rel(treatment, control)
    delta = mean(treatment) - mean(control)
    return {
        "delta": round(delta, 4),
        "p_value": round(float(result.pvalue), 4),
        "significant": bool(result.pvalue < alpha and delta > 0),
    }
4

Apply the decision rule

Automate ship/no-ship so the decision follows the pre-registered rule, not enthusiasm.

experiments/decide.py
def decide(primary: dict, guardrails: dict[str, dict], min_delta: float = 0.03) -> str:
    """Return a ship decision from primary and guardrail test results.

    Args:
        primary: Significance result for the primary metric.
        guardrails: Significance results keyed by guardrail metric name.
        min_delta: Minimum improvement required on the primary metric.

    Returns:
        One of ``"ship"``, ``"hold"``, or ``"reject"``.
    """
    regressed = [m for m, r in guardrails.items() if r["delta"] < -min_delta and r["significant"]]
    if regressed:
        return "reject"
    if primary["significant"] and primary["delta"] >= min_delta:
        return "ship"
    return "hold"

Use Cases

Prompting

Prompt A/B Tests

Prove a reworded system prompt actually improves accuracy before rolling it out.

Model Selection

Model Bake-off

Compare a cheaper model against the incumbent on quality, cost, and latency together.

RAG Tuning

Retriever Comparison

Test chunk size or embedding model changes on frozen replay fixtures.

Cost/Quality

Trade-off Analysis

Quantify how much quality you lose by halving token spend โ€” then decide deliberately.

Best Practices & Pitfalls

Do

  • Pre-register. Write the hypothesis and primary metric before running.
  • Change one variable. Isolate cause; batch changes make results uninterpretable.
  • Use paired tests. Score both arms on identical examples for maximum sensitivity.
  • Log everything. Version prompts, datasets, and results for a reproducible trail.

Avoid

  • Chasing noise. A +1% bump on 30 examples is almost certainly random.
  • p-hacking. Don't test ten metrics and report only the one that "won".
  • Ignoring guardrails. An accuracy win that tanks safety or latency isn't a win.
  • Moving the dataset. Changing the eval set between arms invalidates the comparison.

FAQ

How many examples do I need?

It depends on effect size and variance, but small sets (under ~30) rarely detect anything but huge differences. Run a power analysis, or grow your golden set until effects are detectable.

Which statistical test should I use?

For paired continuous scores, a paired t-test or Wilcoxon signed-rank works well. For pass/fail proportions, use McNemar's test. Bootstrap confidence intervals are a robust general option.

How does this use the layers below?

Experiments run on golden sets (L1) with good coverage (L2), replayed deterministically (L4), scored by rubrics (L5), with error analysis (L3) explaining why an arm won. It's the capstone of the stack.

Resources