ASDRP · Agentic RAG Track · Module 07

Retriever Metrics

Score your retriever with two complementary RAGAS metrics — context precision and context recall — and learn to read what each number is really telling you.

Precision = are retrieved chunks relevant & ranked well?  ·  Recall = did we find everything needed?
ragas LLMContextPrecisionWithReference LLMContextRecall
Built by mui-group @ ASDRP· Advanced Student Directed Research Program

What we'll cover

Contents

1

Concept: the two metrics

  • Precision vs Recall intuition
  • The fishing analogy
  • Why they pull in opposite directions
2

Code: evaluate() + single-sample debug

  • RAGAS model setup (judge + embeddings)
  • Building the EvaluationDataset
  • evaluate() over 8 golden questions
  • single_turn_ascore() for one sample
3

Cautions & next steps

  • When precision misleads
  • When recall grades your test set
  • LLM-judge variance
  • Preview: Module 08 adds entity recall + noise

Retriever evaluation

Why two metrics for one retriever?

Context Precisionquality & rank
Of all the chunks you returned, how many were actually useful — and were the useful ones near the top?
Context Recallcoverage
Did the retriever surface every chunk of evidence needed to answer this question fully?
⚠ KEY INSIGHT
These two metrics trade off against each other. Retrieving fewer chunks lifts precision but hurts recall. Retrieving many chunks improves recall but lets irrelevant noise in.
Context precision and context recall diagram

Intuition

The fishing analogy

Precision
"Did I mostly catch fish, not old boots?"
High precision = most returned chunks were relevant. Score drops if garbage chunks fill the top slots.
Recall
"Did I catch all the fish that were in the lake?"
High recall = every useful passage was retrieved. Score drops if you left important evidence behind.
You can be an excellent fisherman (high precision) and still leave half the fish uncaught (low recall) — or net everything in the lake (high recall) and haul up mostly boots (low precision).

Both numbers are needed. A system with 0.9 precision and 0.4 recall is broken in a different, equally serious way from one with 0.4 precision and 0.9 recall.

LLMContextPrecisionWithReference

How RAGAS computes context precision

from ragas.metrics import LLMContextPrecisionWithReference

# RAGAS sends each (question, chunk, reference) triple to the judge LLM
# and asks: "Is this chunk useful for answering the question given this reference?"
# It weights the verdicts by position — top-ranked relevant chunks score higher.
metric = LLMContextPrecisionWithReference(llm=judge_llm)
Inputs required
  • user_input — the question
  • retrieved_contexts — list of chunks the retriever returned
  • reference — the gold-standard reference answer
Score interpretation
0 → every top slot is irrelevant  |  1 → relevant chunks dominate and rank first.
The position weighting means a relevant chunk at rank 1 counts more than the same chunk at rank 5.

LLMContextRecall

How RAGAS computes context recall

from ragas.metrics import LLMContextRecall

# RAGAS decomposes the reference answer into individual statements,
# then checks each statement: "Is there a retrieved chunk that supports this?"
# Recall = fraction of reference statements that are covered.
metric = LLMContextRecall(llm=judge_llm)
Inputs required
  • user_input — the question
  • retrieved_contexts — list of chunks the retriever returned
  • reference — the gold-standard reference answer (decomposed into statements)
Score interpretation
0 → no reference statement was supported by retrieved chunks  |  1 → every reference statement was found.
This grades coverage, not ranking.

Comparison

Precision vs Recall — side by side

Property LLMContextPrecisionWithReference LLMContextRecall
Question it answers "Are retrieved chunks relevant and well-ordered?" "Did we retrieve all the needed information?"
Compares against Reference answer (for relevance verdict) Reference answer statements (coverage check)
Sensitive to ranking? Yes — position-weighted No — only cares about presence, not order
Hurt by too few chunks? No (fewer is often better) Yes — missed evidence lowers recall
Hurt by noisy chunks? Yes — irrelevant chunks at top = low score No — extra noise is ignored

Step 0 · Setup

Environment setup: shared .env + RAGAS stub

import os
from dotenv import load_dotenv, find_dotenv
load_dotenv(find_dotenv())          # resolves to tutorials/.env
HAVE_KEYS = bool(os.environ.get("OLLAMA_API_KEY"))

# RAGAS 0.4.3 import stub — must come BEFORE importing ragas
import sys, types
_vx = types.ModuleType("langchain_community.chat_models.vertexai")
class ChatVertexAI: pass
_vx.ChatVertexAI = ChatVertexAI
sys.modules["langchain_community.chat_models.vertexai"] = _vx

