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

Behandel JSON-uitvoer als een voorstel dat je controleert

Parse begrensde JSON, toets een expliciet ticketcontract en handel fouten af met een eindige herstelstrategie.

90 min3 checkpoints
DIT GA JE ONTDEKKEN
  • Je onderscheidt geldige JSON, een geldig schema en een inhoudelijk juist antwoord.
  • Je weigert ambigue gegevens en stopt na een begrensd aantal pogingen.

Een model kan keurig ogende JSON geven met een verkeerd label of een verzonnen citaat. We bouwen daarom drie poorten: parsen leest de tekst, het domeincontract toetst velden en bewijs, en een inhoudelijke beoordeling toetst de betekenis. Geen van die stappen vervangt automatisch de volgende.

Deze oefening verwerkt eigen fictieve tickets met echte Python-controles. Ze doet geen modelcall. Een opgegeven JSON-schema kan de mogelijke vorm beperken, maar bewijst op zichzelf niet dat een antwoord waar is.

1. Lees één begrensd JSON-object

De standaarddecoder leest JSON met json.loads. Onze applicatie vereist bovenaan een object, maximaal 4096 UTF-8-bytes en maximaal acht geneste containers. Dit zijn oefenafspraken; JSON zelf staat ook andere hoofdwaarden toe.

import json
print(json.loads('{"id":"r1"}'))
# {'id': 'r1'}
print(len('é'),len('é'.encode('utf-8')))
# 1 2

Controleer vóór het decoderen de bytegrootte met encode. De tweede regel laat zien waarom tekens tellen onvoldoende is. Accepteer geen codehekken of uitleg rondom het object en gebruik nooit eval voor modeltekst.

Een dictionary kan dubbele sleutels al hebben overschreven. Gebruik daarom object_pairs_hook: de decoder geeft ieder object eerst als een lijst sleutel/waardeparen aan jouw functie. Die kan een herhaalde sleutel weigeren vóór informatie verdwijnt.

def unique(pairs):
    result = {}
    for key,value in pairs:
        if key in result:
            raise ValueError('dubbel')
        result[key] = value
    return result

raw = '{"id":"r1","id":"r2"}'
try:
    json.loads(raw,
        object_pairs_hook=unique)
except ValueError:
    print('geweigerd')  # geweigerd

Ook getallen vragen een expliciete afspraak. De Python-decoder accepteert standaard NaN en Infinity als uitbreiding; parse_constant kan ze weigeren. Een gewone exponent zoals 1e999 kan daarnaast in een oneindige float eindigen, dus controleer gedecodeerde floats met math.isfinite.

def no_constant(text):
    raise ValueError('geen JSON-getal')

try:
    json.loads('{"x":NaN}',
        parse_constant=no_constant)
except ValueError:
    print('geweigerd')  # geweigerd

Loop na het decoderen door dictionaries en lijsten. De hoofdcontainer heeft diepte één; een container daarin diepte twee. Gewone getallen en strings verhogen het aantal geneste containers niet.

def depth(value):
    if isinstance(value,dict):
        children = value.values()
    elif isinstance(value,list):
        children = value
    else:
        return 0
    return 1 + max(
        (depth(x) for x in children),
        default=0)

print(depth({'x':[{'y':1}]}))  # 3

Controleer tijdens diezelfde wandeling ook strings met encode('utf-8'), en controleer sleutels in de objecthook. Zo weiger je losse Unicode-surrogaten, ook wanneer ze via JSON-escapes binnenkomen. Vertaal decodeer-, Unicode- en recursiefouten naar ValueError; maak parse_contract_json met alle genoemde grenzen.

01
CHECKPOINTParse precies één begrensd object

2. Toets velden en letterlijk bewijs

Ons ticket heeft exact id, label en evidence. Het ID hoort bij de aanvraag; label is factuur, storing, vraag of onduidelijk. Een inhoudelijk label heeft evidence met exact start, end en text; bij onduidelijk moet evidence juist None zijn.

