AI agent tools bouwen zonder vijftien versies bij te houden
Foto: Pixabay via Pexels
Als je AI agent tools bouwen wil voor meerdere providers (OpenAI, Claude, eigen agent-loop), onderhoud je nu voor elke tool een aparte definitie per provider. Met een unified JSON Schema manifest schrijf je een tool één keer en compileer je die automatisch naar elk provider-formaat. Dat scheelt bij 5 tools en 3 providers al 10 extra definities die je anders handmatig gesynchroniseerd moet houden.
Waarom heb je straks vijftien tool-definities zonder dat je het wilt?
Stel je bouwt een web_search-tool. Bij OpenAI verpak je die in een tools-array met een geneste function-sleutel. Bij Anthropic wil je een plat object met input_schema aan de top. Klinkt als een kleine aanpassing. Het is er een, maar je moet hem voor elke tool apart bijhouden.
Het echte probleem is drift. Je past een parameter aan in je OpenAI-definitie en vergeet de Anthropic-versie. De tool werkt nog, maar de AI dwingt de parameter niet meer af. De tool call komt binnen zonder dat verplichte veld, en je applicatie crasht op een plek die niks met de wijziging te maken leek te hebben.
Ik zou dit niet accepteren voor reguliere code. Je schrijft een functie ook niet twee keer. Voor AI-begrippen en agent-architecturen geldt hetzelfde principe: één bron van waarheid, de rest is compilatie.
Wat is een unified manifest en hoe werkt het? Één manifest, N adapters
Het manifest gebruikt JSON Schema draft 2020-12 als vocabulaire voor de parameter-beschrijvingen. JSON Schema is een open standaard voor het beschrijven van datastructuren. Handig, want elke JSON Schema-validator werkt direct op je manifests, zonder custom code.
Een manifest heeft vijf onderdelen:
- toolId + metadata: een stabiele identifier, versienummer en beschrijving die naar de provider gaat.
- inputSchema: de parameters van de tool, beschreven als gewone JSON Schema-objecten.
- outputSchema: de verwachte returnwaarde. Providers gebruiken dit nu nog niet, maar je eigen agent-loop kan ermee valideren.
- permissions: welke systeemrechten de tool claimt, zoals
networkofexec. Een sandbox-runtime kan een tool weigeren als die meer vraagt dan toegestaan. - executionBoundary: timeouts, retry-beleid en concurrency-limieten. Dit is voor je eigen orchestrator, niet voor de provider-API.
De adapter doet daarna het simpele maar foutgevoelige werk: hij pakt de toolId als functienaam voor OpenAI, wikkelt de inputSchema in de juiste nesting, en laat de rest weg. Voor Anthropic doet hij hetzelfde met een andere wrapper. Structureel zijn de inner schemas bijna identiek; alleen de buitenste laag verschilt.
Let op: default-waarden in JSON Schema zijn documentatie, geen gedrag. OpenAI noch Anthropic passen die toe. Je implementeert defaults in je eigen aanroep-laag, niet in het manifest.
5 tools × 3 providers = 15 definities: de wiskunde die je wil vermijden
Vergelijk het met een tolk bij een vergadering. Zonder tolk praat elke deelnemer een andere taal en heb je voor elk gesprekspaar een aparte vertaling nodig. Met een tolk vertaal je alles via één gemeenschappelijke taal. De tolk is de adapter; het manifest is die gemeenschappelijke taal.
De wiskunde is het sterkste argument. Stel je voegt een vierde provider toe aan een systeem met 10 tools:
- Zonder manifest: 10 nieuwe definities schrijven, elke bestaande tool aanpassen.
- Met manifest: 1 nieuwe adapter schrijven, klaar.
Adapters zijn bovendien onafhankelijk testbaar. Je kunt een unit-test schrijven die controleert of de OpenAI-adapter altijd een strict-veld meeneemt, of dat de Anthropic-adapter nooit een type: "function"-wrapper toevoegt. Die tests slagen of zakken ongeacht welke tools je later toevoegt.
Voor developers die werken met multi-agent setups, zie ook DeerFlow als open-source voorbeeld van zo'n multi-agent architectuur. Zo houd je het beheerbaar naarmate het aantal tools groeit.
Hoe ziet een concreet manifest eruit in TypeScript?
Hier is een minimaal manifest voor een zoektool:
{
"toolId": "web_search",
"version": "1.0.0",
"name": "Webzoekopdracht",
"description": "Zoekt actuele informatie op het web.",
"inputSchema": {
"type": "object",
"properties": {
"query": { "type": "string", "description": "Zoekterm" },
"max_results": { "type": "integer", "description": "Aantal resultaten", "default": 5 }
},
"required": ["query"]
},
"permissions": ["network"],
"executionBoundary": {
"timeout": 5000,
"retryPolicy": { "maxRetries": 2, "backoffMs": 500 }
}
}De TypeScript-adapter voor OpenAI pakt dit manifest en geeft terug:
{
type: "function",
function: {
name: manifest.toolId,
description: manifest.description,
parameters: manifest.inputSchema,
strict: false
}
}De Anthropic-adapter geeft:
{
name: manifest.toolId,
description: manifest.description,
input_schema: manifest.inputSchema
}Twaalf regels verschil. Mechanisch. En toch is het iets wat je handmatig fout gaat zodra je tien tools hebt en onder tijdsdruk werkt. Geautomatiseerd gaat het altijd goed.
De toolId volgt een patroon van kleine letters, cijfers, underscores en punten. Max 64 tekens, want sommige providers knippen namen af. Dat soort grenzen staan in het schema zelf, zodat validatie ze al afvangt voordat je ook maar één API-call doet.
Wat lost dit niet op, en wanneer is het overkill? Niet altijd nodig
De benadering heeft ook echte beperkingen die je moet kennen.
Provider-specifieke functies verdwijnen. OpenAI's strict-modus, waarbij alle properties verplicht zijn en additionalProperties false moet zijn, past niet in een generiek manifest. Je kunt het als extensie-veld toevoegen, maar dan verlies je de vendor-neutraliteit deels weer.
Output-validatie werkt alleen in je eigen loop. Noch OpenAI noch Anthropic valideert tool-output tegen je outputSchema. Je moet dat zelf implementeren in de orchestrator die de tool-resultaten verwerkt voordat ze terug naar het model gaan.
Defaults zijn documentatie, geen gedrag. Als je "default": 5 in je schema zet, doet geen enkele provider daar iets mee. Je implementeert defaults in je eigen aanroep-code.
Dit patroon is het meest waardevol voor teams die nu al twee providers gebruiken, of die een tool-bibliotheek bouwen die anderen gaan gebruiken. Voor een solo-project met één AI-provider: begin gewoon met het native formaat en refactor als je een tweede provider toevoegt. Vroeg abstraheren verspilt tijd. Vergelijk het met migreren tussen LLM-modellen: je doet het als de noodzaak er is, niet als voorzorgsmaatregel voor een probleem dat misschien nooit komt.
Conclusie
Een unified manifest vervangt N×M aparte tool-definities door N manifests plus M adapters.
Structuurverschillen tussen OpenAI en Anthropic zijn mechanisch oplosbaar via een adapter-laag.
JSON Schema als basis maakt validatie, documentatie en code-generatie direct bruikbaar zonder extra tooling.
Veelgestelde vragen
-
OpenAI verwacht een
tools-array met een genestfunction-object dat de naam, beschrijving en parameters bevat. Anthropic gebruikt een plat object metinput_schemadirect op het top-niveau. De inner parameter-schema's zijn bijna identiek; alleen de buitenste wrapper verschilt. Meer details over de exacte structuur vind je in de OpenAI function calling documentatie. -
Nee.
default-waarden in JSON Schema zijn puur documentatie. Noch OpenAI noch Anthropic past ze toe op inkomende tool-calls. Je implementeert defaults in je eigen code, in de laag die de tool aanroept voordat het resultaat terug naar het model gaat. -
Niet per se. Een manifest-architectuur levert de meeste waarde als je twee of meer providers ondersteunt, of een tool-bibliotheek bouwt die anderen gaan gebruiken. Voor een enkel project met één provider voeg je een abstractielaag toe zonder directe winst. Begin met het native formaat en refactor als je een tweede provider toevoegt. Zie ook hoe je LLM-modellen migreert zonder gedoe voor een vergelijkbare afweging.
-
AJV (Another JSON Validator) is de meest gebruikte keuze voor Node.js. Versie 8 ondersteunt JSON Schema draft 2020-12. Installeer via
npm install ajv@8en compileer je schema eenmalig bij het opstarten van je applicatie. AJV is snel en breed ondersteund. De officiële documentatie staat op ajv.js.org. -
Permissions zijn een enum-gebaseerde lijst van rechten die een tool claimt nodig te hebben, zoals
network,filesystemofexec. Provider-API's negeren dit veld volledig. Het is bedoeld voor je eigen sandbox-runtime of orchestrator, die een tool kan weigeren te laden als die meer rechten vraagt dan de omgeving toestaat. Als jenoneopgeeft, mag dat de enige waarde zijn. -
De aanpak in dit artikel is handmatig: je schrijft TypeScript-interfaces die de JSON Schema-structuur spiegelen. Dat is minder elegant dan automatisch genereren, maar het vermijdt een build-dependency op een code-generator. Als je team groter is of het schema vaak verandert, is automatisch genereren via tools zoals
json-schema-to-typescripteen alternatief. Meer achtergrond over hoe APIs en schema-validatie samenwerken legt de basis uit.