Naar de inhoud
NLEN
Illustratie: Lokale Function Calling met Ollama en Python Bouwen

Lokale Function Calling met Ollama en Python Bouwen

Door Ivo Donker — samengesteld met AI-ondersteuning (Claude & Gemini)
Systeemvereisten en softwarestack:
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:

  1. 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.
  2. 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.
  3. 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.
  4. 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:

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:

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.