Werkplaats / De werkplaats
In jouw eigen tempo
← Werken met LLM’s
MODULE 01 · LES 01

Maak van een vage vraag een toetsbare opdracht

Scheid rollen, voeg consistente voorbeelden toe en vergelijk twee prompts op precies dezelfde gevallen.

60 min3 checkpoints
DIT GA JE ONTDEKKEN
  • Je bouwt zero-shot- en few-shot-berichten zonder modelgewichten te veranderen.
  • Je vergelijkt promptvarianten per geval en houdt de uiteindelijke testset apart.

Een ticket zegt: “Mijn factuur mist.” De opdracht “Doe iets met dit bericht” laat open of je moet samenvatten, antwoorden of classificeren. Een toetsbare prompt benoemt de taak, de toegestane antwoorden en wat er bij twijfel moet gebeuren.

We gebruiken een fictieve helpdesk met labels factuur, storing, vraag en onduidelijk. “Kies precies één van deze labels; kies onduidelijk als de tekst onvoldoende informatie geeft” is een concreter begin. Of deze formulering werkelijk helpt, meten we later op vastgelegde gevallen.

1. Maak rollen onderdeel van de structuur

Zero-shot betekent hier: een instructie en een nieuwe vraag, zonder demonstraties. De instructie komt in een system-bericht en de vraag in een user-bericht. De rol staat buiten de inhoud; het woord system in een gebruikerstekst maakt daarvan geen systeeminstructie.

rule = (
    'Kies factuur, storing, vraag of '
    'onduidelijk. Bij onvoldoende '
    'informatie: kies onduidelijk.')
text = ' Mijn factuur mist. '
messages = build_messages(rule,text)
print([m['role'] for m in messages])
# ['system', 'user']
print(messages[1]['content'] == text)
# True

Maak build_messages met twee nieuwe berichtdictionaries. Beide teksten moeten na strippen niet leeg zijn en mogen maximaal 2000 Python-tekens bevatten. Controleer die voorwaarden, maar bewaar de oorspronkelijke tekst inclusief witruimte.

Een modelspecifieke chattemplate zet deze berichten daarna om in één tokenreeks. Gebruik de template die bij het getrainde model hoort. Een nette rolstructuur helpt de applicatie, maar garandeert niet dat het model ongewenste instructies uit inhoud negeert; rechten blijven een beslissing van programmacode.

01
CHECKPOINTBewaar de rol buiten de berichttekst

2. Doe hetzelfde antwoordpatroon voor

Few-shot voegt enkele invoer/antwoordvoorbeelden aan de context toe. Het model ziet daardoor een patroon tijdens inferentie; er vinden geen updates van modelgewichten plaats. Laat voorbeelden hetzelfde labelcontract volgen als de uiteindelijke vraag.

examples = [
    {'input':'Waar is mijn bon?',
     'output':'factuur'}]
chat = few_shot_messages(
    rule,examples,'Hallo!')
print([m['role'] for m in chat])
# ['system','user','assistant','user']
print(chat[-1]['content'])  # Hallo!

Maak few_shot_messages door de voorbeelden als user/assistant-paren vóór de nieuwe vraag te plaatsen. Gebruik hoogstens vier voorbeelden, ieder met exact input en output en niet-lege teksten van maximaal 500 tekens. Het totaal van alle contentvelden mag maximaal 6000 tekens zijn; dat is een eigen tekenlimiet, geen tokenbudget.

Kies demonstraties uit trainings- of ontwikkeldata. Een voorbeeld uit je eindtest verraadt een deel van het antwoord en maakt de eindmeting minder eerlijk. Meer voorbeelden zijn bovendien niet automatisch beter: ze kunnen ook een verkeerd patroon of extra contextkosten toevoegen.

02
CHECKPOINTLaat voorbeelden hetzelfde contract voordoen

3. Vergelijk dezelfde gevallen per ID

Stel dat vijf afgesproken ja/nee-antwoorden worden vergeleken. A en B hebben één geval allebei goed, één alleen A goed, twee alleen B goed en één allebei fout. A haalt dan 2/5=.4 en B 3/5=.6.

Δ=Bonly−AonlyN

N is het aantal gedeelde gevallen; de twee tellers bevatten uitsluitend de gevallen waarop de prompts verschillen in juistheid. Hier is Δ=(2−1)/5=.2. De gezamenlijke goede en foute antwoorden veranderen dit verschil niet.

expected = {'1':'ja','2':'nee','3':'ja',
            '4':'nee','5':'ja'}
a = {'1':'ja','2':'ja','3':'nee',
     '4':'nee','5':'nee'}
b = {'1':'ja','2':'nee','3':'ja',
     '4':'ja','5':'nee'}
s = paired_prompt_score(expected,a,b)
print(s['only_a'],s['only_b'])  # 1 2
print(s['accuracy_a'],s['accuracy_b'])
# 0.4 0.6
print(s['delta_b_minus_a'])  # 0.2

Maak paired_prompt_score voor 1–100 gevallen met exact dezelfde IDs. Vergelijk strings letterlijk; een lege voorspelling telt als fout antwoord. De visualisatie gebruikt verzonnen antwoorden en is geen echte modelmeting.

VERGELIJK DEZELFDE GEVALLENGelijke scores kunnen andere fouten verbergen

Een synthetische validatieset van acht gevallen. Verander welke antwoorden van B goed zijn; er draait hier geen taalmodel.

