DEV Community

Cover image for RAG evaluation using ragas: a practical guide
Ayush Kumar
Ayush Kumar

Posted on • Originally published at logiclooptech.dev

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 Dockerfile
pip install ragas[all]  # pulls in optional dependencies like transformers
Enter fullscreen mode Exit fullscreen mode

Configuration is minimal. You need two things:

  1. A retriever function that returns a list of passages given a query.
  2. 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, Tuple
from ragas import evaluate
from sentence_transformers import CrossEncoder, SentenceTransformer
import openai

# 1️⃣ Retriever – dense similarity search
embedder = 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 ChatCompletion
def 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()
Enter fullscreen mode Exit fullscreen mode

Now you can feed a dataset of (query, ground_truth, documents) tuples to RAGAS:

from ragas import Dataset

# Example dataset entry
samples = [
    {
        "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() internally
results = evaluate(
    dataset,
    retriever=retrieve,
    generator=generate,
    metrics=["retrieval_precision", "retrieval_recall",
             "answer_faithfulness", "answer_relevance",
             "answer_correctness"],
)
print(results.to_dataframe())
Enter fullscreen mode Exit fullscreen mode

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:

  1. Chunk the knowledge base – see my post on Practical RAG chunking strategies for reliable RAG pipelines.
  2. Store embeddings in Pinecone (or any vector DB).
  3. Retrieve top-k chunks per query.
  4. 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 pinecone
from 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]
Enter fullscreen mode Exit fullscreen mode

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
          fi
Enter fullscreen mode Exit fullscreen mode

The 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.

Top comments (0)