Neem “Mijn factuur mist.”. De eerste vijf tekens zijn “Mijn ”; factuur begint op index vijf en eindigt exclusief op twaalf. Het bewijs kan dus onafhankelijk worden teruggevonden.

source = 'Mijn factuur mist.'
evidence = {'start':5,'end':12,
            'text':'factuur'}
ticket = {'id':'r1','label':'factuur',
          'evidence':evidence}
raw = json.dumps(ticket)
value = parse_contract_json(raw)
checked = validate_ticket(
    value,'r1',source)
print(source[5:12])  # factuur
print(checked['label'])  # factuur

Maak validate_ticket met gehele Python-posities: booleans zijn hier geen geldige indices. Het eindpunt moet groter zijn dan het begin en binnen source liggen; de tekst moet exact met die uitsnede overeenkomen. Geef een nieuwe dictionary terug, inclusief een kopie van evidence.

Hetzelfde citaat met label storing zou deze vormcontrole nog steeds passeren. Het label is immers toegestaan en het citaat bestaat echt; of het citaat het label ondersteunt is een inhoudelijk oordeel. Gebruik Python-tekenindices consequent: een JavaScript-client heeft voor Unicode een expliciete omzetting nodig.

02
CHECKPOINTEen geldig citaat is nog geen juiste classificatie

3. Geef herstel een eindpunt

Een ongeldig ticket krijgt hoogstens een beperkt aantal herstelpogingen. De applicatie houdt de teller bij; het model kan zijn eigen limiet niet verhogen. Onze helper kiest alleen accept, retry of reject en doet zelf geen nieuwe aanvraag.

for attempt in [1,2]:
    result = accept_ticket(
        '{}','r1',source,attempt)
    print(result['status'])
# retry
# reject
print(accept_ticket(
    raw,'r1',source,1)['status'])
# accept

Maak accept_ticket met standaard twee pogingen en een instelbare limiet van één tot drie. Controleer ongeldige tellerargumenten vóór de afhandeling. Een fout uit de parser of ticketvalidator wordt retry zolang een poging resteert, anders reject; in beide gevallen is value None.

Verander ontbrekend of verkeerd bewijs niet stilzwijgend in een plausibel citaat. Een afwijzing is informatie over wat de applicatie nog niet betrouwbaar kan gebruiken. Gerichte herstelvragen en inhoudelijke evaluatie blijven afzonderlijke ontwerpkeuzes.

03
CHECKPOINTEen herstelpoging heeft een einde

Wat neem je mee?

  • JSON-syntax, een domeincontract en betekenis zijn drie verschillende poorten.
  • Bytegrenzen en decoderhooks maken het parserbeleid controleerbaar.
  • Beperkt herstel voorkomt een eindeloze lus zonder geldige uitkomst.

Hierna gebruik je hetzelfde principe voor voorstellen om tools uit te voeren.

DE VOLGENDE STAPLaat het model een tool voorstellen en de applicatie beslissen
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
Parse precies één begrensd object

Schrijf parse_contract_json(text) met een dictionary. Controleer UTF-8-lengte vóór parsen, object_pairs_hook voor unieke sleutels, parse_constant voor NaN/Infinity en een boomcontrole voor containerdiepte ≤8 en eindige floats. Root moet een object zijn. Alle ongeldige gevallen geven ValueError.

Een hint

json.loads verwerkt één volledige JSON-waarde. Vang ook RecursionError en UnicodeError af. Verifieer unicode van gedecodeerde sleutels en strings met encode("utf-8").

02
Een geldig citaat is nog geen juiste classificatie

Schrijf validate_ticket(value,request_id,source) met een nieuwe dictionary, inclusief een gekopieerde evidence-dictionary. Request-ID 1–40 tekens, bron 1–2000 tekens. Evidence heeft exact start/end/text; start en end zijn echte ints (geen bool), 0≤start<end≤len(source), text==source[start:end]. Elk contractprobleem geeft ValueError.

Een hint

