Naar inhoud
lightbulb Welkom op de nieuwe kennisbank | We hebben de docs volledig vernieuwd met meer dan 160 features. Bekijk wat nieuw isarrow_forward

RAG evalueren met RAGAS

Meet de kwaliteit van een RAG-systeem met RAGAS: context precision, context recall, faithfulness en answer relevance, plus een werkende evaluatiepipeline.

Waarom RAG evalueren moeilijk is

RAG-evaluatie is uitdagender dan gewone model-evaluatie omdat je drie componenten tegelijk meet: de retriever, de context en de generator. Een goed antwoord kan gebaseerd zijn op slechte retrieval, bijvoorbeeld doordat het model de gaten vulde met parametrische kennis. Andersom kan slechte retrieval leiden tot een antwoord dat goed oogt maar feitelijk incorrect is.

Je hebt daarom twee soorten evaluatie nodig:

  1. Component-evaluatie: hoe goed is de retriever afzonderlijk?
  2. End-to-end-evaluatie: hoe goed is het systeem als geheel?
info

Wat is RAGAS?

RAGAS (Retrieval Augmented Generation Assessment) is een veelgebruikt framework voor RAG-evaluatie. Het berekent de kernmetrieken zonder dat je een aparte, handmatig geannoteerde set nodig hebt: een LLM-as-judge beoordeelt de kwaliteit. De vier kernmetrieken zijn context precision, context recall, faithfulness en answer relevance.

Testset opbouwen

Begin met een set representatieve vragen met een verwacht antwoord (ground_truth). Houd de bronnen erbij zodat je later ook recall kunt controleren.

interface RAGTestCase {
  question: string;
  ground_truth: string;
  source_documents?: string[];
}

const testCases: RAGTestCase[] = [
  {
    question: "Hoe vraag ik verlof aan?",
    ground_truth: "Verlof aanvragen doe je via het HR-portaal. Log in, ga naar Verlof, selecteer de data en kies het verloftype. Je leidinggevende ontvangt automatisch een goedkeuringsverzoek.",
    source_documents: ["hr-handleiding-sectie-3"],
  },
];

Context precision meten

Context precision meet hoe relevant de opgehaalde chunks zijn en of de relevante chunks bovenaan staan:

async function measureContextPrecision(
  question: string,
  retrievedDocs: string[],
  groundTruth: string
): Promise<number> {
  const relevanceScores = await Promise.all(
    retrievedDocs.map(async (doc) => {
      const response = await anthropic.messages.create({
        model: "claude-haiku-4-5",
        max_tokens: 10,
        messages: [{
          role: "user",
          content: `Is dit document relevant voor de vraag en het verwachte antwoord?
Antwoord alleen: ja of nee.

Vraag: ${question}
Verwacht antwoord: ${groundTruth}
Document: ${doc}`,
        }],
      });
      const text = response.content[0].type === "text" ? response.content[0].text.toLowerCase() : "";
      return text.includes("ja") ? 1 : 0;
    })
  );

  let precisionAtK = 0;
  let relevantSoFar = 0;
  for (let k = 0; k < relevanceScores.length; k++) {
    if (relevanceScores[k] === 1) {
      relevantSoFar++;
      precisionAtK += relevantSoFar / (k + 1);
    }
  }
  const totalRelevant = relevanceScores.reduce((a, b) => a + b, 0);
  return totalRelevant > 0 ? precisionAtK / totalRelevant : 0;
}

Faithfulness meten

Faithfulness meet of het gegenereerde antwoord consistent is met de context. De aanpak is in twee stappen: extraheer eerst de feitelijke claims, controleer daarna of elke claim door de context wordt ondersteund.

