Lokale Function Calling met Ollama en Python Bouwen
Ollama met de officiële Ollama Python SDK en Python 3.10 of hoger.
Hardware-richtlijn: een moderne processor met voldoende werkgeheugen of een grafische kaart met dedicated VRAM om instructie-getunede modellen (zoals 7B, 8B of 12B varianten) in 4-bit kwantisatie volledig in het geheugen te laden.
Geschikte referentiemodellen: instructie-afgestemde varianten zoals Qwen 2.5, Llama 3.1 en Mistral Nemo.
Binnen de leercyclus van lokale taalmodellen bevinden we ons nu in de fase van toepassingen en workflow-koppelingen. Nadat een model succesvol lokaal is ingericht, ontstaat direct de behoefte om het model acties te laten uitvoeren: data ophalen uit een relationele database, een interne API aanroepen of bestanden op het bestandssysteem inspecteren. Wie de basisinstallatie nog moet voltooien, leest eerst de handleiding over hardware voor lokale LLM's kiezen en kan voor een snelle start op Apple-apparatuur terecht bij de stap-voor-stap handleiding voor Ollama op macOS installeren.
Function calling transformeert een zuiver tekstgenererend model in een operationele beslissingsmotor. In plaats van een vrij antwoord in proza te formuleren, genereert het taalmodel een gestructureerde instructie met een functienaam en de bijbehorende parameters. In deze gids doorlopen we de volledige keten: hoe de interactielus werkt, hoe JSON-schema's worden opgesteld met Pydantic, hoe foutafhandeling en zelfcorrectie worden ingericht bij ongeldige modeluitvoer, en welke modelkeuzes doorslaggevend zijn voor een betrouwbare pijplijn.
1. De Anatomie van Lokale Function Calling
Bij function calling (of tool use) voert het taalmodel zelf nooit direct programmacode of netwerkverzoeken uit. Het model fungeert als interpreterende component die een natuurlijk taalverzoek analyseert en vertaalt naar een gestructureerd formaat dat de lokale Python-applicatie kan valideren en uitvoeren. Wie de fundamentele theorie achter token-segmentatie en JSON-representatie wil bestuderen, kan de achtergrond over tool calling en gestructureerde uitvoer raadplegen op het leerplatform.
De interactie verloopt in een gesloten lus van vier vaste stappen:
- Definitie & Aanroep: Het Python-script stuurt het gebruikersbericht samen met een lijst van beschikbare gereedschappen (gedefinieerd via JSON-schema's) naar de lokale Ollama-service.
- Besluitvorming door het model: Het model evalueert de context. Als een gereedschap noodzakelijk is om de vraag te beantwoorden, genereert het een JSON-payload met de functienaam en de geëxtraheerde argumenten.
- Lokale Executie: De Python-runtime parseert de JSON-uitvoer, valideert de parameters tegen het datamodel, voert de daadwerkelijke lokale code uit (zoals een databasequery of bestandsscanner) en vangt het resultaat op.
- Synthese: Het functieresultaat wordt teruggestuurd naar het model met de rol
tool. Het model leest deze context en formuleert een definitief, natuurlijk taalantwoord voor de gebruiker.
Het grote voordeel van deze lokale architectuur is volledige controle over data en privacy. Bedrijfsgevoelige gegevens, interne documenten en API-sleutels blijven strikt binnen de eigen infrastructuur. Zoals beschreven in het artikel over privacyvriendelijk AI-gebruik, verlaat er geen enkele byte aan interactiegeschiedenis de lokale machine.
2. JSON-Schema's Definiëren met Python en Pydantic
Ollama verwacht functiebeschrijvingen in een gestandaardiseerd JSON-schemaformaat. Handmatig geschreven dictionaries zijn foutgevoelig: typefouten in veldnamen of onjuiste JSON-syntax kunnen ervoor zorgen dat het model ongeldige aanroepen genereert. Door Pydantic te gebruiken worden datamodellen programmatisch gedefinieerd en automatisch omgezet naar foutloze JSON-schema's.
Voor kwantisaties en modelgroottes wordt in lokale omgevingen veelal gewerkt met 4-bit compressie om geheugengebruik en rekentijd in evenwicht te houden. Voor een gedetailleerde toelichting over hoe gewichtscompressie werkt, zie het overzicht over kwantisatiemethoden en formaten.
Hieronder staat een implementatie van twee functies: een tool voor het opzoeken van marktkoersen en een tool voor het inspecteren van lokale bestanden.
import json
from typing import List, Dict, Any
from pydantic import BaseModel, Field
import ollama
# 1. Definieer Pydantic-modellen voor parameter-validatie
class StockPriceArgs(BaseModel):
ticker: str = Field(
description="Het aandelensymbool, bijvoorbeeld AAPL, ASML of MSFT"
)
currency: str = Field(
default="EUR",
description="De gewenste valuta voor de prijs (EUR of USD)"
)
class FileSearchArgs(BaseModel):
directory: str = Field(
description="Het absolute pad naar de te doorzoeken directory"
)
extension: str = Field(
default=".log",
description="De bestandsextensie waarop gefilterd moet worden"
)
# 2. Converteer de definities naar het Ollama gereedschapsformaat
tools_schema = [
{
"type": "function",
"function": {
"name": "get_stock_price",
"description": "Haal de meest recente marktprijs op voor een specifiek aandeel.",
"parameters": StockPriceArgs.model_json_schema()
}
},
{
"type": "function",
"function": {
"name": "search_local_files",
"description": "Zoek bestanden van een specifiek type binnen een lokale directory.",
"parameters": FileSearchArgs.model_json_schema()
}
}
]
Houd de veldbeschrijvingen (description) beknopt en ondubbelzinnig. Kleinere open modellen presteren aanzienlijk stabieler wanneer schema's direct to-the-point zijn en geen tegenstrijdige termen bevatten.
3. De Chat- en Uitvoeringslus Implementeren
De interactie met de Ollama API vereist een orchestratielus die controleert of het modelbericht een tool_calls-veld bevat. Wanneer het model een functie wil aanroepen, moeten de functies lokaal worden opgezocht in een dispatch-tabel, worden uitgevoerd en met hun output worden toegevoegd aan het gespreksverloop.
# Functie-implementaties
def get_stock_price(ticker: str, currency: str = "EUR") -> Dict[str, Any]:
# Illustratieve implementatie van een externe databron
mock_prices = {"ASML": 845.20, "AAPL": 215.50, "MSFT": 420.10}
price = mock_prices.get(ticker.upper(), 100.0)
return {"ticker": ticker.upper(), "price": price, "currency": currency}
def search_local_files(directory: str, extension: str = ".log") -> Dict[str, Any]:
# Veilige mock voor bestandsinspectie
return {
"directory": directory,
"found_files": [f"app_{extension.lstrip('.')}_01.log", f"app_{extension.lstrip('.')}_02.log"],
"count": 2
}
# Dispatch mapping tabel
TOOL_REGISTRY = {
"get_stock_price": get_stock_price,
"search_local_files": search_local_files
}
def run_agent_loop(user_prompt: str, model_name: str = "llama3.1:8b"):
messages = [
{
"role": "system",
"content": "Je bent een technische assistent. Gebruik de beschikbare tools wanneer dynamische of feitelijke gegevens vereist zijn."
},
{"role": "user", "content": user_prompt}
]
client = ollama.Client()
# Eerste aanroep naar Ollama met gereedschappen
response = client.chat(
model=model_name,
messages=messages,
tools=tools_schema,
options={"temperature": 0.1}
)
messages.append(response["message"])
# Controleer of het model gereedschap wil aanroepen
tool_calls = response["message"].get("tool_calls", [])
if not tool_calls:
return response["message"]["content"]
for tool in tool_calls:
function_name = tool["function"]["name"]
function_args = tool["function"]["arguments"]
if function_name in TOOL_REGISTRY:
# Voer de Python-functie lokaal uit
tool_output = TOOL_REGISTRY[function_name](**function_args)
# Voeg het resultaat toe als tool role
messages.append({
"role": "tool",
"content": json.dumps(tool_output),
})
else:
messages.append({
"role": "tool",
"content": json.dumps({"error": f"Functie {function_name} niet geregistreerd"})
})
# Tweede aanroep: laat het model het eindantwoord synthetiseren
final_response = client.chat(
model=model_name,
messages=messages,
options={"temperature": 0.2}
)
return final_response["message"]["content"]
Wie op zoek is naar een alternatief zonder eigen Python-scripts, kan de interactie automatiseren via visuele pipelines; zie hiervoor het artikel over lokale agentic workflows met n8n en Ollama.
4. Foutafhandeling en Self-Correction bij Lokale Modellen
In tegenstelling tot grote commerciële cloud-API's vertonen compacte lokale modellen vaker afwijkingen bij het genereren van strikt gestructureerde data. Veelvoorkomende patronen zijn:
- Afwijkende parameternamen: Het model stuurt bijvoorbeeld
{"symbol": "ASML"}in plaats van het gedefinieerde{"ticker": "ASML"}. - Niet-bestaande functies: Het model verzint een gereedschap dat niet in de lijst met definities voorkomt.
- Syntaxfouten in JSON: Ontbrekende sluitaccolades of ongeldige karakters bij geneste structuren.
Om te voorkomen dat de runtime crasht, bouwen we een herstelmechanisme rondom de executie. Als Pydantic een validatiefout (ValidationError) detecteert, sturen we de exacte foutmelding terug naar het model met het verzoek de parameters te corrigeren. Dit sluit aan op de techniek voor gestructureerde JSON-outputs afdwingen.
from pydantic import ValidationError
def execute_with_recovery(model_name: str, messages: list, tool_call: dict, client: ollama.Client, max_retries: int = 2) -> dict:
fn_name = tool_call["function"]["name"]
raw_args = tool_call["function"]["arguments"]
for attempt in range(max_retries):
try:
if fn_name == "get_stock_price":
validated_args = StockPriceArgs(**raw_args)
return get_stock_price(**validated_args.model_dump())
elif fn_name == "search_local_files":
validated_args = FileSearchArgs(**raw_args)
return search_local_files(**validated_args.model_dump())
else:
raise ValueError(f"Onbekende functie '{fn_name}' aangeroepen.")
except (ValidationError, ValueError) as err:
if attempt == max_retries - 1:
return {"error": f"Functieaanroep definitief mislukt na {max_retries} pogingen: {str(err)}"}
# Voeg de validatiefout toe aan het gespreksverloop
messages.append({
"role": "tool",
"content": json.dumps({
"status": "validation_error",
"error_details": str(err),
"instruction": "Corrigeer de argumenten zodat ze exact voldoen aan het JSON-schema."
})
})
correction_response = client.chat(
model=model_name,
messages=messages,
tools=tools_schema,
options={"temperature": 0.0}
)
new_calls = correction_response["message"].get("tool_calls", [])
if new_calls:
raw_args = new_calls[0]["function"]["arguments"]
else:
return {"error": "Model leverde geen gecorrigeerde parameters."}
5. Kwalitatieve Vergelijking van Modellen voor Tool Use
Niet elk open-source model is getraind met speciale tokens voor gereedschapsgebruik. Oudere architecturen genereerden platte tekst die achteraf met reguliere expressies moest worden ontleed. Hedendaagse modellen bevatten specifieke instructie-afstemming voor function calling.
Bij het selecteren van een model is het raadzaam de benchmarkmethodologie te bestuderen op function calling nauwkeurigheid testen bij complexe schema's. Tevens biedt het overzicht over modellen kiezen voor function calling inzicht in architecturele eigenschappen.
| Modelarchitectuur | Type Afstemming | Schema-Discipline | Kwalitatieve Eigenschappen |
|---|---|---|---|
| Qwen 2.5 Instruct | Native Tool Calling Tokens | Zeer consistent | Uitstekende parsing van geneste argumenten en strikte opvolging van type-restricties. |
| Llama 3.1 Instruct | Ingebouwde Function Tokens | Consistent | Reageert snel; vraagt soms om extra precisie in veldbeschrijvingen bij meer dan vier actieve tools. |
| Mistral Nemo | Instructie-afgestemd | Consistent | Ruim contextvenster; presteert stabiel bij meertalige parameter-extractie en samengestelde taken. |
| Phi-3.5 Mini | Lichtgewicht Instructiemodel | Wisselend bij complexe schema's | Geschikt voor eenvoudige sleutel-waardeparen; kan bij geneste parameters velden weglaten. |
6. Prestaties bij Nederlandstalige Invoer
Lokale modellen worden overwegend getraind op Engelstalige instructiesets. Wanneer een gebruiker een Nederlandstalige vraag stelt, maakt het model een dubbele vertaalslag: het interpreteren van de Nederlandse intentie en het correct koppelen daarvan aan Engelstalige functienamen en JSON-sleutels.
Een illustratief praktijkvoorbeeld met een Nederlandse invoerzin:
"Zoek in de map /var/log/audit naar alle bestanden die eindigen op .json en geef me de statistieken."
Een goed afgestemd instructiemodel extraheert hieruit de juiste structuur:
{
"name": "search_local_files",
"arguments": {
"directory": "/var/log/audit",
"extension": ".json"
}
}
Bij compacte modellen kan het voorkomen dat een parameterwaarde onbedoeld wordt vertaald (zoals .json-bestanden) of dat een pad niet letterlijk wordt overgenomen. Om de betrouwbaarheid van Nederlandse interacties te optimaliseren, kunnen de richtlijnen worden toegepast uit het artikel over de AI beter laten presteren in het Nederlands. Het toevoegen van duidelijke voorbeelden binnen de veldbeschrijvingen helpt het model om Nederlandse termen correct naar de juiste parameters te leiden.
7. Beveiliging en Risico's van Lokale Executie
Omdat function calling leidt tot daadwerkelijke code-uitvoering op het systeem, brengt deze architectuur specifieke beveiligingsrisico's met zich mee. Wanneer externe data (zoals een binnengehaald document of webpagina) instructies bevat die het model manipuleren, ontstaat het risico op zogeheten indirect prompt injection.
Hanteer daarom altijd de volgende beveiligingsprincipes:
- Geen dynamische code-evaluatie: Vermijd functies zoals
eval()ofexec()en koppel functienamen uitsluitend via een vaste dispatch-tabel. - Strikte padcontrole: Controleer bij functies die bestandspaden accepteren altijd of het doelpad binnen een veilige directory valt om ongeoorloofde toegang tot systeembestanden te voorkomen.
- Scheiding van lees- en schrijfrechten: Bouw voor acties met blijvende impact (zoals het verwijderen van bestanden of bijwerken van records) een expliciete bevestigingsstap in voordat de code wordt uitgevoerd.
- Minimale rechten: Draai de Python-applicatie onder een gebruikersaccount met beperkte toegangsrechten op het host-besturingssysteem.
Conclusie en Volgende Stappen
Lokale function calling met Ollama en Python biedt een solide en privacy-veilige basis voor geautomatiseerde workflows. Door JSON-schema's te valideren met Pydantic en een herstelmechanisme in te richten voor afwijkende modeluitvoer, kunnen ook lokale modellen betrouwbaar worden ingezet in operationele pipelines. Voor wie de stap wil maken naar complexere architecturen met meerdere samenwerkende agents, vormen deze bouwstenen het fundament voor veilige lokale automatisering.


