Werkplaats / De werkplaats
In jouw eigen tempo
← AI-security & red teaming
MODULE 02 · LES 08

Begrens het werk van een hele agentuitvoering

Controleer vijf gedeelde budgetten vóór een volgende stap en tel geweigerde stappen niet als verbruikt werk.

90 min2 checkpoints
DIT GA JE ONTDEKKEN
  • Je telt totale kosten, maximale diepte en fan-out van een eindig oefenplan.
  • Je stopt vóór overschrijding en houdt het verbruik van de geaccepteerde prefix bij.

Een assistent krijgt één vraag, maar maakt daar twintig vervolgacties van. Ook wanneer iedere losse actie klein is, kan het totaal te groot worden. Een grens per verzoek is daarom iets anders dan een grens voor de hele uitvoering.

We gebruiken een eindig oefenplan met vooraf vertrouwde bovengrenzen voor tokens en uitvoereenheden. Die getallen zijn geen claims van het model en geen echte factuur of tokenizermeting. Er wordt niets uitgevoerd behalve het tellen van eigen gegevens.

1. Kijk naar het hele werk

Stel je een project voor met een hoofdtaak en twee deeltaken. Kosten stapelen op; nesting vergroot de diepte en meerdere kinderen van dezelfde ouder vergroten de vertakking, ook wel fan-out. Diepte en breedte kunnen dus verschillende problemen veroorzaken.

Iedere planregel heeft een id, een eerdere ouder of None, en twee kostentellers. Een root heeft diepte één. In deze oefening delen alle roots de virtuele ouder None, zodat ook meerdere losse hoofdtaken meetellen bij fan-out.

U is het totale gebruik van één soort eenheid. We tellen de bijdrage u van iedere stap i op; n is het aantal stappen. Dezelfde optelling geldt afzonderlijk voor tokens en output; aantal stappen, maximale diepte en maximale fan-out bewaren we daarnaast als eigen tellers.

U=∑i=1nui

Met de hand kosten de eerste twee stappen twee en drie tokens. Samen is dat vijf, terwijl geen losse stap meer dan drie kost. De tweede stap is kind van de eerste, dus de maximale diepte is twee.

plan = [
    dict(id="a", parent=None,
         tokens=2, output=1),
    dict(id="b", parent="a",
         tokens=3, output=2),
]
used = aggregate_usage(plan)
print(used["tokens"])
print(used["max_depth"])
print(used["max_fanout"])
# 5
# 2
# 1
01
CHECKPOINTTel de hele eindige uitvoering
VERKEN HET ZELFStop vóór de stap die het budget overschrijdt

De limieten gelden voor het hele plan. Een geweigerde stap en alle latere stappen tellen niet mee als verbruik. Exact op de grens mag wel.

Het hele plan zonder grens

Stappen
4
Oefentokens
6
Uitvoereenheden
4
Maximale diepte
3
Maximale fan-out
2

Werkelijk geaccepteerde prefix

Stappen
1
Oefentokens
2
Uitvoereenheden
1
Maximale diepte
1
Maximale fan-out
1

Stap aGeaccepteerd

Bovenliggende stap: None (hoofdtaak)

2 oefentokens · 1 uitvoereenheden

Stap bHier gestopt

Bovenliggende stap: a

3 oefentokens · 0 uitvoereenheden

Stap cNiet bereikt

Bovenliggende stap: a

0 oefentokens · 2 uitvoereenheden

Stap dNiet bereikt

Bovenliggende stap: b

1 oefentokens · 1 uitvoereenheden

Geaccepteerd: a

Gestopt vóór b door tokenlimiet.

Hoofdtaken hebben diepte 1 en delen één virtuele ouder voor fan-out. Ook een gratis stap verbruikt een stap. Kosten zijn vooraf vertrouwde oefenbovengrenzen; geen eigen schatting van een model of echte factuur.

