ragbench

Open-source evaluation harness

Choose a RAG configuration on evidence: quality, latency and cost, with confidence intervals.

ragbench runs a matrix of retrieval and agentic search pipelines over the same queries, scores retrieval, answers, groundedness, latency and cost-to-serve, and recommends the best configuration under your budget. It runs offline with no API keys; Anthropic and OpenAI adapters are optional.

Quickstart View on GitHub Full report

Pipelines compared
8
over 55 queries, 176 documents
Recommended under budget
dense-lsa-k3
answer_f1 0.606, $0.475 per 1k queries, p95 862 ms
Best answer_f1 vs baseline
+0.073
hybrid-rerank-agent3-k5 vs bm25-k5, 95% CI [+0.018, +0.145] (significant), at 2.9x the cost and 2.2x the p95
answer_f1 versus cost per 1,000 queries for every pipeline, with the Pareto frontier and the cost budget
answer_f1 against cost per 1,000 queries on the bundled synthetic help-center corpus. The star is the recommended configuration; the vertical line is the cost budget.

Quickstart

Requires Python 3.11+ and uv. One command runs the whole demo offline, with no API keys:

git clone https://github.com/seanmcrae/ragbench.git && cd ragbench && make demo

make demo runs these three steps, which you can also run one at a time:

uv run rag-eval run configs/example.yaml                 # 8 pipelines x 55 queries
uv run rag-eval report --img-dir docs/img                # runs/example/report.{md,html} + chart
uv run rag-eval compare bm25-k5 hybrid-rerank-agent3-k5  # paired deltas with 95% CIs

Other targets: make ci (lint, format check, type check, tests), make site (the static docs site in site/), make scifact (optional public benchmark, downloads about 3 MB). To use a hosted model instead of the mock, install an extra and export its key:

uv sync --frozen --extra anthropic && export ANTHROPIC_API_KEY=...
# in the experiment YAML: generator: {provider: anthropic, model: claude-haiku-4-5}
#                         judge:     {provider: openai, model: gpt-4o-mini}

Results

Generated by make site from rag-eval run configs/example.yaml (experiment example). Nothing below is typed by hand.

Dataset: synthetic-helpcenter (176 documents, 55 queries, fingerprint 32cc21df822d5c4e). Prices: illustrative table dated 2026-10-07. Run created 2026-10-08T07:36:22+00:00.

Generation latency comes from the offline mock's token-based latency model, not from a live API; retrieval latency is measured wall-clock.

Recommendation

Budget: cost <= $1.00 per 1k queries and p95 latency <= 1,500 ms. Selection: highest answer_f1 on the in-budget Pareto frontier.

Recommended: dense-lsa-k3 (answer_f1 0.606, $0.475 per 1k queries, p95 862 ms).

Versus the baseline bm25-k5: +0.057 answer_f1 [-0.013, +0.145], no significant difference at 95% over 55 queries.

Best answer_f1 regardless of budget: hybrid-rerank-agent3-k5 (0.622, +0.016 vs the recommendation) at 3.9x the cost and 2.3x the p95 latency.

In-budget Pareto frontier (cheapest first): dense-lsa-k3.

In budget but dominated: bm25-k5, hybrid-k5, hybrid-rerank-k5, dense-lsa-k5.

Over budget: hybrid-rerank-agent3-k5 (cost $1.868/1k > $1.00; p95 1961 ms > 1500 ms); dense-lsa-agent3-k3 (cost $1.337/1k > $1.00; p95 1877 ms > 1500 ms); dense-lsa-agent3-k5 (cost $1.916/1k > $1.00; p95 1928 ms > 1500 ms).

All pipelines

pipelineanswer_f1exact_matchjudge_scoregroundednessnDCG@5recall@5MRRp50 msp95 ms$/1k qsteps
bm25-k50.5490.4180.5730.9090.8020.8180.8916528840.6461.00
hybrid-k50.5520.4360.5730.9090.8100.8180.9386528740.6501.00
hybrid-rerank-k50.5490.4180.5730.9090.7950.8090.9036798850.6511.00
hybrid-rerank-agent3-k50.6220.4910.6450.9270.7950.8090.9031,6031,9611.8681.67
dense-lsa-k30.6060.4910.6270.9090.8300.8180.9796358620.4751.00
dense-lsa-agent3-k30.5700.4550.5910.9270.8270.8000.9791,5451,8771.3371.73
dense-lsa-k50.6060.4910.6270.9090.8300.8180.9796478750.6471.00
dense-lsa-agent3-k50.5700.4550.5910.9270.8300.8180.9791,5991,9281.9161.65

Versus the baseline (bm25-k5)