async function measureFaithfulness(
  answer: string,
  context: string[]
): Promise<number> {
  const claimsResponse = await anthropic.messages.create({
    model: "claude-haiku-4-5",
    max_tokens: 500,
    messages: [{
      role: "user",
      content: `Extraheer alle feitelijke claims uit dit antwoord als een JSON-array van strings.

Antwoord: ${answer}

Geef alleen de JSON-array terug:`,
    }],
  });
  const claimsText = claimsResponse.content[0].type === "text" ? claimsResponse.content[0].text : "[]";
  const claims: string[] = JSON.parse(claimsText.match(/\[.*\]/s)?.[0] ?? "[]");

  if (claims.length === 0) return 1;

  const contextStr = context.join("

");
  const supportedChecks = await Promise.all(
    claims.map(async (claim) => {
      const response = await anthropic.messages.create({
        model: "claude-haiku-4-5",
        max_tokens: 10,
        messages: [{
          role: "user",
          content: `Wordt deze claim ondersteund door de context? Antwoord alleen: ja of nee.

Context: ${contextStr}
Claim: ${claim}`,
        }],
      });
      const text = response.content[0].type === "text" ? response.content[0].text.toLowerCase() : "";
      return text.includes("ja") ? 1 : 0;
    })
  );

  return supportedChecks.reduce((a, b) => a + b, 0) / claims.length;
}

Answer relevance meten

Answer relevance keert de vraag om: laat het model vragen genereren die bij het antwoord passen en meet hoe sterk die op de oorspronkelijke vraag lijken via embeddings.

async function measureAnswerRelevance(
  question: string,
  answer: string,
  numVariants = 3
): Promise<number> {
  const variantsResponse = await anthropic.messages.create({
    model: "claude-haiku-4-5",
    max_tokens: 300,
    messages: [{
      role: "user",
      content: `Genereer ${numVariants} mogelijke vragen die dit antwoord zou beantwoorden.
Geef elke vraag op een nieuwe regel.

Antwoord: ${answer}`,
    }],
  });
  const variantsText = variantsResponse.content[0].type === "text" ? variantsResponse.content[0].text : "";
  const generatedQuestions = variantsText.split("
").filter(Boolean).slice(0, numVariants);

  const questionEmbedding = await embedText(question);
  const similarities = await Promise.all(
    generatedQuestions.map(async (q) => {
      const emb = await embedText(q);
      return cosineSimilarity(questionEmbedding, emb);
    })
  );

  return similarities.reduce((a, b) => a + b, 0) / similarities.length;
}

Complete evaluatiepipeline

Knoop de metrieken aan elkaar en draai ze over je hele testset. Het gemiddelde per metriek vormt je RAGAS-rapport.

async function evaluateRAGSystem(
  testCases: RAGTestCase[],
  ragFunction: (q: string) => Promise<{ answer: string; retrieved_docs: string[] }>
): Promise<RAGASReport> {
  const results = await Promise.all(
    testCases.map(async (tc) => {
      const { answer, retrieved_docs } = await ragFunction(tc.question);
      const [contextPrecision, faithfulness, answerRelevance] = await Promise.all([
        measureContextPrecision(tc.question, retrieved_docs, tc.ground_truth),
        measureFaithfulness(answer, retrieved_docs),
        measureAnswerRelevance(tc.question, answer),
      ]);
      return { contextPrecision, faithfulness, answerRelevance };
    })
  );

  const avg = (key: keyof typeof results[0]) =>
    results.reduce((sum, r) => sum + r[key], 0) / results.length;

  return {
    context_precision: avg("contextPrecision"),
    faithfulness: avg("faithfulness"),
    answer_relevance: avg("answerRelevance"),
    num_test_cases: testCases.length,
  };
}
lightbulb

Houd kosten in de hand

Elke metriek doet meerdere LLM-calls per testcase. Met 100 testcases tikt dat snel aan. Gebruik een goedkoop, snel model zoals claude-haiku-4-5 voor de judge-stappen, cache resultaten per testcase en draai de volledige evaluatie alleen bij releases, niet bij elke wijziging.

Interpretatie van scores

Metriek < 0.6 0.6 tot 0.8 > 0.8
Context precision Slechte retrieval, verbeter chunking of embeddings Acceptabel, overweeg reranking Goed
Faithfulness Model hallucineert, versterk de system prompt Acceptabel Goed
Answer relevance Antwoorden zijn off-topic Acceptabel Goed
warning

Een score is geen eindoordeel

LLM-as-judge is niet onfeilbaar. Een hoge faithfulness-score betekent dat het antwoord past bij de opgehaalde context, niet dat de context zelf klopt. Combineer de cijfers met een handmatige steekproef van de slechtst scorende cases om te zien wat er echt misgaat.

Hoeveel testcases heb ik nodig?

Minimaal 50 voor een eerste indicatie en 100 tot 200 voor betrouwbare benchmarks. Zorg voor diversiteit: makkelijke en moeilijke vragen, verschillende documenttypen en ook vragen waarop de kennisbank geen antwoord heeft.

Kan ik RAGAS ook zonder de Python-library gebruiken?

Ja. Je kunt de metrieken zelf implementeren zoals in de voorbeelden hierboven. De Python-library (te installeren met pip install ragas) is een gemak, geen vereiste.

Hoe maak ik een testset zonder handmatige annotatie?

Gebruik een LLM om vraag-antwoordparen te genereren vanuit je eigen documenten. Dat is synthetische data: valideer altijd een steekproef handmatig voordat je de testset vertrouwt.

Wat doe ik als faithfulness laag is?

Voeg een verificatie-instructie toe aan de system prompt, zoals: geef alleen antwoorden die direct te herleiden zijn tot de context en zeg expliciet wanneer de context de vraag niet beantwoordt. Controleer daarnaast of je retrieval de juiste chunks aanlevert.

Waarom mist context recall in de pipeline?

Context recall vergelijkt de opgehaalde chunks met je source_documents of ground_truth. Je hebt er dus betrouwbare bronlabels voor nodig. De voorbeeldpipeline hierboven richt zich op metrieken die zonder die labels werken; voeg recall toe zodra je per testcase weet welke chunks de juiste zijn.

Welk model gebruik ik als judge?

Een snel en goedkoop model volstaat meestal, bijvoorbeeld claude-haiku-4-5. Voor grensgevallen of een audit kun je dezelfde cases nog eens door een sterker model halen en de oordelen vergelijken.