Aantal gevallen per combinatie
Variant A ↓ / B →B goedB fout
A goed31
A fout22
Accuracy A50,0%Accuracy B62.5%B − A12.5 procentpunt

Vergelijk per geval, niet alleen het gemiddelde. Acht verzonnen gevallen bewijzen geen prestatie op nieuwe data.

03
CHECKPOINTDezelfde accuracy kan andere fouten verbergen

Leg bij een echt experiment modelrevisie, promptversie en generatie-instellingen vast. Kies prompts op de validatieset en meet de gekozen versie pas daarna op de onaangeroerde eindtest. Eigen varianten kun je proberen in het model-lab, zonder dat kleine model als automatische beoordelaar te behandelen.

Wat neem je mee?

  • Rollen geven structuur; voorbeelden geven een patroon zonder trainingsupdate.
  • Een uitvoercontract maakt de opdracht toetsbaar.
  • Een eerlijke vergelijking gebruikt dezelfde gevallen en een aparte eindtest.

Hierna controleer je of een antwoord ook werkelijk een bruikbaar JSON-ticket is.

DE VOLGENDE STAPBehandel JSON-uitvoer als een voorstel dat je controleert
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
Bewaar de rol buiten de berichttekst

Schrijf build_messages(instruction,user_text). Retourneer een nieuwe lijst met achtereenvolgens een system- en user-bericht, elk exact role/content. Beide teksten zijn niet-leeg na strip(), maximaal 2000 Python-tekens; verander de oorspronkelijke witruimte niet. Anders ValueError.

Een hint

Valideer de strings, maar gebruik strip() alleen bij de leegtecontrole. Plak de rollen niet aan de tekst vast.

02
Laat voorbeelden hetzelfde contract voordoen

Schrijf few_shot_messages(instruction,examples,user_text). Gebruik build_messages; examples is een lijst van maximaal vier dictionaries met exact input/output, beide niet-lege strings van maximaal 500 tekens. Totale contentlengte maximaal 6000; anders ValueError. Kopieer de berichtenstructuur.

Een hint

Begin met system, voeg per voorbeeld user en assistant toe, eindig met de nieuwe user. Controleer het totaal pas na samenstellen.

03
Dezelfde accuracy kan andere fouten verbergen

Schrijf paired_prompt_score(expected,predictions_a,predictions_b) met both_correct, only_a, only_b, both_wrong, accuracy_a, accuracy_b en delta_b_minus_a. expected bevat 1–100 niet-lege string-IDs en doelstrings. Voorspellingen zijn dictionaries met precies dezelfde IDs en stringwaarden; anders ValueError. Vergelijk exact, zonder stil normaliseren.

Een hint

Loop over de IDs in expected en bereken per ID twee booleans. Een verkeerd of leeg voorspeld label telt als fout, niet als ontbrekend geval.

Vastgelopen? Bekijk de voorbeeldoplossing
def build_messages(instruction, user_text):
    if any(not isinstance(s, str) or not s.strip() or len(s) > 2000 for s in [instruction, user_text]):
        raise ValueError('Geef twee niet-lege teksten van maximaal 2000 tekens')
    return [{'role': 'system', 'content': instruction}, {'role': 'user', 'content': user_text}]

def few_shot_messages(instruction, examples, user_text):
    base = build_messages(instruction, user_text)
    if not isinstance(examples, list) or len(examples) > 4:
        raise ValueError('Maximaal vier voorbeelden')
    result = [base[0]]
    for example in examples:
        if not isinstance(example, dict) or set(example) != {'input', 'output'}:
            raise ValueError('Een voorbeeld heeft input en output')
        if any(not isinstance(v, str) or not v.strip() or len(v) > 500 for v in example.values()):
            raise ValueError('Voorbeeldteksten zijn niet leeg en maximaal 500 tekens')
        result.extend([{'role': 'user', 'content': example['input']},
                       {'role': 'assistant', 'content': example['output']}])
    result.append(base[1])
    if sum(len(m['content']) for m in result) > 6000:
        raise ValueError('Het totale tekenbudget is 6000')
    return result

def paired_prompt_score(expected, predictions_a, predictions_b):
    if not isinstance(expected, dict) or not 1 <= len(expected) <= 100:
        raise ValueError('Gebruik 1 tot 100 vastgelegde validatiegevallen')
    if any(not isinstance(k, str) or not k or not isinstance(v, str) or not v for k, v in expected.items()):
        raise ValueError('IDs en doelantwoorden zijn niet-lege strings')
    for predictions in [predictions_a, predictions_b]:
        if not isinstance(predictions, dict) or set(predictions) != set(expected) or any(not isinstance(v, str) for v in predictions.values()):
            raise ValueError('Beide varianten moeten exact dezelfde gevallen beantwoorden')
    counts = {'both_correct': 0, 'only_a': 0, 'only_b': 0, 'both_wrong': 0}
    for key, target in expected.items():
        a, b = predictions_a[key] == target, predictions_b[key] == target
        name = 'both_correct' if a and b else 'only_a' if a else 'only_b' if b else 'both_wrong'
        counts[name] += 1
    n = len(expected)
    return {**counts, 'accuracy_a': (counts['both_correct'] + counts['only_a']) / n,
            'accuracy_b': (counts['both_correct'] + counts['only_b']) / n,
            'delta_b_minus_a': (counts['only_b'] - counts['only_a']) / n}

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