Paired bootstrap over queries: difference in mean answer_f1 with a 95% percentile interval.

pipelinedelta answer_f195% CIpverdict
hybrid-k5+0.003[-0.047, +0.055]0.956no significant difference
hybrid-rerank-k5+0.000[+0.000, +0.000]1.000no significant difference
hybrid-rerank-agent3-k5+0.073[+0.018, +0.145]0.038better
dense-lsa-k3+0.057[-0.013, +0.145]0.095no significant difference
dense-lsa-agent3-k3+0.021[-0.031, +0.091]0.455no significant difference
dense-lsa-k5+0.057[-0.013, +0.145]0.095no significant difference
dense-lsa-agent3-k5+0.021[-0.031, +0.091]0.455no significant difference

answer_f1 by query type

pipelineerror_causeerror_fixfeature_limitfeature_locationfeature_planintegration_objectsintegration_syncmulti_hopplan_attributepolicy
bm25-k50.850.691.000.560.800.000.400.000.710.46
hybrid-k50.850.691.000.390.800.000.600.000.710.46
hybrid-rerank-k50.850.691.000.560.800.000.400.000.710.46
hybrid-rerank-agent3-k50.850.691.000.560.800.000.400.500.710.46
dense-lsa-k30.850.691.000.390.800.001.000.000.810.46
dense-lsa-agent3-k30.850.691.000.390.800.000.400.120.810.46
dense-lsa-k50.850.691.000.390.800.001.000.000.810.46
dense-lsa-agent3-k50.850.691.000.390.800.000.400.120.810.46

hybrid-rerank-agent3-k5 vs bm25-k5, every metric

The highest-quality configuration regardless of budget, against the baseline. Same output as rag-eval compare bm25-k5 hybrid-rerank-agent3-k5.

metricbm25-k5hybrid-rerank-agent3-k5delta95% CIp
answer_f10.5490.622+0.073[+0.018, +0.145]0.038
exact_match0.4180.491+0.073[+0.018, +0.145]0.038
judge_score0.5730.645+0.073[+0.018, +0.145]0.038
groundedness0.9090.927+0.018[+0.000, +0.055]0.695
ndcg@50.8020.795-0.007[-0.027, +0.016]0.485
mrr0.8910.903+0.012[-0.009, +0.039]0.450
$/1k queries0.6461.868+1.222
p95 ms8841,961+1,077

How evaluation works

  1. Run. Each pipeline answers every query: retrieve top-k (optionally rerank from a deeper pool), build a prompt from the passages, generate an answer with [doc-id] citations. An agentic pipeline may ask for another search, up to max_steps. Every stage is timed and every completion records its token usage.
  2. Score each query.
    • Retrieval: recall@k, precision@k, MRR and nDCG@k against graded qrels (2 = states the answer, 1 = supporting or bridge document).
    • Answer: SQuAD-style token F1 and exact match against the reference answers.
    • Groundedness: share of answer sentences that cite a retrieved document, times the share of the answer's content terms found in the documents it cites. Uncited answers score 0.
    • Judge: a 1-5 rubric (correct, complete, supported by cited passages) normalised to [0, 1]; the offline judge maps F1 and citation coverage onto it, an LLM judge reads the passages.
    • Latency and cost: per-stage wall-clock (modeled for the mock generator), and tokens priced from configs/prices.yaml.
  3. Summarise. Means per pipeline, p50/p95 latency, cost per 1k queries, quality by query type.
  4. Decide. Drop configurations over the cost or p95 budget (reasons recorded), take the Pareto frontier of (quality, cost, p95) among the rest, and pick the highest quality, ties to cheaper then faster. Deltas against the baseline use a paired bootstrap over queries.
  5. Gate. rag-eval compare A B --fail-on-regression exits 1 only when B's gate metric is significantly worse than A's (the CI excludes zero) and the drop exceeds --tolerance.

Architecture

Architecture: runner, retrievers, reranker, generator, cache, scoring and reports
Mermaid source
flowchart LR
    cfg["Experiment YAML: pipelines, matrix, budget"] --> run
    data["Dataset: corpus, queries, qrels, answers"] --> run
    run["Runner: each pipeline x each query"] --> ret
    ret["Retriever: BM25, dense or hybrid RRF"] --> rr["Reranker (optional)"]
    rr --> gen["Generator: mock, Anthropic or OpenAI"]
    gen -->|"agentic: next search"| ret
    gen <--> cache[("Response cache: SQLite, content-hashed")]
    gen --> score["Scoring: retrieval, answer, groundedness, judge, latency, cost"]
    prices["Price table YAML"] --> score
    score --> results["runs/NAME: per_query.jsonl, summary.csv, manifest.json"]
    results --> report["rag-eval report: budgeted Pareto pick, bootstrap CIs, MD, HTML, PNG"]
    results --> compare["rag-eval compare: paired deltas, release gate"]
