Ragas 0.4 Tutorial: An Evaluation Harness That Splits Retrieval Failures from Generation Failures

2026-10-24 8 min read

The assistant answers every question in fluent paragraphs that nobody can trace back to a document. Retrieval returns the wrong chunk, the model ignores the right one, and both look identical in the logs, so the team argues about prompts while the real bug sits in chunking.

💡 What You Will Learn

The assistant answers every question in fluent paragraphs that nobody can trace back to a document. Retrieval returns the wrong chunk, the model ignores the right one, and both look identical in the l

📜 Table of Contents

A RAG system that answers confidently and wrongly is easy to build. The hard part is knowing which of the two halves broke. Retrieval that returns the wrong chunk and a model that ignores the right chunk produce the same symptom from the outside: a fluent paragraph that no one can trace back to a document. Ragas exists to split that symptom into numbers, and version 0.4 changed how you call it.

This walkthrough builds a small evaluation harness against your own documents, using the pieces that ship in ragas 0.4.3 as of 2026-10-24. Every import path and metric signature below was read out of the published wheel, not from memory.

What is in the box right now

Four numbers, four different bugs

Pick metrics because each one can only fail for one reason. That constraint is what makes a dashboard useful.

Read them as a matrix. Recall low, faithfulness high: fix retrieval. Recall high, faithfulness low: fix generation, usually by tightening the instruction to quote and by lowering temperature. Both high, relevancy low: the prompt is answering the wrong thing.

The signatures, exactly

These are the keyword arguments the 0.4.3 wheel requires. Guessing them costs an afternoon of TypeError, so they are worth printing once:

from ragas.metrics.collections import (
    Faithfulness, AnswerRelevancy, ContextPrecision, ContextRecall,
)

Faithfulness(llm=judge).ascore(
    user_input="...question...", response="...answer...",
    retrieved_contexts=["chunk 1", "chunk 2"])
AnswerRelevancy(llm=judge).ascore(user_input="...", response="...")
ContextRecall(llm=judge).ascore(
    user_input="...", retrieved_contexts=[...], reference="...gold answer...")
ContextPrecision(llm=judge).ascore(
    user_input="...", reference="...gold answer...", retrieved_contexts=[...])

Note the asymmetry: recall takes the reference answer, precision takes the reference too but orders its arguments differently. Faithfulness needs the retrieved contexts and nothing else. The judge is created with llm_factory:

from openai import AsyncOpenAI
from ragas.llms import llm_factory

client = AsyncOpenAI()
judge = llm_factory("gpt-4o-mini", client=client)

llm_factory returns an instructor-backed model that produces structured output, which is what the collections metrics insist on. Legacy LangChain-wrapped LLMs are rejected here with an explicit error rather than silently scoring garbage, and the wrapper classes are marked deprecated in favour of llm_factory.

Build the eval set from your own logs

Synthetic questions generated from your documents are systematically easier than the questions real users type. Ragas ships test-set generation and it is fine for a first pass, but a harness built on production traffic will find bugs that generation never invents.

The rule that saves the most time later: capture the retrieved contexts at query time and store them next to the question. If you re-run retrieval when you evaluate, you are measuring a different system than the one that served the user, and quiet index changes will look like model regressions.

# eval_set.jsonl, one row per answered question
{"question": "...", "answer": "...", "contexts": ["...", "..."], "reference": "..."}

Twenty to fifty rows is enough to see a direction, provided the rows are real. Tag each row with the document family it came from so a bad score can be traced to one section of the corpus rather than to "the RAG".

Run it as a batch, not one row at a time

import asyncio, json
from ragas.metrics.collections import Faithfulness, ContextRecall

async def score_row(row, sem, faith, recall):
    async with sem:
        f = await faith.ascore(user_input=row["question"], response=row["answer"],
                               retrieved_contexts=row["contexts"])
        r = await recall.ascore(user_input=row["question"],
                                retrieved_contexts=row["contexts"],
                                reference=row["reference"])
        return {**row, "faithfulness": f.value, "context_recall": r.value}

