Werkplaats / De werkplaats
In jouw eigen tempo
← RAG
MODULE 02 · LES 07

Zoek uit waar de RAG-pijplijn faalt

Meet retrieval, antwoordjuistheid en onthouding apart; voorkom dat één totaalscore de oorzaak verstopt.

75 min3 checkpoints
DIT GA JE ONTDEKKEN
  • Je berekent precision, recall en reciprocal rank op vaste relevante bronlabels.
  • Je onderscheidt ontbrekend bewijs van een fout antwoord ondanks gevonden bewijs.

Probeer ook echte tekstvectoren in het embedding-lab.

Je RAG-systeem geeft soms een fout antwoord. Je kunt dan meer passages ophalen, de context veranderen of het antwoordmodel aanpassen. Eerst moet je weten welke stap faalt.

We bewaren daarom zowel gevonden bron-IDs als het uiteindelijke antwoord. Onze labels en deterministische maten zijn eigen onderwijsbeleid. Onderzoek zoals Ragas onderscheidt eveneens retrieval en generatie; we reproduceren hier geen Ragas-implementatie of LLM-beoordelaar.

1. Kijk naar bronnen én hun rang

Een relevante passage op plek één is eerder bruikbaar dan dezelfde passage op plek tien. Maar één gevonden bron zegt niet hoeveel andere relevante bronnen ontbreken. We meten beide aspecten.

Neem ranking a,x,b en relevante bronnen a,b. Binnen de eerste twee resultaten zit één relevante bron: H=1. Precision deelt door k=2, recall door de twee relevante IDs en reciprocal rank gebruikt de eerste relevante positie r=1.

P@k=HkR@k=H|G|RR@k=1r

G is de set gelabelde relevante bronnen. Zonder relevante hit is RR nul; zonder relevante labels zijn recall en RR hier None. Zulke vragen tellen niet mee in de gemiddelde retrievalscores.

for k in [2, 4]:
    m = retrieval_metrics(
        ['a', 'x', 'b'], ['a', 'b'], k)
    print(m['precision'],
          m['recall'], m['rr'])
# 0.5 0.5 1.0
# 0.5 1.0 1.0

Bij k=4 geeft onze lijst maar drie resultaten. Toch blijft de precisionnoemer vier: lege plaatsen tellen volgens deze afspraak mee. RR kijkt uitsluitend naar de eerste relevante hit, niet naar een gemiddelde van alle relevante rangen.

Schrijf retrieval_metrics met unieke begrensde IDs en een echte integer k van 1 tot 1000. Ongeldige invoer geeft ValueError.

01
CHECKPOINTMeet zowel gevonden bronnen als hun plaats

2. Geef iedere vraag hetzelfde gewicht

Vraag q1 heeft één relevante bron en vindt die. Vraag q2 heeft drie relevante bronnen en vindt er één. Het gemiddelde van de recalls is dus (1+1/3)/2=2/3.

Twee gevonden bronnen gedeeld door vier relevante bronnen geeft .5. Dat is een andere, gepoolde maat. Onze evaluate_rag gebruikt het gemiddelde per antwoordbare vraag.

Met m antwoordbare vragen middelen we hun afzonderlijke recalls R_i en reciprocal ranks RR_i. Ook een antwoordbare vraag met een gemiste bron blijft met nul meetellen.

R¯=1m∑i=1mRiMRR=1m∑i=1mRRi
def example(i, ranking, relevant,
            answer, expected, ok):
    return dict(
        id=i, ranking=ranking,
        relevant=relevant,
        answer=answer,
        expected=expected, supported=ok)

a = example('q1', ['a'], ['a'],
            'ja', 'ja', True)
b = example(
    'q2', ['b'], ['b', 'c', 'd'],
    'nee', 'ja', False)
print(round(evaluate_rag(
    [a, b], 1)['recall'], 3))
# 0.667

Voeg een antwoordbare vraag met een gemiste bron toe. Daarnaast zijn er twee expliciet onbeantwoordbare vragen: eenmaal onthoudt het systeem zich terecht, eenmaal verzint het een antwoord.

None betekent onthouden. De boolean supported komt uit een aparte beoordeling tegen de gebruikte context; gelijkheid met expected bepaalt die boolean niet. In onze oefenset zijn antwoorden exacte teksten of labels.

miss = example('q3', ['x'], ['e'],
               None, 'ja', False)
unknown = example('q4', [], [],
                  None, None, False)
invented = example(
    'q5', [], [],
    'fantasie', None, False)
rows = [a, b, miss, unknown, invented]
r = evaluate_rag(rows, 1)
for key in ['accuracy', 'coverage',
            'faithfulness',
            'unknown_abstention']:
    print(key, round(r[key], 3))
print(round(r['recall'], 3),
      round(r['mrr'], 3))
