# RAG evalueren met RAGAS [[TOC]] ## 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 title="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. ```typescript 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: ```typescript async function measureContextPrecision( question: string, retrievedDocs: string[], groundTruth: string ): Promise { 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. ```typescript async function measureFaithfulness( answer: string, context: string[] ): Promise { 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. ```typescript async function measureAnswerRelevance( question: string, answer: string, numVariants = 3 ): Promise { 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. ```typescript async function evaluateRAGSystem( testCases: RAGTestCase[], ragFunction: (q: string) => Promise<{ answer: string; retrieved_docs: string[] }> ): Promise { 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, }; } ``` :::tip title="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 | :::warn title="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. ::: :::faq ### 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. :::