async def main():
    rows = [json.loads(line) for line in open("eval_set.jsonl")]
    sem = asyncio.Semaphore(4)
    faith, recall = Faithfulness(llm=judge), ContextRecall(llm=judge)
    results = await asyncio.gather(*(score_row(r, sem, faith, recall) for r in rows))
    with open("scores.jsonl", "w") as fh:
        for r in results:
            fh.write(json.dumps(r) + "\n")

asyncio.run(main())

The semaphore matters more than it looks. Four concurrent judge calls is polite to a hosted API and keeps failures distinguishable from rate limits. Set it to fifty and half your rows come back as errors that look like model failures.

Turn scores into a gate

A dashboard nobody reads changes nothing. Two thresholds, checked in CI on every prompt or index change, are enough to start:

Pick the numbers from your own baseline rather than from this article. Run the harness once, record the scores, then treat a five-point drop as a regression. Absolute score levels are not comparable across corpora, and a team that copies someone else thresholds spends a month chasing a gap that means nothing.

What a faithfulness run actually costs

Faithfulness is not one call per answer. It generates atomic statements from the answer, then checks each statement against the context with an NLI call. A twelve-claim answer is roughly thirteen judge calls. Fifty answers at that length is about 650 calls for one metric, which is why teams that retry the whole set on every commit quietly stop running evals. Cache the judge results, or restrict the full set to nightly runs and a ten-row smoke subset to pre-commit.

Four traps that make the numbers meaningless

For rubrics that are not in the box, DiscreteMetric takes a prompt and an allowed_values list and returns a label with a reason, which covers things like "does this answer cite the right policy number". Keep those custom metrics small and specific; a metric that tries to score overall quality ends up measuring style.

Where the batch API still fits

The dataset-style path (EvaluationDataset plus evaluate or aevaluate) still exists in 0.4.3 and is the shorter route when you already have a dataframe of question, answer and context columns. Use it for reporting; use the async per-metric calls when you want one number in isolation, because a batch run that fails on row 37 tells you nothing about why.

FAQ

Do I need an OpenAI key? No, but the judge must support structured output through an instructor-compatible client. Local models work when they can reliably return the schema, and smaller local judges fail on that before they fail on judgement quality.

How many rows before the scores mean anything? Twenty real questions reveal a direction. Below ten, a single unusual answer moves the average by ten points and the harness becomes a random number generator with a CSV.

Are my prompts and data sent anywhere? Ragas sends content to whichever model you configure as judge, and collects anonymised usage analytics by default. Set RAGAS_DO_NOT_TRACK=true if your policy requires it.

Should evaluation live in CI? The ten-row smoke subset yes, the full set no. Full runs go nightly, with the score history committed so regressions have a date.

Once the numbers separate retrieval from generation, the next common failure is orchestration rather than quality; the LangGraph supervisor walkthrough covers routing bugs, and the PDF to Markdown harness covers the ingestion step that quietly ruins recall before a single query runs.

Related Articles
2026-10-20
Best AI Field Service Software in Halifax 2026: Dispatch Across the Harbour and HST-Clean Invoices
2026-10-22
Ollama in Docker With a GPU: The Compose Block, the Env Defaults, and the VRAM Math Behind Silent CPU Offload
2026-10-23
Running a Local LLM on a Linux Server: systemd Units, GPU Checks, and the vLLM vs llama.cpp Call
2026-10-23
PDF to Markdown Before You Build RAG: Docling, Marker and MinerU, and a Harness That Scores All Three
2026-10-19
Best AI Lease Abstraction Software for Commercial Property Managers in 2026: Scoring Clause Coverage, Not Just Fields
2026-10-21
Best AI Invoice Software in Poznan 2026: KSeF Deadlines, 23% VAT and a Two-Person Studio

Written by our editorial team; tools listed here are tested or verified against public sources. Links point to official sites or GitHub repos for reference only โ€” no paid placements.

๐Ÿ’ฌ Comments (0)

No comments yet. Be the first!

Login to comment