AI Ops

Evaluating LLM Outputs: Automated Testing with Ragas

By DexNox Dev Team Published May 20, 2026

Modern Retrieval-Augmented Generation (RAG) Evaluation

Deploying Retrieval-Augmented Generation (RAG) pipelines to production introduces a critical operational challenge: how to reliably audit the quality of generated answers and retrieved contexts at scale. Traditional software testing relies on unit tests and static assertions, which fail to capture the stochastic nature of Large Language Models (LLMs). Manual evaluation by human domain experts represents the gold standard for quality, but it is slow, expensive, and impossible to scale across continuous integration and continuous deployment (CI/CD) pipelines.

To solve this, automated evaluation frameworks utilize LLMs as evaluators (LLM-as-a-judge). Ragas (Retrieval Augmented Generation Assessment) is an open-source framework designed to evaluate RAG pipelines by assessing the retrieval and generation stages independently. By isolating these components, engineers can pinpoint whether a system failure stems from poor retrieval (missing or irrelevant documents) or poor generation (hallucinations, disjointed answers, or failure to leverage retrieved text).

Evaluating these components requires a structured suite of metrics, a pipeline for synthesizing representative test datasets, and a robust execution runtime. This guide presents the mathematical formulation of key Ragas metrics, implements an end-to-end evaluation pipeline in Python, compares evaluation methodologies, and details how to mitigate common production failures.


Technical Metric Explanations

Ragas separates the evaluation of the retrieval component from the generation component. The framework relies on three primary metrics: Faithfulness, Answer Relevance, and Context Recall.

1. Faithfulness

Faithfulness measures the factual consistency of the generated answer against the retrieved context. This metric directly addresses the issue of hallucinations. The operational intuition is to identify what fraction of claims made in the generated answer can be directly mapped back to and supported by the retrieved context.

To compute Faithfulness, Ragas runs a two-step LLM chain:

  1. Statement Extraction: The LLM parses the generated answer and extracts all distinct factual statements or claims. Let this set of statements be represented as S.
  2. Statement Verification: For each statement in S, the LLM analyzes the retrieved context C and determines if the statement is logically supported by C. The verification outputs a binary score (1 for supported, 0 for unsupported).

The Faithfulness score is defined as:

Faithfulness = (Number of claims supported by context) / (Total number of claims in generated answer)

If the LLM makes assertions not found in the context, the score drops. A score of 1.0 indicates complete grounding in the retrieved context.

2. Answer Relevance

Answer Relevance measures how well the generated answer addresses the user query. It penalizes answers that are incomplete, redundant, or contain tangential information, regardless of their factual correctness.

To compute Answer Relevance, Ragas uses a reverse-query generation method to avoid direct semantic comparison bias:

  1. The evaluator LLM takes the generated answer and generates multiple potential questions that could be answered by the generated response.
  2. An embedding model converts the generated questions and the original user query into high-dimensional vectors.
  3. The cosine similarity is calculated between each generated question vector and the original query vector.

The Answer Relevance score is the mean cosine similarity:

Answer Relevance = average cosine similarity between generated questions and original query

A high score indicates that the generated answer aligns closely with the user’s intent.

3. Context Recall

Context Recall evaluates the quality of the retrieval system. It measures the extent to which the retrieved context aligns with the ground truth answer. This requires a curated dataset where each user query is paired with a verified ground truth (the target gold standard answer).

The computation steps are:

  1. The evaluator LLM analyzes the ground truth answer and breaks it down into individual statements.
  2. For each statement, the LLM checks if the details can be found within the retrieved context. If the context contains the information, it is marked as found, otherwise not found.

The Context Recall score is computed as:

Context Recall = (Number of ground truth statements found in context) / (Total number of ground truth statements)

A low Context Recall indicates that the retrieval system is failing to fetch the source documents containing the necessary information to construct the target answer.


End-to-End Python Evaluation Pipeline

The following production-ready Python script sets up a custom test set generation pipeline using local document inputs, initializes Ragas metrics with custom LLM configurations, constructs an evaluation dataset, runs the audit asynchronously, and exports the metrics.

import os
import asyncio
import logging
from typing import List, Dict, Any, Optional
from dataclasses import dataclass
from datasets import Dataset

# LangChain and Ragas Imports
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from ragas import evaluate
from ragas.metrics import faithfulness, answer_relevance, context_recall
from ragas.testset.generator import TestsetGenerator
from ragas.testset.evolutions import simple, reasoning, multi_context
from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import TokenTextSplitter

# Setup logging configuration
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(name)s: %(message)s"
)
logger = logging.getLogger("RagasEvaluationPipeline")

@dataclass
class EvaluationConfig:
    openai_api_key: str
    model_name: str = "gpt-4o"
    embedding_model: str = "text-embedding-3-small"
    temperature: float = 0.0
    max_retries: int = 3
    timeout: float = 60.0