Module Responsibility
datasets/ Dataset model, BEIR-format loader/writer, seeded synthetic generator
retrieval/ BM25, dense retrieval over pluggable embedders, RRF hybrid, rerankers
generation/ Generator protocol, extractive mock, LLM generator, Anthropic/OpenAI clients
judge.py Shared rubric; heuristic judge (default) and LLM-as-judge
pipeline.py Retrieve, rerank, generate; agentic loop with a step budget; per-stage timing
metrics/ recall/precision/MRR/nDCG@k, token F1/EM, citation groundedness, percentiles
cost.py Token usage, price table, cost per 1k queries
cache.py Content-addressed generation and judge cache
config.py, factory.py, runner.py, results.py Experiment matrix, wiring, persistence, summaries
stats.py, pareto.py, report/ Paired bootstrap, frontier and budget pick, rendering

Design decisions

Data

Configuration

An experiment is one YAML file; configs/example.yaml is annotated. Top-level keys:

Key Meaning Default
name Run name; results go to output_dir/name/ required
dataset {kind: synthetic} or {kind: beir, path: ..., split: test}; optional limit synthetic
pipelines / matrix / defaults Explicit pipelines, Cartesian axes, shared settings
quality_metric Metric the recommendation ranks on answer_f1
baseline Pipeline the deltas are measured against first pipeline
budget max_cost_per_1k_usd, max_p95_latency_ms none
prices Price table YAML (USD per million tokens, dated) configs/prices.yaml
judge {provider: heuristic}, or anthropic / openai with a model heuristic
metrics_k Cut-offs for retrieval metrics [1, 3, 5, 10]
bootstrap_samples, seed Bootstrap resamples and RNG seed 2000, 0
output_dir, cache_dir Where runs and the response cache live runs, .rag_eval_cache

Per pipeline: retriever (bm25 with k1/b, dense with an embedder, hybrid with components, rrf_k, weights), reranker (query-likelihood or cross-encoder, depth), generator (mock, anthropic, openai; model, max_tokens, and price_as for the mock), top_k, max_steps. Unknown keys are rejected. Hosted providers read ANTHROPIC_API_KEY or OPENAI_API_KEY from the environment and are only imported when used.

Where it fails

Failure modes from the runs above, split by where the limit sits. Issue links track the ones with a planned fix.

Model and data limits (the offline generator, the corpus, the sample size)

Slice What happens Evidence
Multi-hop questions (8 of 55) Every single-shot pipeline scores 0.00 answer F1; the best agentic pipeline reaches 0.50 answer_f1 by query type in the demo report
integration_objects (2 of 55) 0.00 answer F1 on all 8 pipelines: the right document is retrieved, but the mock extractor misses "synced" for "sync" and quotes the sync schedule instead #5
Public text, offline dense retriever TF-IDF/LSA scores nDCG@10 0.442 on SciFact vs 0.641 for BM25, and 113 of 300 claims get nothing relevant in the top 10 (68 for BM25). On the synthetic corpus the same embedder looks best (MRR 0.979), an artefact of a small templated vocabulary #4
Sample size 55 queries leave the recommended config's +0.057 lift inside the noise CI [-0.013, +0.145]

Design and scaffolding limits (choices in the harness itself)

Slice What happens Evidence
Agentic loop over-searches hybrid-rerank-agent3-k5 issued follow-up searches on 34 of 55 queries though only 8 need a second hop, tripling prompt size (1,730 vs 549 input tokens). On the dense retriever the same loop drops integration_sync F1 from 1.00 to 0.40 per-query records, demo report
SciFact budget configs/scifact.yaml caps cost at $2.00 per 1k, but all three pipelines cost $2.45 to $2.85, so the report recommends nothing on the public benchmark #3
Heuristic judge Built from token F1 and citation coverage, so it moves with answer F1 (identical deltas for the agentic comparison) and adds no independent signal rag-eval compare output
Agentic retrieval metrics Passages are scored in the order the generator saw them, so second-hop results can sit beyond rank k pipeline.py

Considered and rejected: a t-test for comparing pipelines. Answer metrics are bounded and mostly right-or-wrong per query, so normality is a poor assumption at n=55. The paired bootstrap makes no distributional assumption, pairing removes per-question difficulty, and it yields the interval a ship decision needs. The cost is 2,000 resamples per comparison, which is negligible at this size (docs/PRODUCT.md, trade-offs).

The known limitations follow.

Limitations

Product brief

The problem, users, scope, success metrics, trade-offs, risks and roadmap are in the product brief.