# accuracy 0.4
# coverage 0.6
# faithfulness 0.333
# unknown_abstention 0.5
# 0.444 0.667

Van vijf vragen zijn q1 en q4 goed, drie krijgen antwoord en daarvan heeft alleen q1 ondersteuning. Van de twee onbeantwoordbare vragen krijgt er één terecht geen antwoord. De vier concrete verhoudingen hieronder gebruiken dus verschillende noemers.

A=25=0.4C=35=0.6F=13U=12

A staat voor accuracy, C voor coverage, F voor faithfulness en U voor unknown_abstention. De gemiddelde recall is nu (1+1/3+0)/3=4/9; MRR is (1+1+0)/3=2/3. Alleen de drie antwoordbare vragen doen aan die gemiddelden mee.

Schrijf evaluate_rag met de gegeven rijvalidatie en verander de invoer niet. Gebruik None bij een lege groepsnoemer. Onbekende goldlabels betekenen in echte data niet automatisch onbeantwoordbaar: onze onderwijsset legt die status expliciet vast.

VIER EIGEN MEETGEVALLEN · GEEN MODEL-BENCHMARKEen betere zoeker lost geen antwoordfout op

Drie vragen hebben één voldoende bron; één vraag is expliciet onbeantwoordbaar. Verander onderdelen en vergelijk de noemers.

Vraag 1Verwacht: tien
Bron in top 1: ja
Antwoord: tienjuist en ondersteund
Vraag 2Verwacht: tien
Bron in top 1: nee
Antwoord: onthoudenbron gemist
Vraag 3Verwacht: tien
Bron in top 1: ja
Antwoord: vijfverkeerd antwoord
Vraag 4Verwacht: onthouden
Bron in top 1: nee
Antwoord: onthoudenterecht onthouden
Recall@1 · 3 vragen67%Juist · alle 450%Ondersteund · 2 antwoorden50%Antwoorddekking50%

Alleen de zoekstap herstellen verandert vraag 2 in onnodig onthouden. Voor een goed antwoord moet ook de generator het aanwezige bewijs gebruiken. Extra gokken verhoogt dekking, maar kan juistheid en ondersteuning verlagen.

02
CHECKPOINTEen correct antwoord kan nog zonder bewijs zijn

3. Leg de diagnosevolgorde vast

Bij q3 ontbreken relevante bronnen én een antwoord. Onze diagnose noemt eerst retrieval-miss. Daardoor verdwijnt de gemiste bron niet achter een latere onthoudingsbeslissing.

for result in diagnose_rag(rows, 1):
    print(result['id'],
          result['reason'])
# q1 supported-correct
# q2 wrong-answer
# q3 retrieval-miss
# q4 appropriate-abstention
# q5 unanswerable-answered

Schrijf diagnose_rag met de beschreven prioriteit. Controleer eerst onbeantwoordbaarheid, dan een retrieval-miss, vervolgens ontbrekend antwoord, antwoordgelijkheid en ondersteuning. Met een gevonden bron maar zonder antwoord volgt unnecessary-abstention; een goed maar onondersteund antwoord heet unsupported-answer.

Een diagnose bewijst geen causale oorzaak. Proberen met de juiste context kan helpen onderzoeken welke stap doorslaggevend was. Onze relevante IDs zijn alternatieve voldoende bronnen; vragen die twee bronnen tegelijk vereisen, vragen een ander bewijsprotocol.

03
CHECKPOINTMaak de foutvolgorde expliciet voordat je bijstuurt

Wat neem je mee?

  • Precision, recall en RR beantwoorden verschillende retrievalvragen.
  • Accuracy, antwoorddekking en ondersteuning gebruiken verschillende cohorten.
  • Een vaste foutprioriteit maakt vergelijken mogelijk, maar bewijst geen causaliteit.

In het notebook kiezen we op dev en meten we de bevroren RAG-variant op test.

DE VOLGENDE STAPBouw je eigen zoekende antwoordgever
JOUW EXPERIMENT

Probeer het zelf

Python
main.py

Python start zodra jij code uitvoert.

UITVOER

Je uitvoer verschijnt hier.
Alles rekent in je eigen browser.

Jouw checkpoints0 / 3
01
Meet zowel gevonden bronnen als hun plaats

Schrijf retrieval_metrics(ranking,relevant,k). Beide lijsten maximaal 1000 unieke niet-lege string-IDs ≤100. k echte int 1–1000. Geef precision,recall,rr volgens de beschreven noemers; recall/rr None bij lege relevant. Ongeldig: ValueError.

Een hint

Rangen beginnen bij 1. Bekijk ranking[:k], niet de hele ranglijst. Set(relevant) maakt lidmaatschap eenvoudig zonder rangorde te verliezen.

02
Een correct antwoord kan nog zonder bewijs zijn