import nest_asyncio; nest_asyncio.apply()   # RAGAS uses asyncio; notebooks already run a loop
⚠ COST NOTE Every evaluate() call sends each (question × chunk) pair to the judge LLM. With 8 golden questions and k=10 chunks each, that is ~80 LLM calls per metric. Keep k small during development.

Step 1 · RAGAS models

Wiring the judge LLM + embeddings

import litellm
from ragas.llms import llm_factory
from ragas.embeddings.base import embedding_factory

JUDGE_MODEL     = "ollama_chat/gemma4:31b-cloud"
EMBEDDING_MODEL = "ollama/qwen3-embedding:0.6b"

judge_llm       = llm_factory(JUDGE_MODEL, provider="litellm",
                               client=litellm.completion, temperature=0.0)
ragas_embeddings= embedding_factory("litellm", model=EMBEDDING_MODEL,
                                     api_base=os.environ["OLLAMA_API_BASE"])
Why a separate judge model?
The generator writes answers; the judge grades them. Using the same model for both creates a conflict of interest — it tends to rate its own outputs highly. gemma4:31b-cloud is a deliberate different model at temperature 0.

Step 2 · Dataset

Building the EvaluationDataset from the golden set

import json
from ragas.dataset_schema import SingleTurnSample, EvaluationDataset

golden = json.load(open("golden_questions.json"))

samples = [
    SingleTurnSample(
        user_input=g["question"],
        retrieved_contexts=rag_result["retrieved_contexts"],
        response=rag_result["response"],
        reference=g["reference"],
    )
    for g in golden
    for rag_result in [rag_answer(g["question"])]
]

eval_dataset = EvaluationDataset(samples=samples)
print(f"Dataset: {len(samples)} samples")

Each SingleTurnSample bundles everything the metrics need: the question, what the retriever fetched, the generated answer, and the reference answer to grade against.

Step 3 · Evaluate

Running evaluate() over the full dataset

from ragas import evaluate
from ragas.metrics import LLMContextPrecisionWithReference, LLMContextRecall

retriever_metrics = [
    LLMContextPrecisionWithReference(),
    LLMContextRecall(),
]

results = evaluate(
    dataset=eval_dataset,
    metrics=retriever_metrics,
    llm=judge_llm,
    embeddings=ragas_embeddings,
)

df = results.to_pandas()
print(df[["user_input", "context_precision", "context_recall"]])
Illustrative output (live values depend on your run):
context_precision ≈ 0.78  |  context_recall ≈ 0.71

Step 4 · Single-sample debug

Debugging one sample with single_turn_ascore()

import asyncio

# Pick a sample that scored low on recall and inspect it
sample = samples[6]   # the multi-hop silver question

precision_score = asyncio.run(
    LLMContextPrecisionWithReference(llm=judge_llm).single_turn_ascore(sample)
)
recall_score = asyncio.run(
    LLMContextRecall(llm=judge_llm).single_turn_ascore(sample)
)

print(f"Question: {sample.user_input}")
print(f"Precision: {precision_score:.3f}   Recall: {recall_score:.3f}")
print(f"Retrieved {len(sample.retrieved_contexts)} chunks")
Why this matters: aggregate scores hide per-question variation. Multi-hop questions often show lower recall because a single retrieval pass may miss a second required chunk.

Watch out

Three things that make these numbers lie

⚠ High precision + low recall is easy to miss
Every chunk you returned was relevant, but you missed half of what you needed. This looks like success on a precision dashboard but the answer is still incomplete.
⚠ Recall grades your test set, not just your retriever
Recall is measured against the reference you wrote. If your reference is vague or incomplete, recall will look worse than the retriever really is. The metric is only as good as the golden set.
⚠ LLM-judge variance
RAGAS uses an LLM to decide relevance. Re-running the same dataset can produce slightly different scores. Report means over multiple runs when you need precise comparisons; don't read too much into a 0.02 difference.

Summary

What you built in Module 07

1

Two complementary metrics

  • Precision: relevant chunks ranked high
  • Recall: all needed evidence retrieved
  • They trade off — you need both
2

Live evaluation code

  • RAGAS judge + embeddings wired via LiteLLM
  • evaluate() over 8 golden questions
  • single_turn_ascore() for sample-level debug
3

Honest limitations

  • Precision hides recall gaps
  • Recall grades the golden set too
  • LLM-judge variance — report means

Run the notebook: 07_retriever_metrics.ipynb  ·  Next: Module 08 — More Retriever Metrics (ContextEntityRecall + NoiseSensitivity)