class RagasEvaluator:
    def __init__(self, config: EvaluationConfig):
        self.config = config
        os.environ["OPENAI_API_KEY"] = config.openai_api_key
        
        # Initialize LLMs for evaluation and question generation
        self.eval_llm = ChatOpenAI(
            model=config.model_name,
            temperature=config.temperature,
            max_retries=config.max_retries,
            request_timeout=config.timeout
        )
        
        self.generator_llm = ChatOpenAI(
            model="gpt-4o-mini",
            temperature=0.5,
            max_retries=config.max_retries
        )
        
        self.embeddings = OpenAIEmbeddings(
            model=config.embedding_model
        )

    def generate_test_set(
        self, 
        document_paths: List[str], 
        test_size: int = 10
    ) -> Dataset:
        """
        Generates a synthetic evaluation test dataset from raw text documents.
        """
        logger.info(f"Loading documents for test set generation: {document_paths}")
        documents = []
        for path in document_paths:
            try:
                loader = TextLoader(path, encoding="utf-8")
                documents.extend(loader.load())
            except Exception as e:
                logger.error(f"Failed to load document at {path}: {str(e)}")
                raise
        
        # Split documents into chunks optimized for generation
        splitter = TokenTextSplitter(chunk_size=1000, chunk_overlap=200)
        split_docs = splitter.split_documents(documents)
        logger.info(f"Split documents into {len(split_docs)} chunks.")

        # Initialize the generator using the evaluator configurations
        generator = TestsetGenerator.from_langchain(
            generator_llm=self.generator_llm,
            critic_llm=self.eval_llm,
            embeddings=self.embeddings
        )

        # Define question evolution distributions
        distributions = {
            simple: 0.5,
            reasoning: 0.3,
            multi_context: 0.2
        }

        logger.info("Generating synthetic test set. This executes multiple LLM calls.")
        try:
            testset = generator.generate_with_langchain_docs(
                documents=split_docs,
                test_size=test_size,
                distributions=distributions
            )
            df = testset.to_pandas()
            logger.info(f"Successfully generated {len(df)} evaluation pairs.")
            return Dataset.from_pandas(df)
        except Exception as e:
            logger.error(f"Error during test set generation: {str(e)}")
            raise

    async def execute_evaluation(
        self, 
        eval_dataset: Dataset
    ) -> Dict[str, Any]:
        """
        Runs the Ragas evaluation asynchronously over the provided dataset.
        Expected dataset columns: 'question', 'contexts', 'answer', 'ground_truth'
        """
        required_cols = {"question", "contexts", "answer", "ground_truth"}
        current_cols = set(eval_dataset.column_names)
        if not required_cols.issubset(current_cols):
            missing = required_cols - current_cols
            raise ValueError(f"Dataset is missing required columns: {missing}")

        logger.info("Starting Ragas evaluation metric assessment.")
        
        # Configure metrics with custom LLM configurations
        faithfulness.llm = self.eval_llm
        answer_relevance.llm = self.eval_llm
        answer_relevance.embeddings = self.embeddings
        context_recall.llm = self.eval_llm

        metrics = [faithfulness, answer_relevance, context_recall]

        try:
            # Wrap synchronous evaluate call in async executor if running in async environments
            loop = asyncio.get_running_loop()
            result = await loop.run_in_executor(
                None,
                lambda: evaluate(
                    dataset=eval_dataset,
                    metrics=metrics,
                    llm=self.eval_llm,
                    embeddings=self.embeddings
                )
            )
            logger.info("Evaluation metrics successfully computed.")
            return result
        except Exception as e:
            logger.error(f"Failed to execute Ragas evaluation: {str(e)}")
            raise

# Demonstration of execution setup
async def main():
    # Example execution flow
    api_key = os.getenv("OPENAI_API_KEY", "mock-key-for-compilation")
    config = EvaluationConfig(openai_api_key=api_key)
    evaluator = RagasEvaluator(config)
    
    # Create a dummy text document for test generation context
    mock_doc = "temp_kb_doc.txt"
    with open(mock_doc, "w", encoding="utf-8") as f:
        f.write(
            "DexNox Orchestrator operates on port 8080. It uses an isolated sandbox "
            "execution layer to process user workloads. Memory limits are restricted to "
            "512MB per container to avoid out-of-memory cascading faults. Cold storage "
            "is avoided by pre-warming caches with Redis."
        )
    
    try:
        # 1. Generate synthetic test set
        # For demonstration purposes, we show the API surface.
        # In a real pipeline, the target dataset is collected from active system logs.
        logger.info("Initializing synthetic test generation...")
        
        # 2. Run evaluation on target dataset structure
        # Target format matching real evaluation inputs
        eval_data = {
            "question": [
                "Which port does DexNox Orchestrator use?",
                "What is the container memory ceiling for DexNox?"
            ],
            "contexts": [
                ["DexNox Orchestrator operates on port 8080. It uses sandbox isolation."],
                ["Memory limits are restricted to 512MB per container to prevent restarts."]
            ],
            "answer": [
                "The system operates on port 8080.",
                "The memory limit is set to 512MB per container."
            ],
            "ground_truth": [
                "DexNox Orchestrator runs on port 8080.",
                "Each container is capped at 512MB of RAM."
            ]
        }
        
        dataset = Dataset.from_dict(eval_data)
        metrics_result = await evaluator.execute_evaluation(dataset)
        print("--- Evaluation Score Results ---")
        print(metrics_result)
        
    finally:
        if os.path.exists(mock_doc):
            os.remove(mock_doc)

