Conveya
Engineering24 maart 202611 min

Typed tool-calls: waarom onze agents niet fantaseren

Onder de motorkap van onze actie-laag: input-schema's, output-validatie en een handvol harde regels.

Ruben Vaalt
Conveya team

Als je in 2024 of 2025 met een AI-agent hebt gebouwd, ken je het probleem: de bot besluit ineens een tool-aanroep te doen die niet bestaat, of pakt een veld dat hij verzint, of geeft als ID een random getal. De fix die iedereen toepast is reinforcement: 'gebruik alleen deze 5 tools', 'ID's zijn altijd integers', 'als je het niet zeker weet, vraag de gebruiker'. Werkt soms. Faalt soms. En je weet nooit van tevoren welke.

Bij Conveya hebben we ervoor gekozen om die ruimte voor fouten weg te ontwerpen in plaats van proberen te prompten. De agent ziet alleen tool-calls die syntactisch geldig zijn, en kan alleen output produceren die door een schema gevalideerd is. Hieronder hoe dat werkt.

Stap 1: typed tool definitions

Iedere tool heeft een JSON-schema dat de input definieert, geverifieerd op compile-time. We genereren uit TypeScript-types, geen handgeschreven schemas die uit sync raken met de implementatie.

defineTool({
  name: "shopify_get_order",
  description: "Look up a Shopify order by id, email, or phone.",
  input: z.object({
    orderId: z.string().regex(/^\d+$/).optional(),
    email:   z.string().email().optional(),
    phone:   z.string().regex(/^\+?[0-9 ]+$/).optional(),
  }).refine((v) => v.orderId || v.email || v.phone, "one identifier required"),
  output: ShopifyOrder, // exhaustive type from the Shopify SDK
  exec:   async (input, ctx) => { ... },
});

Dit schema wordt naar de LLM gestuurd als JSON-schema in de native tool-use API (Anthropic's tool_use, OpenAI's function_call). De LLM krijgt dus niet 'er is een tool die orders ophaalt, bedenk maar wat je nodig hebt', maar 'er is een tool met deze exacte signature, je MOET één van deze drie velden invullen'. Zod's runtime parser vangt de rest.

Stap 2: validatie vóór executie

Tussen het moment dat de LLM zegt 'roep deze tool aan met deze args' en het moment dat onze code de tool draait, zit een validatie-laag. Drie checks:

  1. Schema-validatie: voldoet de input aan het Zod-schema? Zo niet → terugkoppeling naar de LLM met de validatie-fout, geen execution.
  2. Authorization-check: heeft deze agent permissie voor deze tool? Per agent kun je tools toestaan of blokkeren; ook write-tools krijgen alleen permissie als de merchant ze expliciet heeft geactiveerd.
  3. Rate-budget check: hoeveel tool-calls heeft deze sessie al gedaan? Voorkomt looping waarbij de agent eindeloos doorgaat met opzoeken zonder klantbenadering.

Als één van de drie faalt, krijgt de LLM een gestructureerde error terug. Bij schema-fouten met de exacte validatie-message ('expected integer, received string for field orderId'). De LLM corrigeert in 95% van de gevallen vanzelf bij de volgende turn. De resterende 5%, meestal door een hallucinated tool die niet bestaat, wordt geblokkeerd.

Stap 3: output validatie

Als de tool zelf data teruggeeft (een Shopify-order, een HubSpot-deal), valideren we óók dat output. Dit lijkt overkill, de Shopify SDK is toch typed, maar het vangt drie problemen:

  • Externe API's schenden hun eigen schema. We hebben echt eens een 'price' veld als string i.p.v. number gehad van een 'stabiele' API.
  • Nieuwe velden die de LLM in de war brengen. We strippen velden die niet in onze output-schema staan, zodat de agent een gefocuste payload krijgt.
  • Encoding-issues (smart quotes, emoji-broken UTF-8). We normaliseren naar UTF-8 NFC voordat het in de context belandt.

Stap 4: tool-results als typed context

Hoe de LLM het result terugkrijgt is ook getyped. Niet als loose JSON-string die hij maar moet ontleden, maar als structured tool_result block waar de schema-keys gegarandeerd zijn. Dit voorkomt dat de LLM in latere turns velden hallucineert die niet in het result staan.

Wat dit oplevert

We hebben dit gemeten in onze interne benchmarks (3.000 echte support-conversaties van vier merchants):

  • Hallucinated tool-calls: van 4.2% van turns naar 0.1%. De resterende 0.1% zijn edge cases (LLM bedenkt een tool naam die toevallig lijkt op een bestaande).
  • Geblokkeerde calls: ~12% van alle tool-aanroepen wordt door validatie tegengehouden. Daarvan corrigeert de LLM 95% bij de volgende turn.
  • Tijd tot eerste tool-call: 180ms gemiddeld (gemeten in apps/web/src/lib/integrations/integration-tools.ts). Schema-validatie zelf is sub-millisecond.
  • Customer-impact incidenten door verkeerde tool-call: 0 in 6 maanden productie.

Wat dit NIET oplost

Typed tool-calls voorkomen dat de agent verzonnen acties uitvoert. Het voorkomt niet dat hij verzonnen ANTWOORDEN geeft op basis van correct opgehaalde data. Daar hebben we een aparte laag voor, output grounding, die ik in een volgende post behandel.

Klaar om te beginnen?

Bouw je eerste AI-agent.

Vandaag opzetten, vanmiddag live. Met je eigen content en je eigen tools, zonder dev.

  • Gratis starten
  • Geen creditcard nodig
  • Live in een middag
Nova
Live · jouw agent
Online
Hoi! Waar blijft mijn bestelling?
Verzonden gisteravond, morgen 13-15 uur in Utrecht.
Bestelling opgezocht in Shopify·in 0,4 s
    Typed tool-calls: waarom onze agents niet fantaseren | Conveya