Schrijf evaluate_rag(rows,k). Gebruik de gegeven validate_rag_rows: 1–1000 vaste meetrijen, unieke IDs; expected=None dan en alleen dan relevant leeg; geen supported=True bij answer=None. Geef n,answerable,answered, gemiddelde recall en mrr op antwoordbare vragen, accuracy,coverage,faithfulness,unknown_abstention. Gebruik None voor lege groepsnoemers. Verander de rijen niet.

Een hint

Mean recall geeft iedere antwoordbare vraag hetzelfde gewicht, ook als aantallen relevante bronnen verschillen. Trek geen algemene kwaliteitsconclusie uit deze kleine eigen set.

03
Maak de foutvolgorde expliciet voordat je bijstuurt

Schrijf diagnose_rag(rows,k), met dezelfde invoercontrole. Geef per rij id/reason volgens bovenstaande prioriteit. Redenen zijn precies appropriate-abstention,unanswerable-answered,retrieval-miss,unnecessary-abstention,wrong-answer,unsupported-answer,supported-correct.

Een hint

Controleer eerst expected is None, dan RR@k==0, dan answer is None, dan antwoordgelijkheid en ten slotte supported.

Vastgelopen? Bekijk de voorbeeldoplossing
import re
import hashlib
import numpy as np

def retrieval_metrics(ranking, relevant, k):
    for items in [ranking, relevant]:
        if not isinstance(items, list) or len(items) > 1000 or any(not isinstance(i, str) or not 1 <= len(i) <= 100 for i in items) or len(set(items)) != len(items):
            raise ValueError('Unieke begrensde IDs vereist')
    if type(k) is not int or not 1 <= k <= 1000:
        raise ValueError('k tussen 1 en 1000')
    selected, gold = ranking[:k], set(relevant)
    positions = [rank for rank, identifier in enumerate(selected, 1) if identifier in gold]
    return {'precision': len(positions) / k, 'recall': len(positions) / len(gold) if gold else None,
            'rr': (1 / positions[0] if positions else 0.) if gold else None}

def validate_rag_rows(rows):
    if not isinstance(rows, list) or not 1 <= len(rows) <= 1000:
        raise ValueError('Geef 1 tot 1000 meetrijen')
    seen = set()
    for row in rows:
        if not isinstance(row, dict) or set(row) != {'id', 'ranking', 'relevant', 'answer', 'expected', 'supported'} or not isinstance(row['id'], str) or not 1 <= len(row['id']) <= 100 or row['id'] in seen:
            raise ValueError('Unieke meet-IDs en vaste velden vereist')
        seen.add(row['id']); retrieval_metrics(row['ranking'], row['relevant'], 1)
        if any(v is not None and (not isinstance(v, str) or not v.strip() or len(v) > 2000) for v in [row['answer'], row['expected']]) or type(row['supported']) is not bool:
            raise ValueError('Antwoorden zijn tekst of None; supported is bool')
        if (row['expected'] is None) != (not row['relevant']) or (row['answer'] is None and row['supported']):
            raise ValueError('Deze oefening vereist bronlabels voor antwoordbare vragen en geen support bij onthouding')

def evaluate_rag(rows, k):
    validate_rag_rows(rows)
    metrics = [retrieval_metrics(r['ranking'], r['relevant'], k) for r in rows]
    answerable = [m for m in metrics if m['recall'] is not None]
    answered = [r for r in rows if r['answer'] is not None]
    unknown = [r for r in rows if r['expected'] is None]
    return {'n': len(rows), 'answerable': len(answerable), 'answered': len(answered),
            'recall': sum(m['recall'] for m in answerable) / len(answerable) if answerable else None,
            'mrr': sum(m['rr'] for m in answerable) / len(answerable) if answerable else None,
            'accuracy': sum(r['answer'] == r['expected'] for r in rows) / len(rows),
            'coverage': len(answered) / len(rows),
            'faithfulness': sum(r['supported'] for r in answered) / len(answered) if answered else None,
            'unknown_abstention': sum(r['answer'] is None for r in unknown) / len(unknown) if unknown else None}

def diagnose_rag(rows, k):
    validate_rag_rows(rows)
    result = []
    for row in rows:
        metrics = retrieval_metrics(row['ranking'], row['relevant'], k)
        if row['expected'] is None:
            reason = 'appropriate-abstention' if row['answer'] is None else 'unanswerable-answered'
        elif metrics['rr'] == 0:
            reason = 'retrieval-miss'
        elif row['answer'] is None:
            reason = 'unnecessary-abstention'
        elif row['answer'] != row['expected']:
            reason = 'wrong-answer'
        elif not row['supported']:
            reason = 'unsupported-answer'
        else:
            reason = 'supported-correct'
        result.append({'id': row['id'], 'reason': reason})
    return result

Voer de code uit en verklaar voor jezelf waarom elke stap werkt.