RAG Evaluation: Retrieval vs Generation

Score RAG retrieval and generation quality independently with five metrics in one evaluate() call to pinpoint whether failures are at retrieval or generation.

📝
TL;DR

Score retrieval quality and generation quality independently with five metrics to pinpoint whether your RAG pipeline fails at retrieval or generation.

Open in ColabGitHub
TimeDifficultyPackage
15 minIntermediateai-evaluation
Prerequisites

Install

pip install ai-evaluation
export FI_API_KEY="your-api-key"
export FI_SECRET_KEY="your-secret-key"

Tutorial

Set up a RAG test case

Define a realistic query, retrieved context chunks, and generated answer. This example simulates a company knowledge-base RAG system.

query = "What is the refund policy and how long does processing take?"

retrieved_context = (
    "Chunk 1: Customers may request a full refund within 30 days of purchase. "
    "Refunds are processed within 5-7 business days after approval. "
    "Chunk 2: To initiate a refund, contact support@example.com with your order number. "
    "Chunk 3: Gift cards and promotional items are non-refundable. "
    "Chunk 4: Our company was founded in 2015 and is headquartered in San Francisco."
)

generated_answer = (
    "You can request a full refund within 30 days of purchase. "
    "Once approved, refunds are processed in 5-7 business days. "
    "To start, email support@example.com with your order number. "
    "Gift cards and promotional items cannot be refunded."
)

Chunk 4 is irrelevant to the query, a common retrieval problem. The metrics below surface it.

You should see: nothing yet, this step only defines the fixtures the rest of the tutorial scores.

Score retrieval quality

evaluate() takes an eval name (or a list of eval names), the fields that eval needs as keyword arguments, and a model (or engine) to run it on. Each of the five RAG metrics reads a different combination of context, input, and output:

MetricStageWhat it measures
context_relevanceRetrievalAre the retrieved chunks relevant to the query?
chunk_attributionRetrievalWas the context chunk used in generating the response?
chunk_utilizationRetrievalHow effectively does the response use the context chunks?
completenessGenerationDoes the response fully address all parts of the query?
factual_accuracyGenerationAre the facts in the output correct?

For hallucination-specific metrics (faithfulness, groundedness, and context_adherence), see Hallucination Detection.

Run the three retrieval metrics individually first, so each score and reason is easy to read on its own.

from fi.evals import evaluate

# Context relevance: are the retrieved chunks relevant to the query?
relevance = evaluate(
    "context_relevance",
    context=retrieved_context,
    input=query,
    model="turing_flash",
)
print(f"Context Relevance: score={relevance.score}, passed={relevance.passed}")
print(f"  Reason: {relevance.reason}\n")

# Chunk attribution: was the context chunk used in the response?
attribution = evaluate(
    "chunk_attribution",
    context=retrieved_context,
    output=generated_answer,
    model="turing_flash",
)
print(f"Chunk Attribution: score={attribution.score}, passed={attribution.passed}")
print(f"  Reason: {attribution.reason}\n")

# Chunk utilization: how effectively does the response use the context?
utilization = evaluate(
    "chunk_utilization",
    context=retrieved_context,
    output=generated_answer,
    model="turing_flash",
)
print(f"Chunk Utilization: score={utilization.score}, passed={utilization.passed}")
print(f"  Reason: {utilization.reason}\n")

You should see (illustrative, scores and reasons vary by run):

Context Relevance: score=0.75, passed=True
  Reason: Three of four chunks are relevant to the query; Chunk 4 is unrelated.

Chunk Attribution: score=1.0, passed=True
  Reason: Every claim in the output maps to a specific context chunk.

Chunk Utilization: score=0.75, passed=True
  Reason: The output uses content from 3 of 4 retrieved chunks.

evaluate() called with a single eval name returns an EvalResult, with .score, .passed, .reason, .eval_name, .latency_ms, .status, and .error fields.

Tip

Low context_relevance or chunk_utilization with high chunk_attribution means your retriever is fetching irrelevant chunks. Fix your embedding model or retrieval logic. High relevance but low attribution means the LLM is generating claims not grounded in any chunk.

Score generation quality

These metrics evaluate whether the LLM fully addressed the query and produced factually accurate claims. To make the diagnostic concrete, score a deliberately wrong answer first, then fix it and rerun.

from fi.evals import evaluate

# Completeness: does the response fully address the query?
completeness = evaluate(
    "completeness",
    input=query,
    output=generated_answer,
    model="turing_flash",
)
print(f"Completeness: score={completeness.score}, passed={completeness.passed}")
print(f"  Reason: {completeness.reason}\n")

# Factual accuracy: introduce a claim the context doesn't support.
bad_answer = (
    "You can request a full refund within 30 days of purchase. "
    "Refunds are processed within 24 hours after approval. "
    "To start, email support@example.com with your order number."
)
bad_accuracy = evaluate(
    "factual_accuracy",
    input=query,
    output=bad_answer,
    context=retrieved_context,
    model="turing_flash",
)
print(f"Factual Accuracy (bad answer): score={bad_accuracy.score}, passed={bad_accuracy.passed}")
print(f"  Reason: {bad_accuracy.reason}\n")