Alle kaarten, rechten, routes en kosten zijn eigen oefengegevens. Deze oefening voert geen externe tools uit en verstuurt geen gegevens.

2. Controleer vóór je accepteert

Een waarschuwing na het werk kan de overschrijding niet terugnemen. Onze begrenzer berekent daarom eerst het voorgenomen gebruik. Alleen als alle grenzen passen, wordt de stap geaccepteerd en worden de tellers bijgewerkt.

Voor één optelteller is U het geaccepteerde gebruik, u de volgende bijdrage en L de limiet. Gelijkheid mag. Voor diepte en fan-out controleer je op dezelfde plek hun eigen voorgenomen waarde, zonder ze met tokenkosten te verwarren.

U+u≤L

Met een tokenlimiet van vier past stap a. Stap b zou het totaal vijf maken en wordt dus vooraf gestopt. Haar kosten komen niet in usage en eventuele latere stappen worden ook niet geaccepteerd.

limits = dict(steps=4, tokens=4,
              output=10, depth=4,
              fanout=4)
answer = bounded_workflow(plan, limits)
print(answer["accepted"])
print(answer["stopped"])
print(answer["reason"])
print(answer["usage"]["tokens"])
# ['a']
# b
# tokens
# 2
02
CHECKPOINTReserveer vóór de volgende stap

Er zijn vijf verschillende grenzen. Als één volgende stap meerdere grenzen overschrijdt, toont onze oefening eerst steps, daarna tokens, output, depth en fanout. Die vaste redenvolgorde maakt een test reproduceerbaar; zij geeft geen rangorde van alle werkelijke beveiligingsrisico’s.

3. Test de grens en de vorm van het werk

Een veelgemaakte fout is al weigeren wanneer het totaal precies gelijk wordt aan de grens. Verhoog de tokenlimiet naar vijf en beide normale stappen passen. De geaccepteerde uitvoering eindigt dan met complete en heeft geen geweigerde stap.

exact = dict(limits, tokens=5)
done = bounded_workflow(plan, exact)
print(done["reason"])
print(done["stopped"])
print(done["usage"]["tokens"])
# complete
# None
# 5

Een tweede fout is alleen betaalde stappen tellen. Ook een vervolgactie met nul oefenkosten verbruikt één stap en kan nieuwe kinderen voorstellen. Daartegen helpen een stapbudget en aparte diepte- en fan-outgrenzen.

free = [dict(id="z", parent=None,
             tokens=0, output=0)]
no_steps = dict(limits, steps=0)
stop = bounded_workflow(free, no_steps)
print(stop["reason"])
print(len(stop["accepted"]))
print(stop["usage"]["tokens"])
# steps
# 0
# 0

Valideer het hele plan, ook de regels na een waarschijnlijke stop. Een toekomstige ouder, een dubbele id of een bool als kostenteller is ongeldige invoer. Die gevallen horen niet stil te verdwijnen omdat de eerste stap toevallig al wordt geweigerd.

De oefenteller onderbreekt geen hangende callback en meet geen werkelijk modelgebruik. In een echt systeem zijn daarnaast van buitenaf afgedwongen uitvoeringstijd, meting en stopcontroles nodig. De bestaande browserworker houdt zijn eigen grenzen; deze les verruimt of vervangt die niet.

Wat je nu kunt

  • Je houdt totaalgebruik, diepte en fan-out apart zichtbaar.
  • Je weigert vóór de overschrijdende stap en telt alleen het geaccepteerde prefix.
  • Je test grensgelijkheid, gratis stappen en behoud van gewoon werk.

Hierna nemen we ook de beschrijvingen en bevoegdheden van MCP-tools onder de loep.

Bronnen

Gecontroleerd op 30 oktober 2026: OWASP 2026 LLM06 over begrensd resourcegebruik en MITRE ATLAS 2026.10, AML.T0034.002 en AML.M0036. Planregels, tellerdefinities en kosten zijn eigen begrensde oefengegevens.

