RAGEval: Offline RAG Evaluation Toolkit for Faithfulness, Retrieval Metrics, and Hallucination Checks
Keywords: RAG evaluation, LLM evaluation, retrieval metrics, faithfulness, hallucination checking, context precision, context recall, answer relevance, groundedness, RAGAS alternative
Point RAGEval at a set of
(question, retrieved_contexts, answer)records. It returns a scorecard: faithfulness, context precision, context recall, answer relevance, and a freshness signal. It also names the weakest step in the pipeline.
RAGEval is a small Python toolkit for RAG evaluation. You already have questions, the chunks a retriever returned, and the answers a model generated. RAGEval scores those triples. It does not retrieve, and it does not generate. It tells you whether the retriever missed evidence, whether the generator invented claims, and whether the contexts are stale.
| Piece | Role |
|---|---|
| Deterministic metrics | Token and n-gram overlap. No API key. This is the offline scorecard. |
LLMJudge |
Optional Anthropic tool-use loop (claude-opus-5) for faithfulness and answer relevance. Falls back to the overlap proxies with no key. |
evaluate |
Runs selected metrics, aggregates means, names the weakest metric. |
| CLI | rageval run and rageval demo print the scorecard and write a JSON report. |
How do I evaluate a RAG pipeline without an API key?
Write one JSONL row per example with question, retrieved_contexts, answer, and optional ground_truth. Run rageval run records.jsonl. RAGEval scores faithfulness (answer vs contexts), context precision and recall, answer relevance (question keywords in the answer), and freshness from context timestamps. No embeddings, no network.
What does a low faithfulness score mean? The answer uses tokens and n-grams that do not appear in the retrieved chunks. That is a groundedness / hallucination signal: the generator is adding claims the context does not support. The optional judge asks the same question at claim level and still degrades to this proxy if the key is missing.
Where is the pipeline weak?
The scorecard prints Weakest: plus a one-line diagnosis. Low context_recall is a retriever miss. Low context_precision is unrelated chunks. Low faithfulness is hallucination. Low answer_relevance is an off-topic answer. Low freshness is a stale corpus; re-rank with freshness-aware-rag or recrawl.
Most RAG eval stacks start with an LLM judge and an embedding model. That is useful, and it is also slow, billed, and hard to run in CI. RAGEval starts the other way: a pure overlap scorecard that a unit test can pin, then an opt-in judge for the two judgment metrics.
| Embedding + LLM eval suites (RAGAS and similar) | RAGEval | |
|---|---|---|
| Needs a key for the usual path | Yes | No |
| Faithfulness | LLM-as-judge | Token/n-gram overlap; optional judge |
| Context precision / recall | LLM and/or embeddings | Question-keyword coverage / gold-token recall |
| Answer relevance | LLM or embeddings | Question keyword coverage; optional judge |
| Freshness / recency | Not a built-in | Exponential decay from context timestamps |
| CI without network | Extra work | Default |
| Adding a metric | Framework plugin | One file and @register |
Tradeoffs: overlap cannot see paraphrases. "Paris" and "the French capital" will not match. If you need semantic similarity, use an embedding suite or turn on --judge. RAGEval will not replace a full LLM eval board. It will give you a number you can recompute offline.
Everything below is what the code does, not a wish list.
- Input. An
EvalRecordis a question, a list of retrieved contexts, a generated answer, and optional gold text. Contexts may be strings or{text, timestamp, source}objects. Load them from JSONL, a JSON array, or CSV (rageval.io.load_records). - Faithfulness. Unigram precision of the answer against the concatenated contexts, mixed with bigram precision (weights 0.6 / 0.4). Empty answer scores 1.0 (no unsupported claims). Non-empty answer with empty retrieval scores 0.0.
- Context precision. For each chunk, the fraction of question content keywords found in that chunk (stopwords dropped). Extra words in a long on-topic chunk do not hurt. The record score is the mean of those chunk scores. The reason line also reports hit-rate at
hit_threshold(default 0.1). Gold text is not used here. - Context recall. Unigram recall of the gold tokens in the concatenated contexts. Skipped when
ground_truthis missing. - Answer relevance. Set recall of question content tokens (stopwords dropped) in the answer. "What is the capital of France?" is scored on
capitalandfrance. - Freshness. For each dated chunk,
exp(-age_hours / half_life_hours)with default half-life 168 hours. Mean over dated chunks. Skipped when no timestamps are present. Same decay idea as freshness-aware-rag. - Aggregate.
evaluatebuilds aScorecard: per-record results, per-metric means (skipped records excluded), an unweightedoverall, andweakestplus a diagnosis string. - Optional judge. If
ANTHROPIC_API_KEYis set and you pass--judge(oruse_judge=True),LLMJudgestarts a tool-use loop (get_record,get_proxy_scores,submit_judgment) onclaude-opus-5. Arefusalstop reason, a missing SDK, a missing key,--no-judge, or any exception falls back to the overlap proxies.
records.jsonl
│
▼
tokenize ──► faithfulness, precision, recall, relevance
timestamps ─► freshness
│
▼
optional LLMJudge (faithfulness + answer relevance)
│
▼
Scorecard (per record, means, weakest, JSON report)
Python 3.10 or newer.
git clone https://github.com/pandeyvishwas51-oss/rageval.git
cd rageval
python3 -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
pytest -qRuntime dependency: anthropic (imported only if you set a key). pydantic is an optional extra (pip install -e ".[pydantic]"); the toolkit itself uses dataclasses. Deterministic metrics, the demo, and --no-judge do not call the network.
rageval demo
rageval --now 2026-08-21T12:00:00Z --out report.json demo
rageval --json --metrics faithfulness,freshness run examples/sample.jsonlexamples/sample.jsonl covers a grounded Paris fact, a Nile hallucination, unrelated retrieval for photosynthesis, a Mars recall miss, a 2018 library report (stale), an off-topic Mars answer, and a 2026 library opening (fresh).
from datetime import datetime, timezone
from rageval import EvalRecord, evaluate, EvalConfig
now = datetime(2026, 8, 21, 12, 0, tzinfo=timezone.utc)
records = [
EvalRecord(
id="q1",
question="What is the capital of France?",
retrieved_contexts=["Paris is the capital and most populous city of France."],
answer="Paris is the capital of France.",
ground_truth="Paris is the capital of France.",
)
]
card = evaluate(records, config=EvalConfig(now=now))
print(card.means)
print(card.weakest, card.diagnosis)
print(card.format_text())from rageval import faithfulness, EvalRecord
rec = EvalRecord(
question="How long is the Nile?",
retrieved_contexts=["Many sources give the Nile a length of about 6650 kilometres."],
answer="The Nile is 12000 kilometres long and flows through Spain.",
)
print(faithfulness(rec).score, faithfulness(rec).reason)from rageval import LLMJudge, evaluate
card = evaluate(records, use_judge=True) # degrades without a key
# or
results = LLMJudge(force_proxy=True).judge(records[0])Set ANTHROPIC_API_KEY if you want the model loop. Use --no-judge (the default) on the CLI to stay on proxies even when a key is present.
{
"id": "q1",
"question": "What is the capital of France?",
"retrieved_contexts": [
{"text": "Paris is the capital of France.", "timestamp": "2026-08-20T09:00:00Z"}
],
"answer": "Paris is the capital of France.",
"ground_truth": "Paris is the capital of France."
}CSV columns: id,question,answer,ground_truth,retrieved_contexts,timestamps. Separate multiple chunks with |||.
Create one module under src/rageval/metrics/, decorate a pure function, import it from metrics/__init__.py:
from rageval.metrics.base import register
from rageval.models import EvalConfig, EvalRecord, MetricResult
@register("my_metric")
def my_metric(record: EvalRecord, config: EvalConfig | None = None) -> MetricResult:
return MetricResult(name="my_metric", score=1.0, reason="stub")Python 3.10+. Tests run on 3.10, 3.11, 3.12, and 3.13 in CI.
No. The overlap metrics, freshness, sample demo, and JSON report run without one. The key is only for the optional tool-use loop.
claude-opus-5, constant rageval.DEFAULT_MODEL. A refusal stop reason is treated as a fallback to the overlap proxies, not as a crash.
Faithfulness asks whether the answer is supported by the retrieved chunks (groundedness / hallucination). Answer relevance asks whether the answer addresses the question. An answer can be faithful and still off-topic, or on-topic and still invented.
context_recall is skipped. context_precision scores chunks against the question instead. Faithfulness, answer relevance, and freshness do not need gold.
None of the retrieved chunks had a timestamp. Pass ISO-8601 strings or unix seconds on each context. Undated chunks in a mixed record are ignored; dated ones still score.
No. That is the point of the optional judge. The offline path is a lower bound you can run in CI.
Yes. CI unsets ANTHROPIC_API_KEY. The Anthropic layer is mocked or skipped. Hypothesis checks that every metric stays inside [0, 1] or is skipped.
pip install -e ".[dev]"
pytest -qMIT. Free for commercial and personal use.
Related: freshness-aware-rag re-ranks retrieved chunks by recency and source authority before you generate. RAGEval scores the result after you generate.