Repeated Runs¶
LLM outputs are stochastic. The same input can produce different answers across runs. A single evaluation run might pass or fail by chance. Repeated runs give you statistical confidence that your agent's behavior is reliable.
Quick Start¶
Run each test case 3 times and require at least 80% of runs to pass:
from ragpill import Case, Dataset, evaluate_testset_with_mlflow
from ragpill.settings import MLFlowSettings
from ragpill.base import TestCaseMetadata
from ragpill.evaluators import RegexInOutputEvaluator
evaluator = RegexInOutputEvaluator(pattern="paris", expected=True)
case = Case(
inputs="What is the capital of France?",
metadata=TestCaseMetadata(repeat=3, threshold=0.8),
evaluators=[evaluator],
)
testset = Dataset(cases=[case])
result = await evaluate_testset_with_mlflow(
testset=testset,
task=my_agent,
mlflow_settings=MLFlowSettings(),
)
# Three views of the data:
print(result.runs) # One row per (run x evaluator) — most granular
print(result.cases) # One row per (case x evaluator) — aggregated
print(result.summary) # One row per case — overall pass/fail
How It Works¶
When repeat > 1, ragpill uses a two-phase execution model for each test case:
Phase 1 (Task Execution): The task is executed N times inside an MLflow span context. Each run gets its own child span under a shared parent. All spans are committed to MLflow when Phase 1 completes.
Phase 2 (Evaluation): After all task spans are committed, evaluators run for each run individually. A ContextVar ensures span-based evaluators only see spans from their specific run, not from other runs.
Case: "What is the capital of France?" (repeat=3)
├── Phase 1: Execute task
│ ├── run-0: task("What is...") → "Paris is the capital"
│ ├── run-1: task("What is...") → "The capital is Paris"
│ └── run-2: task("What is...") → "France's capital: Paris"
└── Phase 2: Evaluate
├── run-0: RegexInOutput("paris") → True
├── run-1: RegexInOutput("paris") → True
└── run-2: RegexInOutput("paris") → True
→ pass_rate = 3/3 = 1.0 ≥ threshold 0.8 → PASSED
Stateful Tasks (task_factory)¶
If your task is stateful (e.g., an agent with message history), use task_factory instead of task to ensure each run starts with a clean state:
def create_agent():
"""Return a fresh agent instance with empty history."""
return MyAgent(history=[])
result = await evaluate_testset_with_mlflow(
testset=testset,
task_factory=create_agent,
)
See the Task Factory How-To for detailed guidance.
When do I need a factory?
| Scenario | Use task= |
Use task_factory= |
|---|---|---|
| Stateless function (no side effects) | Yes | |
| Agent with message history | Yes | |
| Agent with mutable configuration | Yes | |
| Pure function + repeat=1 | Yes |
Threshold Semantics¶
The threshold parameter controls how pass/fail is decided:
- threshold=1.0 (default): All runs must pass. A single failure means the case fails.
- threshold=0.0: The case always passes regardless of run results.
- threshold=0.8: At least 80% of runs must pass.
- The comparison is
pass_rate >= threshold, sothreshold=0.6with 2/3 runs passing (0.667) is a pass.
A run counts as "passed" when all its evaluators pass. If any evaluator fails, the entire run is considered failed.
Reading the Results¶
evaluate_testset_with_mlflow returns an EvaluationOutput with three DataFrame views:
.runs — Per-run detail¶
One row per (run x evaluator). Includes run_index, repeat_total, threshold, and all the standard columns (evaluator_result, evaluator_reason, etc.).
.cases — Aggregated per case¶
One row per (case x evaluator). Includes pass_rate and passed columns showing the aggregated result across runs.
.summary — Overall verdict¶
One row per case with passed, pass_rate, threshold, and a human-readable summary string.
Per-Case Overrides vs. Global Defaults¶
You can set repeat and threshold at two levels:
Global defaults via MLFlowSettings:
Or via environment variables:
Per-case overrides via TestCaseMetadata:
Per-case values take precedence over global defaults. When a per-case value is None, the global default is used.
CSV Integration¶
Add repeat and threshold columns to your CSV:
Question,test_type,expected,tags,check,repeat,threshold
What is X?,RegexInOutputEvaluator,true,factual,x,3,0.6
What is Y?,RegexInOutputEvaluator,true,geography,y,,
What is X?will run 3 times with threshold 0.6What is Y?will use global defaults (repeat=1, threshold=1.0 unless overridden)- Empty values defer to the global
MLFlowSettingsdefaults
Note
All rows for the same question must have the same repeat and threshold values. Inconsistent values will raise a ValueError.
MLflow Assessment Naming¶
When viewing traces in the MLflow UI, assessments follow this naming convention:
- Per-run:
run-0_RegexInOutput,run-1_RegexInOutput,run-2_RegexInOutput - Aggregate (only when repeat > 1):
agg_RegexInOutput
The aggregate assessment value is pass_rate >= threshold, with a rationale like "Aggregate: 2/3 runs passed (threshold=0.6)".
Failure Explanations¶
When a case fails, the summary field includes details about which runs failed and why: