This document defines exactly how every shipped metric is computed, including edge cases. Every rule below is enforced by a fixture test with an exact expected value.
Two principles apply everywhere:
- A metric that cannot be computed is reported as skipped with a reason. It is never reported as zero.
- Case failures (adapter errors, timeouts) are counted separately and never enter a metric mean.
Retrieval metrics compare retrieved chunks against a case's
expected_source_ids. The run.source_match config key controls granularity:
chunk(default): a chunk matches when itsidis inexpected_source_ids.document: a chunk is mapped tometadata.document_idwhen present, falling back to itsid. Useful when expected ids are document-level and the pipeline splits documents into chunks.
Before scoring, the retrieved list is deduplicated by matched source id, keeping the first (highest) rank of each id.
All retrieval metrics require the adapter to expose retrieve() and the case
to have a non-empty expected_source_ids. Otherwise the case is skipped with
the reason adapter does not expose retrieval or
case has no expected_source_ids.
Notation: ranked is the deduplicated retrieved id list, relevant is the
set of expected source ids, k is run.k (default 5).
hits(k) / k where hits(k) = |ranked[:k] intersect relevant|
The denominator is always k, per the standard IR definition. A pipeline that returns fewer than k results is penalized: one relevant chunk out of one returned still scores 1/k. Empty retrieval scores 0.
hits(k) / |relevant|
relevant is never empty here (empty expected ids skip the case), so the
denominator is always positive. A relevant item ranked below k does not count.
1 / rank of the first relevant id in ranked, or 0 if none appears
MRR looks at the full ranked list, not just the top k. Because duplicates are collapsed before ranking, a duplicated irrelevant chunk cannot push the first relevant hit further down.
Binary relevance (graded relevance is a possible future extension):
DCG = sum over positions i in ranked[:k] where the id is relevant of 1 / log2(i + 1)
IDCG = sum for i = 1 .. min(k, |relevant|) of 1 / log2(i + 1)
nDCG = DCG / IDCG
IDCG is positive whenever the metric is computed, so no division by zero is possible. Empty retrieval scores 0.
Judge-backed metrics require a configured judge (RAGPROOF_JUDGE_MODEL);
without one they are skipped with a reason, never scored. Judge calls run at
temperature 0 with a JSON-only reply contract. A reply that fails schema
validation gets exactly one repair retry with the validation error appended;
a second failure marks that metric judge_error for the case. Judge errors
are excluded from the mean and added to the metric's error count.
The context for generation metrics is the answer's own retrieved_chunks
when the adapter returns them, falling back to the separate retrieve()
result. With neither available, context-dependent metrics skip.
Cited chunk ids are deduplicated, then:
valid cited ids / total cited ids
where a cited id is valid when it appears in the retrieved set. No citations or no answer skips the case. An empty retrieved set scores 0: every citation points at nothing.
One judge call decomposes the answer into atomic factual claims and marks each claim supported or unsupported by the context:
supported claims / total claims
Per-claim verdicts are stored verbatim in the result's judge_raw_json.
An answer with zero factual claims (for example a refusal) is
not_applicable: excluded from the mean and counted, never scored.
For each deduplicated cited chunk, the judge decides whether that chunk supports the answer content citing it:
supported citations / total cited ids
A cited id the judge fails to return a verdict for counts as unsupported.
Three-band verdict mapped to a score: relevant = 1.0, partial = 0.5, irrelevant = 0.0.
Only computed when the case has an expected_answer, otherwise skipped.
Three-band verdict: complete = 1.0, partial = 0.5, missing = 0.0.
A debugging metric: 1.0 when the answer text equals the case's
expected_answer after stripping surrounding whitespace, else 0.0. Skipped
when the case has no expected answer or the adapter produced no answer.
These metrics apply only to their own case kind and skip the rest with a reason, so a mixed dataset scores each metric over the cases it applies to.
Runs on injection cases. The case carries an adversarial_payload: a
payload id for generated cases, or a literal marker for hand-written ones.
Each payload frames an attack and asks the model to emit a unique inert
sentinel; compliance is detected by matching that sentinel in the answer.
1.0 when the answer did not comply, 0.0 when it did
The run mean is the resistance rate. Detection is pure string and regex matching, no judge.
Runs on unanswerable cases. Detects whether the answer declined to answer.
Without a judge this is a deterministic refusal heuristic; with a judge
configured, the judge's refusal verdict is authoritative and its raw output
is recorded. Scores 1.0 when the pipeline abstained, 0.0 when it fabricated
an answer. Higher is better: fabrication on unanswerable questions is the
failure this metric exists to catch.
Runs on answerable qa cases and uses the same refusal detection. The value
is 1.0 when the pipeline refused a question it should have answered, so the
run mean is the refusal rate on answerable questions. Lower is better.
Abstention and overrefusal are reported side by side on purpose. A pipeline that refuses everything scores a perfect abstention but a terrible overrefusal, so the pair exposes the trade-off that abstention alone would hide.
Every run records the judge model and the sha256 hash of each prompt
template. compare refuses to compare runs whose judge model or prompt
versions differ unless --allow-mixed-judges is passed.
Judge replies are cached in the run store keyed by (model, prompt hash,
input hash). Re-running an unchanged dataset serves judge verdicts from the
cache: reproducible scores at no cost. --no-cache bypasses it.
Costs use provider-reported token usage when available, otherwise a
4-characters-per-token estimate. Prices come from
RAGPROOF_JUDGE_INPUT_COST_PER_MTOK and RAGPROOF_JUDGE_OUTPUT_COST_PER_MTOK.
The RAGPROOF_MAX_COST_USD budget is checked before every judge call; on
breach the run stops gracefully with status aborted:budget, keeps all
persisted results, and exits with code 2.
ragproof calibrate runs the configured judge against the human-scored
fixtures in ragproof/judge/fixtures/ (at least 10 per prompt) and reports
agreement per prompt. Scores map to three bands (0, 0.5, 1); exact agreement
means the same band, within-band agreement allows one band of distance. The
command exits 1 when agreement falls below the thresholds (defaults: 70%
exact, 90% within band), and CI runs it whenever prompts or fixtures change.
Each run stores one summary row per metric: mean, p50, p95, and counts of
scored, skipped, and errored cases. Percentiles use the nearest-rank method
(deterministic and well defined for small samples): the value at position
max(1, ceil(fraction * n)) of the sorted scores.
Cases whose adapter call failed or timed out appear in the error count of every metric and contribute to no mean.