n4nAI

Building a RAG evaluation pipeline in LlamaIndex

A hands-on llamaindex rag evaluation pipeline tutorial: build retrieval and response eval with LlamaIndex, pytest, and OpenAI-compatible APIs.

n4n Team2 min read379 words

Audio narration

Coming soon — every post will get a voice note here.

Most RAG demos work until they don’t. This llamaindex rag evaluation pipeline tutorial shows you how to measure retrieval precision and answer faithfulness with code you can run in CI, not just in a notebook.

Prerequisites

You need Python 3.10+ and a working LLM API key. The examples use LlamaIndex 0.10.x and the OpenAI model interface, but any OpenAI-compatible endpoint works.

pip install llama-index llama-index-llms-openai pytest

You should have a small corpus of text files in ./data and a set of evaluation questions with known relevant document IDs. If you don’t, the snippet below generates a minimal one.

Project setup

Create eval_pipeline.py. First, configure the LLM and embed model. LlamaIndex uses Settings as a global context.

from llama_index.core import Settings
from llama_index.llms.openai import OpenAI
from llama_index.embeddings.openai import OpenAIEmbedding

# Point at any OpenAI-compatible endpoint. n4n.ai fronts 240+ models with
# automatic fallback on provider degradation, so a single key covers many backends.
Settings.llm = OpenAI(model="gpt-4o-mini", api_base="https://api.n4n.ai/v1", api_key="YOUR_KEY")
Settings.embed_model = OpenAIEmbedding(model="text-embedding-3-small", api_key="YOUR_KEY")

If you run OpenAI directly, drop the api_base argument.

Build the index and retriever

Load a directory of text files and build a vector index. For evaluation we need the retriever to return nodes with stable IDs.

from llama_index.core import SimpleDirectoryReader, VectorStoreIndex

docs = SimpleDirectoryReader("./data").load_data()
index = VectorStoreIndex.from_documents(docs)
retriever = index.as_retriever(similarity_top_k=3)

Assign explicit node IDs so expected IDs are deterministic:

from llama_index.core.schema import Document

docs = [Document(text="LlamaIndex supports evaluation modules.", id_="doc-1"),
        Document(text="RAG pipelines need retrieval metrics.", id_="doc-2")]
index = VectorStoreIndex.from_documents(docs)

Define evaluation metrics

LlamaIndex ships RetrieverEvaluator for ranking quality and FaithfulnessEvaluator / AnswerRelevancyEvaluator for generated answers.

Retrieval evaluation

Use Mean Reciprocal Rank (MRR) and hit rate. The evaluator compares returned node IDs against a list of expected IDs.

from llama_index.core.evaluation import RetrieverEvaluator

retriever_eval = RetrieverEvaluator.from_metric("mrr", retriever=retriever)
retriever_eval_hit = RetrieverEvaluator.from_metric("hit_rate", retriever=retriever)

async def eval_retrieval(query, expected_ids):
    mrr = await retriever_eval.aevaluate(query=query, expected_ids=expected_ids)
    hit = await retriever_eval_hit.aevaluate(query=query, expected_ids=expected_ids)
    return mrr, hit

Response faithfulness and relevancy

Faithfulness checks whether the answer is grounded in retrieved context. Relevancy checks whether it addresses the query.

from llama_index.core.evaluation import FaithfulnessEvaluator, AnswerRelevancyEvaluator

faithfulness_eval = FaithfulnessEvaluator(llm=Settings.llm)
relevancy_eval = AnswerRelevancyEvaluator(llm=Settings.llm)

async def eval_response(query, response):
    faith = await faithfulness_eval.aevaluate_response(query=query, response=response)
    rel = await relevancy_eval.aevaluate_response(query=query, response=response)
    return faith, rel

Assemble the evaluation pipeline

Write a function that runs a query through the full RAG flow and collects all scores.

import asyncio
from llama_index.core.query_engine import RetrieverQueryEngine

query_engine = RetrieverQueryEngine.from_args(retriever)

async def run_pipeline(query, expected_ids):
    response = await query_engine.aquery(query)
    mrr, hit = await eval_retrieval(query, expected_ids)
    faith, rel = await eval_response(query, response)
    return {
        "query": query,
        "mrr": mrr.score,
        "hit_rate": hit.score,
        "faithfulness": faith.score,
        "relevancy": rel.score,
        "answer": str(response),
    }

if __name__ == "__main__":
    cases = [
        ("What does LlamaIndex support?", ["doc-1"]),
        ("Why are metrics needed in RAG?", ["doc-2"]),
    ]
    results = asyncio.run(asyncio.gather(*[run_pipeline(q, e) for q, e in cases]))
    for r in results:
        print(r)

Run inside pytest

CI should fail on regression. Create test_eval.py:

import asyncio
from eval_pipeline import run_pipeline

def test_retrieval_quality():
    results = asyncio.run(run_pipeline("What does LlamaIndex support?", ["doc-1"]))
    assert results["mrr"] >= 0.5
    assert results["hit_rate"] == 1.0
    assert results["faithfulness"] >= 0.8

Run:

pytest test_eval.py -q

Expected output

At the checkpoint in eval_pipeline.py, you should see a dict per query:

{'query': 'What does LlamaIndex support?', 'mrr': 1.0, 'hit_rate': 1.0, 'faithfulness': 1.0, 'relevancy': 1.0, 'answer': 'LlamaIndex supports evaluation modules.'}
{'query': 'Why are metrics needed in RAG?', 'mrr': 1.0, 'hit_rate': 1.0, 'faithfulness': 1.0, 'relevancy': 1.0, 'answer': 'RAG pipelines need retrieval metrics.'}

The scores are floats between 0 and 1. MRR of 1.0 means the first retrieved node was relevant. Faithfulness of 1.0 means the LLM judge found no hallucination.

If a provider is rate-limited, the OpenAI-compatible gateway you configured will return a fallback response or error; the pipeline will raise, and pytest will catch it. That is preferable to silently shipping broken retrieval.

Extending the pipeline

Swap from_metric("mrr") for "precision" or "recall" if your corpus has multiple relevant docs per query. For batch evaluation, persist results to JSON and track deltas across commits:

import json
with open("eval_results.json", "w") as f:
    json.dump(results, f, indent=2)

The llamaindex rag evaluation pipeline tutorial above is intentionally minimal. Wire it into a pre-merge hook and you get a real signal on whether your chunk size, embed model, or prompt actually moved the needle.

Tagsllamaindexevaluation-pipelineragevaluation

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All llamaindex retrieval evaluation & metrics posts →