Data Integratie

AI agent tools bouwen zonder vijftien versies bij te houden

Developer werkt aan AI agent tool manifest in TypeScript code editor, meerdere provider-integraties op scherm

Foto: Pixabay via Pexels

Kort antwoord

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?

OpenAI, Anthropic en eigen agent-loops gebruiken elk een ander formaat voor tool-definities. Hetzelfde gereedschap, drie keer opschrijven. Dat klinkt als een klein ongemak, maar het schaalt kwaadaardig: 5 tools keer 3 providers is 15 definities die je allemaal gesynchroniseerd moet houden. Één vergeten veld, en de AI laat stilletjes een verplichte parameter weg. Dat soort fouten zie je niet in je logs.

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

Een unified manifest is één JSON-bestand dat een AI-tool volledig beschrijft: wat hij heet, welke parameters hij accepteert, welke rechten hij nodig heeft en hoe hij zich gedraagt bij fouten. TypeScript-adapters lezen dat bestand en compileren het naar het exacte formaat dat OpenAI of Anthropic verwacht. Je schrijft de tool één keer; de adapter regelt de rest.

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 network of exec. 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

De rekensom is eenvoudig. Met een unified manifest-architectuur heb je N manifests plus M adapters. De adapters schrijf je één keer en test je onafhankelijk van de tools. Nieuwe provider? Eén nieuwe adapter, en alle bestaande tools werken direct. Nieuwe tool? Één manifest, en hij werkt op alle providers.

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?

Een manifest is een gewoon JSON-bestand met vijf verplichte velden. TypeScript-interfaces spiegelen die structuur zodat je bij het schrijven van adapters direct compile-time feedback krijgt als je een veld mist of verkeerd typt. Validatie via AJV vangt fouten af vóór runtime.

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

Een unified manifest werkt goed als je écht meerdere providers ondersteunt of verwacht te gaan ondersteunen. Bouw je voor één provider en blijft dat zo, dan voeg je een abstractielaag toe zonder directe winst. Eerlijk is eerlijk: voor een eenvoudig side-project met twee tools en één provider is dit te veel infrastructuur.

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 genest function-object dat de naam, beschrijving en parameters bevat. Anthropic gebruikt een plat object met input_schema direct 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@8 en 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, filesystem of exec. 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 je none opgeeft, 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-typescript een alternatief. Meer achtergrond over hoe APIs en schema-validatie samenwerken legt de basis uit.