DE VOLGENDE STAPHoud toolbeschrijvingen buiten de vertrouwde opdracht
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 / 2
01
Tel de hele eindige uitvoering

Schrijf aggregate_usage(plan). Plan is exact list/tuple met 0–32 exacte dicts id/parent/tokens/output met exact-str keys. Ids zijn unieke geldige ids; parent is None of een eerdere id. Tokens/output zijn exacte Python-ints 0–10000, geen bool/NumPy-scalar. Het zijn afzonderlijk vertrouwde oefenbovengrenzen, geen modelclaims of werkelijk tokenizergebruik. Een root heeft diepte1. Fan-out telt kinderen per parent, ook alle roots bij de ene virtuele parent None. Een id is exact str volgens ASCII [a-z][a-z0-9-]{0,31}. Usage is een nieuw exact dict steps/tokens/output/max_depth/max_fanout met exacte Python-ints. Leeg geeft vijf nullen. Tel alle gebeurtenissen zonder budgetafwijzing: stappen, somtokens, somoutput, maximale voorouderdiepte en maximale fan-out over ouders. Dit is een eindige tegenfeitelijke oefentrace; voer geen echte herhaling uit. Alle invoer valideren, ook op geweigerde of later niet uitgevoerde takken. Exacte Python-builtins zonder omzetting of normalisatie; ongeldige invoer geeft ValueError. Behoud alle invoer, volgorde, readonlyflags en Python-/NumPy-RNG, ook bij fouten. Alle mutabele uitvoercontainers zijn nieuw, delen geen containers met invoer, elkaar of herhaalde resultaten. De validatiehelpers zijn gegeven. Iedere functie werkt zelfstandig wanneer alle overige leerlingfuncties nog TODO zijn. Geen echte tool, dienst of netwerkactie uitvoeren.

Een hint

Een limiet per losse stap begrenst de totale som niet.

02
Reserveer vóór de volgende stap

Schrijf bounded_workflow(plan,limits). Plan is exact list/tuple met 0–32 exacte dicts id/parent/tokens/output met exact-str keys. Ids zijn unieke geldige ids; parent is None of een eerdere id. Tokens/output zijn exacte Python-ints 0–10000, geen bool/NumPy-scalar. Het zijn afzonderlijk vertrouwde oefenbovengrenzen, geen modelclaims of werkelijk tokenizergebruik. Een root heeft diepte1. Fan-out telt kinderen per parent, ook alle roots bij de ene virtuele parent None. Een id is exact str volgens ASCII [a-z][a-z0-9-]{0,31}. Usage is een nieuw exact dict steps/tokens/output/max_depth/max_fanout met exacte Python-ints. Leeg geeft vijf nullen. Limits is exact dict steps/tokens/output/depth/fanout met exact-str keys en exacte niet-negatieve Python-ints; steps/depth/fanout maximaal32, tokens/output maximaal320000. Controleer VOOR iedere stap de komende totale steps/tokens/output en de diepte en geaccepteerde-kindfanout. Eerste overschrijding heeft prioriteit steps,tokens,output,depth,fanout. Gelijkheid mag. Stop vóór die stap; zij en alle latere stappen zijn niet geaccepteerd of verbruikt. Geef exact dict accepted/stopped/reason/usage: nieuwe geordende id-lijst accepted, eerste geweigerde id of None, reden de limietsleutel of complete, usage alleen voor het geaccepteerde prefix. Kost0 verbruikt één stap. Een leeg plan voltooit ook bij nulgrenzen. Valideer het hele plan ondanks een vroege stop. Alle invoer valideren, ook op geweigerde of later niet uitgevoerde takken. Exacte Python-builtins zonder omzetting of normalisatie; ongeldige invoer geeft ValueError. Behoud alle invoer, volgorde, readonlyflags en Python-/NumPy-RNG, ook bij fouten. Alle mutabele uitvoercontainers zijn nieuw, delen geen containers met invoer, elkaar of herhaalde resultaten. De validatiehelpers zijn gegeven. Iedere functie werkt zelfstandig wanneer alle overige leerlingfuncties nog TODO zijn. Geen echte tool, dienst of netwerkactie uitvoeren.