if __name__ == "__main__":
    asyncio.run(main())

Performance Metrics Comparison

Evaluating LLM outputs involves tradeoffs between speed, cost, and evaluation quality. Different approaches suit different stages of the development cycle. The table below compares the performance profiles of the four primary evaluation methods:

Evaluation MethodMetric Precision / Consistency (0.0 to 1.0)Latency per Query (seconds)Cost per 100 Queries (USD)Setup Complexity
Manual Human Audit0.95 (High alignment, human drift exists)120.0s to 300.0s$150.00 to $300.00High (requires UI, expert training)
GPT-4 LLM-as-a-judge0.85 to 0.90 (Consistent, prone to system bias)2.5s to 5.0s$2.00 to $5.00Low (single LLM prompt call)
Ragas Metrics0.82 to 0.88 (Granular separation of metrics)1.5s to 3.5s$0.50 to $1.50Medium (requires parsing schemas)
Basic Regex Assertion0.15 to 0.20 (Brittle, high false negative rate)less than 0.001s$0.00Very Low (standard unit tests)

What Breaks in Production: Failure Modes and Mitigations

Deploying automated LLM evaluations at scale reveals operational edge cases that degrade accuracy or cause unexpected system failures.

1. LLM Evaluator Bias (Self-Preference)

  • Failure Mode: When using a specific LLM model family (e.g., GPT-4) as the evaluator judge, it exhibits a positive bias toward generated text produced by the same model family, scoring it higher than text generated by Claude or Llama. This leads to artificial score inflation.
  • Mitigation: Rotate the evaluator models or use an ensemble average. Evaluate using a different model family than the one generating the responses. For example, use Anthropic’s Claude 3.5 Sonnet to score OpenAI GPT-4o outputs, or calculate a blended metric from multiple judges.

2. Escalating API Costs of LLM-as-a-Judge

  • Failure Mode: Running evaluations on every commit or on continuous production traffic generates thousands of multi-step evaluator calls. Since metrics like Context Recall and Faithfulness require document extraction and validation prompts, API token consumption scales cubically with document chunk sizes.
  • Mitigation: Implement a sampling strategy. Rather than evaluating 100% of production logs, evaluate a random, statistically significant sample (e.g., 5% to 10% of sessions). Additionally, use smaller, fine-tuned local models (like Mistral-7B-Instruct or specialized llama-3-instruct variants) to run basic evaluation metrics on-premise.

3. Evaluator Instability due to Non-Deterministic Responses

  • Failure Mode: System prompts for evaluations return structurally invalid JSON formats or flip scoring decisions between runs, despite setting the model temperature parameter to 0.0. This non-determinism breaks automated CI/CD gating pipelines when a commit randomly fails verification checks.
  • Mitigation: Enforce JSON schema structures using structured output libraries or strict JSON schema flags at the API layer (e.g., response_format={"type": "json_object"}). Additionally, implement a retry mechanism that catches parsing errors and repeat-evaluates the metric to calculate a consensus vote (majority of three runs).

4. Context Window Exhaustion on Long Inputs

  • Failure Mode: When RAG retrievers return large volumes of context (e.g., 20 source chunks of 1000 tokens each), the combined length of the user query, generated answer, and source context exceeds the context window or maximum output tokens of the evaluator model. This throws rate limit errors or causes the model to ignore context boundaries.
  • Mitigation: Truncate or pre-filter retrieved contexts before sending them to the evaluator. Use semantic context compression to retain only the sentences containing semantic overlap with the generated answer, reducing the input payload sizes by up to 70% before evaluating faithfulness.

What is the primary benefit of this design pattern?

It provides deterministic scalability, minimizes resource overhead, and isolates runtime execution contexts safely.

How do we verify the performance improvements?

You can use automated benchmark tools like Apache Benchmark or wrk to measure latency and request throughput.

Frequently Asked Questions

What is the primary benefit of this design pattern?

It provides deterministic scalability, minimizes resource overhead, and isolates runtime execution contexts safely.

How do we verify the performance improvements?

You can use automated benchmark tools like Apache Benchmark or wrk to measure latency and request throughput.