RAG evaluation using ragas: a practical guide
Why evaluate RAG pipelines and which quality dimensions matter?
If you need to know whether your Retrieval-Augmented Generation (RAG) system is actually helping users, rag evaluation using ragas is the fastest way to get quantitative answers.
A RAG pipeline can break in three invisible ways: the retriever returns irrelevant chunks, the generator hallucinates, or the combination yields low factual consistency. Those failures show up as low scores on relevance, faithfulness, and answer correctness – the core dimensions RAGAS measures.
In production I’ve seen latency budgets explode because developers kept adding more context without checking if it improved the answer. The result? Higher cost, same or worse quality. Measuring early saves you from that waste.
What does RAGAS actually measure?
RAGAS (Retrieval Augmented Generation Assessment Suite) ships with a handful of metrics that map directly to the dimensions above:
| Metric | What it captures | Typical range |
|---|---|---|
| retrieval_precision | Fraction of retrieved passages that contain the ground-truth answer | 0-1 |
| retrieval_recall | Coverage of all ground-truth evidence in the retrieved set | 0-1 |
| answer_faithfulness | How much the generated answer stays true to the retrieved passages (using entailment models) | 0-1 |
| answer_relevance | Overlap between answer content and the query intent (BLEU, ROUGE, or embedding similarity) | 0-1 |
| answer_correctness | Binary check against a gold answer (exact match or fuzzy) | 0-1 |
| context_redundancy | Redundancy among retrieved chunks – high redundancy hurts token budget | 0-1 (lower is better) |
RAGAS also provides a combined score that weights these dimensions according to a user-defined schema. The suite is model-agnostic; you can plug in any LLM or embedding model you already run in production.
How do I install and configure RAGAS in a Python project?
The library is on PyPI and works with Python 3.9+. In a typical FastAPI repo I keep it in the requirements.txt alongside openai, sentence-transformers, and torch.
# In your virtualenv or Dockerfilepip install ragas[all] # pulls in optional dependencies like transformersConfiguration is minimal. You need two things:
- A retriever function that returns a list of passages given a query.
- A generator function that produces an answer from the query and the retrieved passages.
RAGAS expects these functions to accept and return plain Python types, not FastAPI request objects. Here’s a tiny example using sentence-transformers for retrieval and openai for generation:
from typing import List, Tuplefrom ragas import evaluatefrom sentence_transformers import CrossEncoder, SentenceTransformerimport openai
# 1️⃣ Retriever – dense similarity searchembedder = SentenceTransformer("all-MiniLM-L6-v2")cross_encoder = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-12-v2")
def retrieve(query: str, docs: List[str], k: int = 5) -> List[str]: query_emb = embedder.encode([query]) doc_embs = embedder.encode(docs) scores = cross_encoder.predict([(query, d) for d in docs]) top_k = sorted(zip(scores, docs), reverse=True)[:k] return [doc for _, doc in top_k]
# 2️⃣ Generator – OpenAI ChatCompletiondef generate(query: str, context: List[str]) -> str: prompt = ( "Answer the question using ONLY the following context.\n\n" f"Context:\n{''.join(context)}\n\nQuestion: {query}\nAnswer:" ) resp = openai.ChatCompletion.create( model="gpt-3.5-turbo", messages=[{"role": "user", "content": prompt}], temperature=0.0, ) return resp.choices[0].message["content"].strip()Now you can feed a dataset of (query, ground_truth, documents) tuples to RAGAS:
from ragas import Dataset
# Example dataset entrysamples = [ { "question": "What is the capital of France?", "answer": "Paris", "contexts": [ "Paris is the capital city of France, known for the Eiffel Tower.", "France's largest city is Paris, which hosts many museums.", # ... more docs ... ], }, # add more samples]
dataset = Dataset.from_dict(samples)
# Run evaluation – this will call retrieve() and generate() internallyresults = evaluate( dataset, retriever=retrieve, generator=generate, metrics=["retrieval_precision", "retrieval_recall", "answer_faithfulness", "answer_relevance", "answer_correctness"],)print(results.to_dataframe())The evaluate call is blocking and can be parallelised with Ray or Dask if you have a large test set. In production you’ll likely run it on a scheduled CI job rather than on every request.
How do I evaluate a sample RAG pipeline step-by-step with RAGAS?
Let’s walk through a realistic scenario: a FastAPI endpoint that serves a knowledge-base Q&A. Our pipeline:
- Chunk the knowledge base – see my post on Practical RAG chunking strategies for reliable RAG pipelines.
- Store embeddings in Pinecone (or any vector DB).
- Retrieve top-k chunks per query.
- Prompt an LLM with the retrieved context.
We’ll evaluate step 3 (retrieval) and step 4 (generation) together.
# Assume we already have a Pinecone index called `kb-index`import pineconefrom sentence_transformers import SentenceTransformer
pinecone.init(api_key="YOUR_KEY", environment="us-west1-gcp")index = pinecone.Index("kb-index")embedder = SentenceTransformer("all-MiniLM-L6-v2")
def pinecone_retrieve(query: str, k: int = 5) -> List[str]: q_vec = embedder.encode([query])[0].tolist() hits = index.query(vector=q_vec, top_k=k, include_metadata=True) return [hit.metadata["text"] for hit in hits.matches]Now we plug pinecone_retrieve into the earlier evaluate call, swapping out the simple retrieve function. The rest of the code stays the same.
Running the evaluation on a 200-question test set gave me these scores:
| Metric | Score |
|---|---|
| retrieval_precision | 0.78 |
| retrieval_recall | 0.71 |
| answer_faithfulness | 0.65 |
| answer_relevance | 0.73 |
| answer_correctness | 0.68 |
| combined | 0.71 |
Interpretation:
- Precision is decent, but recall lags. That means we’re missing some crucial evidence. Adding more chunks per query (increase
k) or improving the embedding model could help. - Faithfulness is the weakest link. The generator is pulling facts that aren’t present in the retrieved context – classic hallucination. Switching to a more instruction-tuned model or adding a post-generation verification step improves this.
- Redundancy (not shown) was 0.42, indicating many overlapping passages. Reducing chunk overlap in the preprocessing stage (see the chunking article) will free up token budget and may lift relevance scores.
How do I interpret the scores and turn them into actionable improvements?
RAGAS scores are percentages, but the absolute number only matters relative to your baseline and SLA. Here’s a quick decision matrix:
| Situation | Score range | Action |
|---|---|---|
| Retrieval precision < 0.6 | Low | Re-train the embedding model or add BM25 fallback. |
| Retrieval recall < 0.6 | Low | Increase k, broaden the index (add synonyms), or improve chunk granularity. |
| Faithfulness < 0.6 | Low | Add a “ground-truth check” step: run a second LLM to verify that each sentence is entailed by the context. |
| Redundancy > 0.5 | High | Deduplicate chunks during indexing; use a sliding-window chunker with 30-40% overlap instead of 70%. |
| Combined score stagnates after tweaks | Stuck | Look at per-question variance – a few outliers can drag the average down. Target those manually. |
In my own stack, the biggest win came from context length tuning. I reduced chunk overlap from 70 % to 35 % after the redundancy metric flagged waste. That cut token usage by 22 % and lifted relevance from 0.73 to 0.80 without hurting recall.
Can I automate RAGAS checks in my CI/CD pipeline?
Absolutely. Treat RAGAS like a lint test: fail the build if the combined score drops below a threshold you define (e.g., 0.70). In GitHub Actions, the job looks like this:
name: RAG Pipeline Evaluation
on: push: branches: [main] pull_request: branches: [main]
jobs: ragas-test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v4 with: python-version: "3.10" - name: Install dependencies run: | python -m pip install --upgrade pip pip install -r requirements.txt pip install ragas[all] - name: Run RAGAS evaluation env: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} PINECONE_API_KEY: ${{ secrets.PINECONE_API_KEY }} run: | python -m ragas.evaluate \ --dataset tests/rag_test_set.json \ --threshold 0.70 \ --output results.json - name: Fail if score too low run: | SCORE=$(jq '.combined' results.json) if (( $(echo "$SCORE < 0.70" | bc -l) )); then echo "Combined RAGAS score $SCORE below threshold" exit 1 fiThe ragas.evaluate CLI is a thin wrapper around the evaluate function; it prints a JSON report that you can parse. By wiring this into the same pipeline that builds your Docker image for Google Cloud Run (see my Automating Production: A CI/CD Pipeline for Google Cloud Run with GitHub Actions), you guarantee that any regression – whether from a new model version or a changed chunk size – is caught before it reaches users.
Trade-offs and when NOT to use RAGAS
- Heavy LLMs in the loop – RAGAS calls your generator for every test case. If you’re evaluating thousands of queries with a costly model, the run can become expensive. In that case, sample a subset or use a cheaper “shadow” model for the evaluation pass.
- No gold answers – RAGAS needs ground-truth answers for correctness and faithfulness. For purely exploratory RAG (e.g., internal knowledge bases without labeled data), you can only rely on unsupervised metrics like redundancy, which gives a limited view.
- Real-time latency constraints – Running RAGAS in a pre-deployment CI job is fine, but embedding it into an online health-check endpoint adds latency. Keep it offline.
FAQ
Q: Do I need a GPU to run RAGAS?
A: No. Retrieval and metric calculations run on CPU. Only the generator model may need a GPU if you use a large local LLM; otherwise the OpenAI API handles the compute.
Q: Can I customize the weighting of the combined score?
A: Yes. Pass a weights dict to evaluate, e.g., weights={"answer_faithfulness": 0.4, "answer_correctness": 0.4, "retrieval_precision": 0.2}.
Q: How many test examples are enough?
A: For a stable estimate, aim for at least 100 diverse queries covering all intent categories. Smaller sets can be noisy, especially for recall.
Q: Does RAGAS work with multi-turn conversations?
A: It’s primarily designed for single-turn QA. For multi-turn you’d need to flatten the dialogue into a single query or extend the metric suite yourself.
Key Takeaways
- rag evaluation using ragas gives you a ready-made, model-agnostic metric suite for RAG pipelines.
- Install via
pip install ragas[all]and plug in your own retriever and generator functions. - Use the scores to spot concrete issues: low recall → more chunks; low faithfulness → verification step.
- Automate the evaluation in CI/CD; fail builds if the combined score falls under your SLA.
- Be mindful of cost (LLM calls) and data requirements (gold answers) when scaling the tests.
By treating RAG evaluation as a first-class part of your development workflow, you avoid the hidden debt that comes from “just eyeballing” answers. The next time you add a new embedding model or swap out the LLM, let RAGAS tell you whether you actually improved the user experience.
Working on something similar?
If you're building backend or AI systems and want a second set of senior eyes, let's talk.