# Fix: match the processing time actually stated in the context, then rerun.
accuracy = evaluate(
    "factual_accuracy",
    input=query,
    output=generated_answer,
    context=retrieved_context,
    model="turing_flash",
)
print(f"Factual Accuracy (fixed): score={accuracy.score}, passed={accuracy.passed}")
print(f"  Reason: {accuracy.reason}\n")

You should see (illustrative):

Completeness: score=1.0, passed=True
  Reason: The response fully addresses the query including refund eligibility, processing time, and exceptions.

Factual Accuracy (bad answer): score=0.25, passed=False
  Reason: The context states refunds are processed within 5-7 business days, not 24 hours; this claim is unsupported.

Factual Accuracy (fixed): score=1.0, passed=True
  Reason: All stated facts are accurate and confirmed by the provided context.

Fixing the one unsupported claim moves factual_accuracy from score=0.25, passed=False to score=1.0, passed=True.

Run all five metrics for a diagnostic overview

Pass a list of eval names to score several metrics in one call. The result comes back as an iterable BatchResult.

from fi.evals import evaluate

diagnostic = evaluate(
    [
        "context_relevance",
        "chunk_attribution",
        "chunk_utilization",
        "completeness",
        "factual_accuracy",
    ],
    input=query,
    output=generated_answer,
    context=retrieved_context,
    model="turing_flash",
)

print("=== RAG Pipeline Diagnostic ===\n")
for result in diagnostic:
    print(f"{result.eval_name:<22} score={result.score} passed={result.passed}")
    print(f"  Reason: {result.reason}\n")

You should see (illustrative):

=== RAG Pipeline Diagnostic ===

context_relevance      score=0.75 passed=True
  Reason: Three of four chunks are relevant; Chunk 4 is off-topic.

chunk_attribution      score=1.0 passed=True
  Reason: Every output claim maps to a specific context chunk.

chunk_utilization      score=0.75 passed=True
  Reason: Output uses 3 of 4 chunks; Chunk 4 is unused.

completeness           score=1.0 passed=True
  Reason: The response fully addresses all parts of the query.

factual_accuracy       score=1.0 passed=True
  Reason: All stated facts are accurate and confirmed by the context.

Interpret results: retrieval problem or generation problem?

Use the diagnostic output to decide where to focus your effort.

PatternDiagnosisFix
Low context_relevance + low chunk_utilizationRetriever fetches irrelevant chunksImprove embeddings, re-rank, or tune top-k
High context_relevance + low chunk_attributionLLM fabricates claims beyond the contextAdd grounding instructions to the system prompt
High context_relevance + low completenessLLM doesn’t fully address the queryRestructure the prompt to cover all parts of the question
High context_relevance + low factual_accuracyLLM distorts facts from the contextSwitch to a more capable model or reduce temperature
All highPipeline is working wellMonitor over time for regressions
# Build a name-keyed lookup, then gate on the real passed field.
scores = {result.eval_name: result for result in diagnostic}

retrieval_ok = scores["context_relevance"].passed and scores["chunk_utilization"].passed
generation_ok = scores["completeness"].passed and scores["factual_accuracy"].passed

if not retrieval_ok:
    print("Action: improve retrieval. Check embeddings, re-ranking, or top-k settings.")
elif not generation_ok:
    print("Action: improve generation. Tune the prompt, lower temperature, or switch models.")
else:
    print("Pipeline healthy.")

You should see: Pipeline healthy. for the example data above, since all five metrics pass.

Note

For deeper hallucination analysis (checking whether the output contradicts or drifts from the context), combine these metrics with faithfulness and groundedness from Hallucination Detection.

Troubleshooting

SymptomCauseFix
status="failed", error="Local metric 'context_relevance' not found in registry"Called evaluate() without model= or engine=, so it defaulted to the local engine, which doesn’t have this evalPass model="turing_flash" (or engine="turing"): these five metrics are cloud-only
TypeError indexing an EvalResult (e.g. result[0])Calling evaluate() with a single eval name returns an EvalResult, not a BatchResult; it isn’t indexable or iterableAccess fields directly: result.score, result.passed; only a list of eval names returns an iterable BatchResult
AttributeError: 'EvalResult' object has no attribute 'name'Using .name instead of .eval_name on an EvalResultUse result.eval_name
Looking up a metric with .get("...") on a BatchResult returns NoneMisspelled or wrong eval name: .get() matches on eval_name and returns None instead of raisingCheck spelling against the metric table, or iterate for r in diagnostic and print r.eval_name to confirm the exact names returned
Every metric returns a low score on data that looks correctMissing or wrong keyword argument for that eval (e.g. passing output= when the eval needs context=)Check the eval’s required fields against the metric table above and pass exactly those keyword arguments
chunk_attribution and chunk_utilization disagree sharplyThey measure different things: attribution is a pass/fail grounding check, utilization is a coverage scoreRead both reason fields before concluding the retriever is at fault
AuthenticationError or 401 from the SDKFI_API_KEY or FI_SECRET_KEY not exported in the current shellRe-run the export commands from the Install step and confirm with echo $FI_API_KEY

For a deeper walkthrough of running evals against the platform’s built-in and Turing models, see Running Your First Eval.

Was this page helpful?

Questions & Discussion