Tool calling en structured outputs: het contract dat standhoudt
24 calls, nul kapotte JSON en twee bruikbare datums. Daarna hetzelfde endpoint met een betere beschrijving — en wat een schema niet oplost.
Op deze pagina
Geef een model een tool voor vluchtzoekopdrachten en vraag het een vlucht van Madrid naar Berlijn te vinden. Dit komt terug:
<tool_call>
{"name": "search_flights",
"arguments": {"from": "Madrid", "to": "Berlin", "date": "3rd October 2026"}}
</tool_call>De JSON is geldig. De toolnaam klopt. Elk verplicht veld is aanwezig. En de call is nutteloos: geen enkele vlucht-API accepteert "Madrid" waar hij een luchthavencode verwacht, of "3rd October 2026" waar hij een datum verwacht.
Die kloof — syntactisch perfect, semantisch onbruikbaar — daar gaat dit hoofdstuk over, en het eerste wat we moeten vaststellen is dat dit geen JSON-probleem is. Over vierentwintig requests met deze tool produceerde het model 24 geldige tool calls en nul kapotte JSON. Het faalde geen enkele keer op het onderdeel dat iedereen debugt.
Het model voert niets uit
Link naar de sectie: Het model voert niets uitVóór de mechanica, de zin die de meeste verwarring voorkomt: een tool call is een verzoek, geen actie.
Het model geeft een gestructureerd bericht terug dat zegt: ik wil graag dat search_flights wordt aangeroepen met deze argumenten. Daarna stopt het. Jouw code ontvangt dat bericht, beslist of het eraan voldoet, roept aan wat het moet aanroepen, en stuurt het resultaat terug als een nieuw bericht. Het model heeft je database nooit aangeraakt, nooit een HTTP-request gedaan, nooit credentials gehad.
Alles over agent-beveiliging in hoofdstuk 30 volgt uit die scheiding, en dat geldt ook voor alles over agent-ontwerp in hoofdstuk 23: het model stelt voor en jouw code beschikt, en de code is waar elke garantie leeft.
Een tool, ontdaan van jargon, bestaat dus uit twee dingen:
Een schema. Een JSON Schema dat een functie beschrijft: de naam, wat die doet, en welke argumenten die aanneemt met hun typen en beperkingen. Dit gaat de prompt in, en het is het enige wat het model ooit ziet.
Een endpoint. Een functie in jouw code die die argumenten aanneemt en iets teruggeeft. Het model ziet die nooit, weet nooit in welke taal die is geschreven, en kan een databasequery niet onderscheiden van een hardcoded string.
Je stuurt de schema’s mee met de request
Link naar de sectie: Je stuurt de schema’s mee met de requestDe tooldefinities gaan de prompt in, geserialiseerd in het formaat waarop het model is getraind. Ze kosten tokens bij elke afzonderlijke call — een feit dat later in dit hoofdstuk met een getal terugkomt.
Het model antwoordt met een call in plaats van tekst
Link naar de sectie: Het model antwoordt met een call in plaats van tekstIn plaats van proza bevat de response een gestructureerd verzoek, en de API rapporteert een finish reason die dat aangeeft. Die reden is belangrijk: daaraan weet je code dat hij een tool moet uitvoeren in plaats van de gebruiker een antwoord te tonen.
Je code voert het uit — of weigert
Link naar de sectie: Je code voert het uit — of weigertDit is de stap waarin geen model zit. Valideer de argumenten tegen het schema, beslis of deze caller dit mag doen, en voer uit.
Je stuurt het resultaat terug als bericht
Link naar de sectie: Je stuurt het resultaat terug als berichtHet resultaat wordt een nieuwe beurt in het gesprek, in een rol die daarvoor is gereserveerd. Het model leest het zoals elke andere context.
Het model antwoordt, of vraagt om een andere tool
Link naar de sectie: Het model antwoordt, of vraagt om een andere toolDat is de loop van hoofdstuk 23, en de reden waarom één request kan uitgroeien tot een dozijn round trips.
Niets hiervan is emergent. Zoals hoofdstuk 11 vaststelde, is tool calling aangeleerd gedrag:1 tijdens post-training zag het model duizenden gesprekken die precies zo waren gevormd. Daarom is het formaat modelspecifiek, daarom verschilt de betrouwbaarheid zo sterk tussen modellen van vergelijkbare grootte, en daarom kan een model een tool aanroepen die het nooit eerder heeft gezien — de vorm is getraind, de specifieke tool komt uit je prompt.
Wat een slecht schema kost, gemeten
Link naar de sectie: Wat een slecht schema kost, gemetenDit is de tool zoals de meeste mensen hem eerst schrijven. Let op: er is niets fout aan; hij is alleen dun:
{
name: "search_flights",
description: "Search for flights.",
parameters: {
type: "object",
properties: {
from: { type: "string", description: "Airport." },
to: { type: "string", description: "Airport." },
date: { type: "string", description: "The date." },
},
required: ["from", "to", "date"],
},
}Vierentwintig requests, zes stadsparen gekruist met vier manieren om een datum uit te drukken ("de 3e van volgende maand", "volgende vrijdag", "15 december", "morgen"), greedy decoding zodat de resultaten reproduceerbaar zijn:
| tool aangeroepen | kapotte JSON | datum in ISO | luchthavens als IATA | alles correct | |
|---|---|---|---|---|---|
| het schema hierboven | 24/24 | 0 | 2/24 | 4/24 | 1/24 |
Lees de eerste twee kolommen vóór de laatste drie. Het model roept elke keer de juiste tool aan en produceert elke keer goed gevormde JSON. De fout zit volledig in de waarden, en die waarden zijn onbruikbaar: "Madrid" in plaats van MAD, "3rd October 2026" in plaats van 2026-10-03.
Dit is het benadrukken waard, omdat het bepaalt waar je kijkt wanneer iets breekt. De reflex is een JSON-parser met retry toe te voegen, of het model dringender om geldige JSON te vragen. Geen van beide pakt iets aan dat hier is gebeurd.
Verander nu alleen de beschrijving
Link naar de sectie: Verander nu alleen de beschrijvingHetzelfde endpoint. Dezelfde code erachter. Hetzelfde model, dezelfde prompts, dezelfde decoding. Het enige dat verandert is de tekst in het schema:
{
name: "search_flights",
description: "Search scheduled flights between two airports on a given day.",
parameters: {
type: "object",
properties: {
from: {
type: "string",
description: "Departure airport as a three-letter IATA code, e.g. MAD for Madrid. Never a city name.",
pattern: "^[A-Z]{3}$",
},
to: { /* same */ },
date: {
type: "string",
description: "Departure date as an ISO 8601 calendar date, YYYY-MM-DD. Resolve relative dates against today before calling.",
format: "date",
pattern: "^\\d{4}-\\d{2}-\\d{2}$",
},
},
required: ["from", "to", "date"],
},
}| datum-FORMAAT | datum-WAARDE | luchthaven-FORMAAT | luchthaven-WAARDE | |
|---|---|---|---|---|
| dun schema | 2/24 | 1/24 | 4/24 | 4/24 |
| beschreven schema | 24/24 | 12/24 | 16/24 | 8/24 |
Het datumformaat gaat van 2 van de 24 naar 24 van de 24. Perfect, door een tekstwijziging, zonder code aan te raken en zonder retry-logica. Als je één operationele gewoonte uit dit hoofdstuk meeneemt, is het deze: wanneer een tool verkeerd wordt aangeroepen, zit de fix bijna altijd in de beschrijving, en dat is de goedkoopste fix in het systeem.
Lees nu de tweede kolom, want dat is de belangrijkere helft.
Een schema beperkt de vorm. Het kan geen kennis leveren.
Link naar de sectie: Een schema beperkt de vorm. Het kan geen kennis leveren.De datum staat 24 van de 24 keer in ISO-formaat. Het is 12 van de 24 keer de juiste dag.
Dus de helft van de calls bevat nu een perfect geformatteerde datum die de verkeerde datum is. De beschrijving vertelde het model welke vorm het moest produceren, en het model produceerde die foutloos — maar "volgende vrijdag" omzetten naar 2026-09-11 vereist kennis van de datum van vandaag en kalenderrekenwerk, en geen enkele hoeveelheid beschrijving levert dat. Hetzelfde geldt voor luchthavens: formaat ging van 4 naar 16, maar waarde slechts van 4 naar 8, omdat MAD schrijven vereist dat je weet dat de luchthaven van Madrid MAD is.
Dat onderscheid is het dragende idee van dit hoofdstuk:
Een schema is een contract over vorm. Het kan de output van het model parsebaar, getypt en consistent maken. Het kan die output niet waar maken, en elke failure mode die een goed schema overleeft, is een kennisfout, geen formaatfout.
Die twee vragen om verschillende fixes, en ze verwarren kost weken. Formaatfouten los je op in de beschrijving of met constrained decoding, hieronder. Kennisfouten los je op door de kennis in de prompt te zetten — de huidige datum in het systeembericht, een luchthaven-lookup als een tweede tool die het model eerst aanroept, een enum in het schema wanneer de set klein genoeg is om op te sommen. Let op wat alle drie gemeen hebben: ze verplaatsen het probleem uit het geheugen van het model naar zijn input, en dat is de kern van hoofdstuk 24.
Structured outputs, en wat "constrained decoding" echt is
Link naar de sectie: Structured outputs, en wat "constrained decoding" echt isAlles hierboven vertrouwt er nog steeds op dat het model kiest om de juiste vorm te produceren. Er is een sterkere garantie beschikbaar, en dat is de beste opbrengst van hoofdstuk 17.
Denk terug aan hoe generatie werkt: bij elke stap produceert het model een logit voor elke token in de vocabulaire, en de sampler kiest er één. Constrained decoding voegt daar een stap tussen. Gegeven een grammar — afgeleid van je JSON Schema — berekent het welke tokens legaal als volgende kunnen komen, zet de logits van alle andere tokens op min oneindig, en laat de sampler kiezen uit wat overblijft.
Als het schema zegt dat het volgende een { moet zijn, dan heeft elke token die niet { is kans nul. Niet "onwaarschijnlijk": nul. Het model kan geen ongeldige JSON uitstoten omdat de ongeldige tokens vóór sampling uit de distributie zijn verwijderd.
Dat is wat er onder "structured outputs", "JSON mode" en "guided generation" zit, en het verklaart hun twee eigenschappen. De garantie is totaal voor alles wat de grammar kan uitdrukken — typen, verplichte velden, enums, nesting — omdat die mechanisch wordt afgedwongen in plaats van beleefd gevraagd. En het zegt niets over inhoud: een grammar kan afdwingen dat "date" een string is die op een datumpatroon past, maar kan niet afdwingen dat het de juiste dag is. Dat is dezelfde muur als in de vorige sectie, alleen vanaf de andere kant bereikt.
Twee praktische opmerkingen. Het is niet gratis: het mask moet bij elke stap worden berekend, en complexe grammars kosten meetbare latency. En het verandert wat het model doet — een model dat wordt weggestuurd van zijn voorkeurs-token kan slechtere inhoud produceren terwijl het perfecte structuur levert. Daarom is "vriendelijk vragen en valideren" nog steeds een redelijk default voor simpele vormen, en verdient constrained decoding zijn kosten wanneer de vorm complex is of de consumer strikt is.
Side effects, en de ene eigenschap die ertoe doet
Link naar de sectie: Side effects, en de ene eigenschap die ertoe doetHoofdstuk 14 mat een timeout gevolgd door een retry die twee generaties factureerde voor één antwoord. Met tools wordt dezelfde fout erger, omdat een tool iets kan doen.
Als je code charge_card aanroept, een timeout krijgt en opnieuw probeert, heb je twee kostenposten. Het model heeft geen idee dat dit is gebeurd; het ziet één toolresultaat. De fix is dezelfde als in elk distributed system en is niet het probleem van het model: maak de operatie idempotent door de call een key te geven, zodat de tweede uitvoering de eerste herkent en het resultaat daarvan teruggeeft in plaats van het werk opnieuw te doen.
De ontwerpregel die hieruit volgt, is het waard om helder te formuleren. Scheid reads van writes in je toolcatalogus. Een read kan vrij worden herhaald, parallel draaien en gecachet worden. Een write kan dat niet, en moet een key, een toestemmingscheck en — voor alles waar een gebruiker vóór uitvoering van zou willen weten — een goedkeuringsstap hebben die een mens tussen het verzoek en de actie zet. Die goedkeuringsstap is geen beleefdheid: het is een van de weinige dingen tussen een prompt injection en een echte consequentie — en, zoals hoofdstuk 30 meet, de zwakste daarvan.
Hoeveel tools voordat het degradeert?
Link naar de sectie: Hoeveel tools voordat het degradeert?De folklore zegt dat veel tools laden het model slechter laat kiezen. Het is beter om dat te meten dan te herhalen, dus: dezelfde vierentwintig requests, met de vluchttool plus een groeiende set andere tools — waaronder drie bewust verwarrende tools (treintijden, veerverbindingen, busroutes).
| geladen tools | prompt tokens | koos search_flights | datum in ISO |
|---|---|---|---|
| 1 | 353 | 24/24 | 24/24 |
| 5 | 730 | 24/24 | 24/24 |
| 10 | 1.193 | 21/24 | 21/24 |
| 20 | 2.119 | 24/24 | 24/24 |
De selectie degradeerde niet. Met twintig tools, waarvan drie plausibel verwarrend, koos een model met een half miljard parameters vierentwintig van de vierentwintig keer de juiste tool. De dip bij tien zijn drie calls die een andere tool noemden, en die overleeft de stap naar twintig niet.
Dat is een negatief resultaat en moet ook zo worden gerapporteerd: bij deze taak, met deze tools, was "te veel tools" niet het probleem. Wat wel monotoon groeide, met een factor zes, was de prompt: 353 tokens naar 2.119, betaald bij elke request in het gesprek, voor altijd, of er nu een tool wordt gebruikt of niet.
De eerlijke versie van de folklore gaat dus over kosten en context, niet accuratesse. Twintig tools is een permanente belasting op elk bericht, en hoofdstuk 16 liet al zien wat een permanente prefix doet met een rekening over veertig beurten. Wanneer mensen melden dat veel tools de kwaliteit schaden, is het mechanisme meestal dat de definities de context verdrongen die ertoe deed — een probleem uit hoofdstuk 24 in een kostuum van hoofdstuk 18. Tools die echt bijna-duplicaten van elkaar zijn, vormen ook een reëel probleem, en de fix daarvoor is niet minder tools maar betere beschrijvingen en namespaces: prefix ze per systeem (crm.search_customer, billing.search_customer), zodat twee catalogi die door twee teams zijn samengevoegd niet botsen, en zodat het model iets heeft om op te discrimineren.
Drie soorten tools, en degene die het volgende deel opent
Link naar de sectie: Drie soorten tools, en degene die het volgende deel opentHet helpt om tools te ordenen op wat ze met de wereld doen, omdat de engineering per soort verschilt.
Datatools lezen: zoeken, ophalen, querieën. Retrybaar, paralleliseerbaar, cachebaar. Ze falen door niets nuttigs terug te geven, en hun grootste risico is dat ze onvertrouwde tekst de context binnenbrengen — het volledige aanvalsvlak van hoofdstuk 30.
Actietools schrijven: verzenden, aanmaken, afrekenen, verwijderen. Niet retrybaar zonder key, niet veilig paralleliseerbaar, en de reden dat goedkeuringsflows bestaan.
Orchestration tools roepen andere modellen aan. Een tool waarvan de implementatie een andere agent is, met zijn eigen prompt, zijn eigen tools en zijn eigen loop — en voor het aanroepende model ziet die er precies zo uit als de andere twee, omdat een schema en een endpoint alles is wat het ooit ziet.
Die derde soort is geen curiositeit. Het is het mechanisme achter de agent-as-a-tool-helft van hoofdstuk 25 — de andere topologie, de handoff, geeft het gesprek weg en krijgt het nooit terug — en het werkt precies omdat de interface in dit hoofdstuk smal genoeg is om er een hele agent achter te laten passen.
Waar dit naartoe gaat
Link naar de sectie: Waar dit naartoe gaatJe hebt nu een model dat om dingen kan vragen, en een contract dat dat vragen parsebaar maakt. Wat je niet hebt, is iets waarover het kan vragen buiten wat in zijn prompt past.
De meest voorkomende tool in productie, met ruime afstand, is een zoekopdracht over een tekstcorpus dat het model tijdens training nooit heeft gezien: je documentatie, je tickets, je contracten. Dat klinkt als een opgelost probleem — embed het, vind de nearest neighbours, plak ze erin — en de delen die niet opgelost zijn, zijn precies de delen die bepalen of het antwoord te vertrouwen is: hoe de tekst wordt opgeknipt voordat hij wordt embedded, welke similarity threshold laag genoeg is om ik weet het niet te betekenen, en hoe een citaat aan een claim wordt gekoppeld zodat een lezer het kan controleren.
Hoofdstuk 19 is retrieval, en het is het hoofdstuk waarin een fout antwoord ophoudt een curiositeit te zijn en een aansprakelijkheid wordt.
Bronnen en methode
Link naar de sectie: Bronnen en methodeDe metingen in dit hoofdstuk komen uit Qwen/Qwen2.5-0.5B-Instruct met greedy decoding, over 24 gegenereerde requests waarin zes stadsparen werden gekruist met vier datumformuleringen, met de eigen chat-template van het model voor tooldefinities. Ze reproduceren exact, en het is een klein model: lees de splitsing tussen formaat en waarde als demonstratie van het mechanisme, niet als benchmark van wat huidige modellen doen. Een frontier model lost "volgende vrijdag" veel vaker correct op — en kan daar nog steeds niet door een schema toe worden gedwongen, en dat is het deel dat generaliseert.
De JSON Schema-vocabulaire die hierboven wordt gebruikt (type, properties, required, pattern, format, enum) is gespecificeerd in de JSON Schema-draft die de documentatie van je provider noemt; de nuttige subset is klein en hetzelfde bij providers, en de verschillen die er zijn — welke keywords door constrained decoding worden afgedwongen in plaats van alleen aan het model te worden doorgegeven — kun je beter in de structured-output-gids van de provider lezen dan aannemen.
Voor constrained decoding als techniek documenteren guidance-achtige libraries en het outlines-project de constructie van grammar naar logit-mask op een manier die direct aansluit op de sampler uit hoofdstuk 17. En voor de round trip zelf is de duidelijkste specificatie geen tutorial maar een protocol: hoofdstuk 26 leest het regel voor regel.
Referenties
Link naar de sectie: Referenties-
Ouyang, L. et al. Training language models to follow instructions with human feedback. arXiv:2203.02155 (2022). Het paper dat het post-training-recept standaard maakte; de vorm van een tool call wordt daar geleerd, uit demonstraties, precies zoals de vorm van een antwoord. ↩