Als je ooit hebt geprobeerd om een chatbot iets nuttigs te laten doen — een database doorzoeken, een API aanroepen, of zelfs maar het weer ophalen — dan weet je dat pure tekstgeneratie niet genoeg is. Daar komt function calling om de hoek kijken. In deze handleiding laat ik je stap voor stap zien hoe je het implementeert met de drie grootste LLM-providers: OpenAI, Anthropic Claude en Google Gemini.
Wat is Function Calling (Tool Use)?
Function calling — ook wel tool use of tool calling genoemd — is het mechanisme waarmee een large language model externe functies, API's en tools kan aanroepen. In plaats van alleen tekst te genereren, besluit het model dat een bepaalde actie nodig is en geeft het gestructureerde argumenten terug waarmee jouw code de functie daadwerkelijk uitvoert.
Dit klinkt misschien simpel, maar het is eerlijk gezegd een gigantische stap vooruit. Zonder function calling kan een LLM alleen maar tekst produceren. Mét function calling kan het databases doorzoeken, e-mails versturen, betalingen verwerken en vrijwel elke operatie uitvoeren die je definieert. Het is precies dit mechanisme dat AI-chatbots transformeert tot echte AI-agents.
In 2026 ondersteunen alle grote LLM-providers native tool calling: OpenAI (GPT-4o, GPT-5), Anthropic (Claude Opus 4, Claude Sonnet 4), Google (Gemini 2.5 Pro) en zelfs lokale modellen zoals Qwen3 en DeepSeek-V3. Het JSON Schema-formaat voor tooldefinities is grotendeels gestandaardiseerd — de responsformaten verschillen helaas nog per provider.
Hoe werkt Function Calling? De Tool Loop
Het basispatroon is bij alle providers hetzelfde en werkt als een zogenaamde tool loop:
- Definieer tools — beschrijf functienamen, parameters en wat ze doen als JSON Schema
- Stuur bericht met tools — voeg tooldefinities toe aan de API-aanroep samen met het gebruikersbericht
- Detecteer tool calls — controleer of het model een functie wil aanroepen
- Voer functies uit — draai je eigen code met de argumenten van het model
- Stuur resultaten terug — geef de output van je functie terug aan het model
- Ontvang eindantwoord — het model verwerkt alles en antwoordt de gebruiker
Even voor de duidelijkheid: het LLM voert nooit zelf functies uit. Het produceert een gestructureerd JSON-verzoek met de tool-naam en argumenten. Jouw applicatiecode verzorgt de daadwerkelijke uitvoering, validatie en foutafhandeling. Het model "denkt" dus alleen maar na over welke tool het wil gebruiken — de rest is aan jou.
Implementatie met OpenAI (GPT-4o / GPT-5)
OpenAI was de eerste grote provider die function calling introduceerde, terug in juni 2023 (voelt alweer een eeuwigheid geleden). Het huidige formaat maakt gebruik van een tools-parameter met type "function". Laten we meteen in de code duiken.
Stap 1: Tools definiëren
import openai
import json
client = openai.OpenAI()
# Definieer je tools als JSON Schema
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Haal het huidige weer op voor een opgegeven stad",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "De naam van de stad, bijv. Amsterdam"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "Temperatuureenheid"
}
},
"required": ["city"]
}
}
}
]
# Je echte functie-implementatie
def get_weather(city: str, unit: str = "celsius") -> dict:
# In productie: roep een weer-API aan
return {"city": city, "temperature": 18, "unit": unit, "condition": "bewolkt"}
Merk op dat de description-velden erg belangrijk zijn. Het model gebruikt die tekst om te bepalen wanneer het een tool moet aanroepen, dus wees zo specifiek mogelijk.
Stap 2: De Tool Loop implementeren
def run_conversation(user_message: str) -> str:
messages = [{"role": "user", "content": user_message}]
# Eerste API-aanroep met tools
response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
tools=tools,
tool_choice="auto" # Model beslist zelf of het tools gebruikt
)
message = response.choices[0].message
# Tool loop: blijf draaien zolang het model tools wil aanroepen
while message.tool_calls:
# Voeg het assistentbericht toe aan de conversatie
messages.append(message)
# Verwerk elke tool call
for tool_call in message.tool_calls:
func_name = tool_call.function.name
func_args = json.loads(tool_call.function.arguments)
# Voer de functie uit
if func_name == "get_weather":
result = get_weather(**func_args)
else:
result = {"error": f"Onbekende functie: {func_name}"}
# Stuur het resultaat terug
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": json.dumps(result)
})
# Volgende API-aanroep met de resultaten
response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
tools=tools,
tool_choice="auto"
)
message = response.choices[0].message
return message.content
# Test
print(run_conversation("Wat is het weer in Amsterdam?"))
De while message.tool_calls-loop is het hart van de hele implementatie. Het model kan namelijk meerdere rondes van tool calls doen voordat het een definitief antwoord geeft. In de praktijk zijn twee tot drie rondes het meest voorkomend.
Tool Choice opties bij OpenAI
OpenAI biedt drie opties voor tool_choice:
"auto"— het model beslist zelf of een tool nodig is (standaard en meestal wat je wilt)"required"— het model moet minstens één tool aanroepen"none"— tools zijn uitgeschakeld, alleen tekst
Implementatie met Anthropic Claude
Claude gebruikt een vergelijkbare maar net iets andere aanpak. Het belangrijkste verschil zit in het responsformaat: Claude retourneert tool calls als content-blokken van het type tool_use, en resultaten stuur je terug als tool_result-blokken. Klinkt verwarrend? Het valt mee als je de code ziet.
import anthropic
import json
client = anthropic.Anthropic()
# Tools in Claude-formaat (vergelijkbaar, maar net anders)
tools = [
{
"name": "get_weather",
"description": "Haal het huidige weer op voor een opgegeven stad",
"input_schema": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "De naam van de stad, bijv. Amsterdam"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "Temperatuureenheid"
}
},
"required": ["city"]
}
}
]
def run_claude_conversation(user_message: str) -> str:
messages = [{"role": "user", "content": user_message}]
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
tools=tools,
messages=messages
)
# Tool loop voor Claude
while response.stop_reason == "tool_use":
# Verzamel alle tool_use blokken
tool_uses = [b for b in response.content if b.type == "tool_use"]
tool_results = []
for tool_use in tool_uses:
if tool_use.name == "get_weather":
result = get_weather(**tool_use.input)
else:
result = {"error": f"Onbekende tool: {tool_use.name}"}
tool_results.append({
"type": "tool_result",
"tool_use_id": tool_use.id,
"content": json.dumps(result)
})
# Stuur assistant-bericht + tool resultaten terug
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": tool_results})
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
tools=tools,
messages=messages
)
# Haal het tekst-antwoord op
text_blocks = [b.text for b in response.content if hasattr(b, "text")]
return "\n".join(text_blocks)
print(run_claude_conversation("Wat is het weer in Amsterdam?"))
Zie je het verschil? Bij OpenAI check je message.tool_calls, bij Claude kijk je naar response.stop_reason == "tool_use". Een subtiel maar belangrijk detail.
Tool Choice opties bij Claude
Claude heeft drie tool_choice-opties:
{"type": "auto"}— model beslist zelf (standaard){"type": "any"}— model moet een tool aanroepen{"type": "tool", "name": "get_weather"}— forceer een specifieke tool
Eén belangrijk verschil: Claude heeft geen "none"-optie. Wil je tools uitschakelen? Laat dan simpelweg de tools-parameter weg uit je aanroep.
Implementatie met Google Gemini
Google hanteert de term function declarations en heeft een eigen formaat dat behoorlijk afwijkt van OpenAI en Anthropic. Nieuw in 2026: Gemini 2.5 Pro ondersteunt ook multimodale inhoud in functionResponse-berichten, wat best handig is als je met afbeeldingen of video werkt.
from google import genai
from google.genai import types
import json
client = genai.Client()
# Definieer tools in Gemini-formaat
weather_tool = types.Tool(
function_declarations=[
types.FunctionDeclaration(
name="get_weather",
description="Haal het huidige weer op voor een opgegeven stad",
parameters=types.Schema(
type=types.Type.OBJECT,
properties={
"city": types.Schema(
type=types.Type.STRING,
description="De naam van de stad"
),
"unit": types.Schema(
type=types.Type.STRING,
enum=["celsius", "fahrenheit"],
description="Temperatuureenheid"
)
},
required=["city"]
)
)
]
)
def run_gemini_conversation(user_message: str) -> str:
response = client.models.generate_content(
model="gemini-2.5-pro",
contents=user_message,
config=types.GenerateContentConfig(
tools=[weather_tool],
tool_config=types.ToolConfig(
function_calling_config=types.FunctionCallingConfig(
mode="AUTO"
)
)
)
)
# Controleer op function calls
part = response.candidates[0].content.parts[0]
if hasattr(part, "function_call") and part.function_call:
fc = part.function_call
args = dict(fc.args)
if fc.name == "get_weather":
result = get_weather(**args)
else:
result = {"error": f"Onbekende functie: {fc.name}"}
# Stuur resultaat terug
response = client.models.generate_content(
model="gemini-2.5-pro",
contents=[
types.Content(
role="user",
parts=[types.Part.from_text(user_message)]
),
response.candidates[0].content,
types.Content(
parts=[types.Part.from_function_response(
name=fc.name,
response=result
)]
)
],
config=types.GenerateContentConfig(tools=[weather_tool])
)
return response.text
print(run_gemini_conversation("Wat is het weer in Amsterdam?"))
De Gemini SDK voelt wat anders aan dan OpenAI en Claude — meer wrapper-classes, meer types. Het went na een paar keer, maar de leercurve is iets steiler.
Vergelijking: OpenAI vs. Claude vs. Gemini
Oké, je hebt nu alle drie de implementaties gezien. Maar welke moet je kiezen? Hieronder een overzicht van de belangrijkste verschillen:
| Kenmerk | OpenAI (GPT-4o/5) | Claude (Sonnet/Opus) | Gemini (2.5 Pro) |
|---|---|---|---|
| Tool-definitie veld | tools + "type": "function" | tools + input_schema | function_declarations |
| Resultaat-formaat | role: "tool" | tool_result blokken | FunctionResponse |
| Parallelle calls | Ja (standaard) | Ja | Ja |
| Tool control | auto, required, none | auto, any, specifieke tool | AUTO, ANY, NONE |
| Sterk in | Betrouwbaarheid bij API-integratie | Lange context, autonoom handelen | Multimodaal, grote context |
In de praktijk merk je dat de keuze vaak afhangt van je bestaande stack. Gebruik je al OpenAI voor andere zaken? Dan is GPT-4o de pad van de minste weerstand. Bouw je autonome agents die lang zelfstandig moeten draaien? Dan is Claude vaak de betere keuze.
Parallelle Function Calls
Een van de krachtigste mogelijkheden van moderne LLMs is het gelijktijdig aanroepen van meerdere tools. Stel, een gebruiker vraagt: "Wat is het weer in Amsterdam en Rotterdam?" — dan kan het model beide get_weather-aanroepen in één keer doen in plaats van ze na elkaar af te handelen.
De voordelen zijn behoorlijk significant:
- Lagere latentie — bij 3 tot 5 parallelle calls daalt de responstijd met 60-80% vergeleken met sequentiële uitvoering
- Minder API-aanroepen — één model-inferentie vervangt meerdere rondes, wat tokens en kosten bespaart
Parallelle calls afhandelen in Python
import asyncio
import json
# Registratie van beschikbare functies
TOOL_REGISTRY = {
"get_weather": get_weather,
"search_flights": search_flights,
"get_hotel_prices": get_hotel_prices,
}
async def execute_tool_calls_parallel(tool_calls: list) -> list:
\"\"\"Voer meerdere tool calls parallel uit met asyncio.\"\"\"
async def execute_single(tool_call):
func_name = tool_call.function.name
func_args = json.loads(tool_call.function.arguments)
func = TOOL_REGISTRY.get(func_name)
if func is None:
return {"tool_call_id": tool_call.id, "error": "Onbekende functie"}
# Voer synchrone functies uit in een thread pool
loop = asyncio.get_event_loop()
result = await loop.run_in_executor(None, lambda: func(**func_args))
return {
"role": "tool",
"tool_call_id": tool_call.id,
"content": json.dumps(result)
}
# Alle tool calls tegelijkertijd uitvoeren
results = await asyncio.gather(
*[execute_single(tc) for tc in tool_calls]
)
return list(results)
In de praktijk combineer je vaak parallelle en sequentiële aanroepen. Denk aan het plannen van een reis: eerst zoek je search_flights en get_hotel_prices parallel op (die hebben geen onderlinge afhankelijkheid), en daarna roep je calculate_total_cost aan om alles bij elkaar op te tellen.
Foutafhandeling in Productie
Oké, je hebt function calling werkend. Maar in productie gaat er van alles mis — geloof me. Er zijn grofweg drie typen fouten waar je rekening mee moet houden.
1. Structurele fouten
Het model genereert ongeldige JSON, verwijst naar niet-bestaande functies, of stuurt parameters met verkeerde types. Komt vaker voor dan je zou verwachten. De oplossing:
def safe_parse_tool_call(tool_call, schema: dict) -> dict:
\"\"\"Veilig parsen en valideren van tool call argumenten.\"\"\"
try:
args = json.loads(tool_call.function.arguments)
except json.JSONDecodeError as e:
return {
"error": True,
"message": f"Ongeldige JSON in tool argumenten: {str(e)}",
"tool_call_id": tool_call.id
}
# Valideer verplichte velden
required = schema.get("required", [])
missing = [f for f in required if f not in args]
if missing:
return {
"error": True,
"message": f"Ontbrekende verplichte velden: {missing}",
"tool_call_id": tool_call.id
}
return {"error": False, "args": args, "tool_call_id": tool_call.id}
2. Runtime fouten
Externe API's falen. Door rate limits, authenticatiefouten of simpelweg downtime. Het hoort erbij. Implementeer retry-logica met exponential backoff:
import time
def execute_with_retry(func, args: dict, max_retries: int = 3) -> dict:
\"\"\"Voer een tool uit met retry-logica voor tijdelijke fouten.\"\"\"
for attempt in range(max_retries):
try:
return {"success": True, "result": func(**args)}
except ConnectionError:
if attempt < max_retries - 1:
wait = 2 ** attempt # Exponential backoff
time.sleep(wait)
else:
return {"success": False, "error": "API niet bereikbaar na meerdere pogingen"}
except ValueError as e:
# Geen retry voor validatiefouten
return {"success": False, "error": str(e)}
3. Logische fouten (hallucinaties)
Dit is de lastigste categorie. Het model stuurt syntactisch correcte maar semantisch onjuiste argumenten — een geldige JSON-structuur, maar met onzin-waarden. Beperk dit risico door:
- Strikte enum-waarden te definiëren in je JSON Schema
- Waardebereiken te valideren (bijv. datums in het verleden afwijzen)
- Gedetailleerde foutmeldingen terug te sturen zodat het model kan zelfcorrigeren
Die laatste is trouwens een handige truc. Als je het model specifieke feedback geeft zoals "Invoerformaat is onjuist: verwacht ISO-8601 datum", dan is de kans groot dat het model de fout herkent en gecorrigeerde JSON genereert in de volgende ronde.
Productie-Architectuur
Voor betrouwbare tool calling in productie heb je meer nodig dan alleen retry-logica. Een gelaagde architectuur helpt enorm:
Je App → Circuit Breaker → Graceful Degradation → Model Fallback → Retry + Backoff → LLM API
Wat doen die lagen precies?
- Circuit Breaker — stopt verzoeken wanneer een dienst herhaaldelijk faalt, voorkomt cascade-fouten
- Graceful Degradation — retourneert een zinvol fallback-antwoord als echt alles faalt
- Model Fallback — schakelt automatisch over naar een alternatief model (bijv. van GPT-5 naar Claude Sonnet)
- Retry + Backoff — behandelt tijdelijke fouten met exponentieel toenemende wachttijden
Context Management
Hier lopen veel ontwikkelaars tegenaan: multi-turn conversaties groeien snel. Elke function call en het bijbehorende resultaat worden onderdeel van de conversatiecontext en verbruiken tokens. Bij complexe flows met meerdere tool-rondes kan dat oplopen.
De pragmatische oplossing? Vat oudere functieresultaten samen of verwijder ze uit de context. Bewaar alleen de meest recente en relevante resultaten om binnen je tokenbudget te blijven.
Best Practices voor Tool-Schema's
De kwaliteit van je tooldefinities bepaalt rechtstreeks hoe goed het model de juiste tool selecteert en correcte argumenten genereert. Na flink wat experimenteren zijn dit de richtlijnen die ik zou aanraden:
- Schrijf gedetailleerde descriptions — een vage beschrijving leidt tot verkeerde tool-selectie. Leg uit wat de functie doet, wanneer die gebruikt moet worden, en wat de beperkingen zijn
- Gebruik duidelijke parameternamen —
city_nameis veel beter danqofparam1 - Definieer enums waar mogelijk — dit beperkt de output tot geldige waarden en voorkomt hallucinaties
- Markeer verplichte velden — gebruik
"required"om ontbrekende argumenten te voorkomen - Beperk het aantal tools — meer dan 15-20 tools verslechtert de selectie-nauwkeurigheid bij de meeste modellen aanzienlijk
- Groepeer gerelateerde tools — als je veel tools hebt, overweeg een twee-staps aanpak: eerst een "categorie"-tool, dan de specifieke tools
Beveiliging en Governance
In productie- en enterprise-omgevingen kun je niet zonder extra beveiligingsmaatregelen. Het model beslist immers welke functies het wil aanroepen, en dat moet je zorgvuldig bewaken:
- Allowlist van tools — beperk de beschikbare acties tot goedgekeurde tools per gebruikersrol
- Schema-validatie — valideer types, verplichte velden en formaten voordat je functies uitvoert (altijd, geen uitzonderingen)
- Autorisatie per aanroep — RBAC voor AI-tools moet door jouw systeem worden afgedwongen, niet impliciet via prompts
- Human-in-the-loop — vereist menselijke goedkeuring voor impactvolle acties zoals verwijderingen, betalingen of externe e-mails
- Rate limits en timeouts — behandel tools als productie-afhankelijkheden met eigen beschermingslagen
- Geen secrets in prompts — gebruik credential stores en scoped tokens, nooit API-sleutels in tool-argumenten
Vooral dat laatste punt wordt nog weleens vergeten. Prompts (en dus tool-argumenten) kunnen gelogd worden, in caches belanden of via prompt injection gelekt worden. Houd credentials daar altijd buiten.
Van Function Calling naar MCP
Het Model Context Protocol (MCP) is in 2026 uitgegroeid tot de dominante standaard voor het koppelen van AI-agents aan externe tools. Oorspronkelijk gelanceerd door Anthropic in november 2024, is het inmiddels omarmd door OpenAI en Google DeepMind.
Wat is het verschil? Function calling is het mechanisme — hoe agents functies aanroepen. MCP is het protocol — een gestandaardiseerde manier om tools te beschrijven en te ontsluiten over verschillende systemen heen. Een vergelijking die ik graag maak: function calling is telefoneren, en MCP is de telecomstandaard die bepaalt hoe gesprekken worden opgebouwd en gerouteerd.
Wil je dieper duiken in MCP? Lees dan onze uitgebreide gids over het Model Context Protocol.
Veelgestelde Vragen
Wat is het verschil tussen function calling en tool use?
Eigenlijk niets — het zijn synoniemen. OpenAI introduceerde oorspronkelijk de term "function calling", maar is later overgestapt op "tool use". Anthropic en Google gebruiken ook "tool use". Technisch verwijzen beide naar hetzelfde: het LLM genereert gestructureerde argumenten voor een externe functie die jouw code uitvoert.
Welk LLM is het beste voor function calling in 2026?
Dat hangt er echt van af. GPT-5 scoort het hoogst op nauwkeurigheid bij multi-turn tool calling (98,7% op TAU2-Bench). Claude Opus 4 excelleert bij langdurig autonoom toolgebruik (72,7% op OSWorld). Gemini 2.5 Pro leidt bij cross-MCP toolcoördinatie. Kort samengevat: GPT-5 voor betrouwbaarheid, Claude voor autonome agents, Gemini voor multimodale taken.
Hoeveel tools kan ik definiëren voor een LLM?
Technisch ondersteunen de meeste modellen tientallen tot honderden tools. Maar — en dit is belangrijk — de selectie-nauwkeurigheid neemt merkbaar af boven de 15-20 tools. Bij grote aantallen kun je een twee-staps aanpak gebruiken, of Claude's Tool Search-functionaliteit inzetten die automatisch relevante tools selecteert uit een grote verzameling.
Kan ik function calling gebruiken met lokale open-source modellen?
Absoluut. In 2026 ondersteunen diverse lokale modellen native tool calling, waaronder Llama 4, Qwen3, DeepSeek-V3 en Mistral Large. Je draait ze via Ollama, vLLM of llama.cpp met een OpenAI-compatibele API, zodat je dezelfde code kunt hergebruiken als met cloud-modellen.
Hoe voorkom ik dat een LLM onjuiste function calls maakt?
Combineer meerdere strategieën: schrijf gedetailleerde toolomschrijvingen, definieer strikte JSON Schema's met enums en verplichte velden, valideer argumenten voordat je functies uitvoert, en stuur duidelijke foutmeldingen terug zodat het model kan zelfcorrigeren. En voor kritieke acties? Voeg altijd een human-in-the-loop stap toe. Beter een bevestiging te veel dan een onbedoelde betaling.