Controleer set(dictionary) voor exact toegestane velden. Accepteer een onthouding apart en voeg ontbrekende velden niet zelf in.

03
Een herstelpoging heeft een einde

Schrijf accept_ticket(raw,request_id,source,attempt,limit=2). Roep parser en ticketvalidator aan; bij succes {status:"accept",value:ticket}. Bij ValueError uit die twee: {status:"retry",value:None} zolang attempt<limit, anders reject. attempt/limit zijn exacte ints met 1≤attempt≤limit≤3; anders ValueError.

Een hint

Valideer attempt vóór de try/except, zodat een fout in het pogingsbeleid niet als modelantwoordfout wordt verstopt.

Vastgelopen? Bekijk de voorbeeldoplossing
import json
import math

def parse_contract_json(text):
    if not isinstance(text, str) or len(text) > 4096:
        raise ValueError('Maximaal 4096 UTF-8-bytes')
    try:
        if len(text.encode('utf-8')) > 4096:
            raise ValueError('Maximaal 4096 UTF-8-bytes')
        def object_pairs(pairs):
            result = {}
            for key, value in pairs:
                key.encode('utf-8')
                if key in result:
                    raise ValueError('Dubbele sleutel')
                result[key] = value
            return result
        def constant(value):
            raise ValueError('NaN en Infinity zijn geen JSON-getallen')
        result = json.loads(text, object_pairs_hook=object_pairs, parse_constant=constant)
        def walk(value, depth):
            if isinstance(value, (dict, list)):
                if depth > 8:
                    raise ValueError('Maximaal acht geneste containers')
                for child in value.values() if isinstance(value, dict) else value:
                    walk(child, depth + 1)
            elif isinstance(value, float) and not math.isfinite(value):
                raise ValueError('Getal past niet in een eindige float')
            elif isinstance(value, str):
                value.encode('utf-8')
        walk(result, 1)
    except (UnicodeError, RecursionError, json.JSONDecodeError) as error:
        raise ValueError('Geen geldig begrensd JSON-object') from error
    if not isinstance(result, dict):
        raise ValueError('Bovenaan staat een object')
    return result

def validate_ticket(value, request_id, source):
    if not isinstance(request_id, str) or not 1 <= len(request_id) <= 40 or not isinstance(source, str) or not 1 <= len(source) <= 2000:
        raise ValueError('Ongeldige aanvraag')
    if not isinstance(value, dict) or set(value) != {'id', 'label', 'evidence'}:
        raise ValueError('Exact id, label en evidence vereist')
    if value['id'] != request_id or not isinstance(value['label'], str) or value['label'] not in {'factuur', 'storing', 'vraag', 'onduidelijk'}:
        raise ValueError('Aanvraag-ID of label ongeldig')
    evidence = value['evidence']
    if value['label'] == 'onduidelijk':
        if evidence is not None:
            raise ValueError('Een onthouding heeft evidence=null')
        return {'id': request_id, 'label': 'onduidelijk', 'evidence': None}
    if not isinstance(evidence, dict) or set(evidence) != {'start', 'end', 'text'}:
        raise ValueError('Bewijs heeft start, end en text')
    a, b = evidence['start'], evidence['end']
    if type(a) is not int or type(b) is not int or not 0 <= a < b <= len(source):
        raise ValueError('Ongeldige tekenposities')
    if not isinstance(evidence['text'], str) or evidence['text'] != source[a:b]:
        raise ValueError('Het citaat komt niet op die positie uit de invoer')
    return {'id': request_id, 'label': value['label'], 'evidence': dict(evidence)}

def accept_ticket(raw, request_id, source, attempt, limit=2):
    if type(limit) is not int or not 1 <= limit <= 3 or type(attempt) is not int or not 1 <= attempt <= limit:
        raise ValueError('Ongeldige poging of pogingslimiet')
    try:
        value = validate_ticket(parse_contract_json(raw), request_id, source)
    except ValueError:
        return {'status': 'retry' if attempt < limit else 'reject', 'value': None}
    return {'status': 'accept', 'value': value}

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