Een hint

Bereken de voorgenomen nieuwe tellerwaarden voordat je iets accepteert.

Vastgelopen? Bekijk de voorbeeldoplossing
import re
_IDENTIFIER = re.compile('[a-z][a-z0-9-]{0,31}', re.ASCII)

# Jouw functies

def aggregate_usage(plan):
    rows = _plan(plan)
    steps = tokens = output = max_depth = max_fanout = 0
    children = {}
    for identifier, parent, token_count, output_count, depth in rows:
        steps += 1
        tokens += token_count
        output += output_count
        children[parent] = children.get(parent, 0) + 1
        max_depth = max(max_depth, depth)
        max_fanout = max(max_fanout, children[parent])
    return {'steps': steps, 'tokens': tokens, 'output': output, 'max_depth': max_depth, 'max_fanout': max_fanout}

def bounded_workflow(plan, limits):
    rows = _plan(plan)
    bounds = _limits(limits)
    accepted = []
    used = {'steps': 0, 'tokens': 0, 'output': 0, 'max_depth': 0, 'max_fanout': 0}
    children = {}
    stopped = None
    reason = 'complete'
    for identifier, parent, tokens, output, depth in rows:
        fanout = children.get(parent, 0) + 1
        next_values = {'steps': used['steps'] + 1, 'tokens': used['tokens'] + tokens, 'output': used['output'] + output, 'depth': depth, 'fanout': fanout}
        violation = next((key for key in ('steps', 'tokens', 'output', 'depth', 'fanout') if next_values[key] > bounds[key]), None)
        if violation is not None:
            stopped, reason = (identifier, violation)
            break
        accepted.append(identifier)
        children[parent] = fanout
        used = {'steps': next_values['steps'], 'tokens': next_values['tokens'], 'output': next_values['output'], 'max_depth': max(used['max_depth'], depth), 'max_fanout': max(used['max_fanout'], fanout)}
    return {'accepted': accepted, 'stopped': stopped, 'reason': reason, 'usage': used}

# Gegeven validatiehelpers

def _identifier(value):
    if type(value) is not str or _IDENTIFIER.fullmatch(value) is None:
        raise ValueError('Gebruik een exacte ASCII-id')
    return value

def _record(value, keys):
    if type(value) is not dict or any((type(key) is not str for key in value)) or set(value) != set(keys):
        raise ValueError('Gebruik precies de gevraagde velden')
    return value

def _plan(value):
    if type(value) not in (list, tuple) or len(value) > 32:
        raise ValueError('Een plan bevat maximaal 32 oefenstappen')
    depths = {}
    rows = []
    for row in value:
        _record(row, ('id', 'parent', 'tokens', 'output'))
        identifier = _identifier(row['id'])
        parent = row['parent']
        if parent is not None:
            _identifier(parent)
        if identifier in depths or (parent is not None and parent not in depths):
            raise ValueError('Ids zijn uniek en ouders komen eerder')
        for key in ('tokens', 'output'):
            if type(row[key]) is not int or not 0 <= row[key] <= 10000:
                raise ValueError('Oefenkosten zijn exacte begrensde ints')
        depth = 1 if parent is None else depths[parent] + 1
        depths[identifier] = depth
        rows.append((identifier, parent, row['tokens'], row['output'], depth))
    return rows

def _limits(value):
    _record(value, ('steps', 'tokens', 'output', 'depth', 'fanout'))
    for key in value:
        maximum = 320000 if key in ('tokens', 'output') else 32
        if type(value[key]) is not int or not 0 <= value[key] <= maximum:
            raise ValueError('Limieten zijn exacte begrensde ints')
    return value

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