NL ▾

Voorbeeld voor toolaanroep in Ollama API met de OpenAI SDK

Tool calling stelt het model in staat je code te vragen een functie uit te voeren en het resultaat vervolgens in zijn antwoord te gebruiken. Hieronder vind je een compleet voorbeeld met de OpenAI SDK tegen een gehoste endpoint, plus streaming en de fouten die de meeste mislukkingen veroorzaken.

Bijgewerkt

Illustratie van een model dat een taak overdraagt aan een functie en het resultaat ontvangt in een lus

Belangrijkste punten

  1. Stuur je functie als een JSON-schema in tools; het model antwoordt met tool_calls in plaats van tekst.
  2. Je voert de functie uit, voegt het resultaat toe als een role tool-bericht en roept de API opnieuw aan.
  3. Parseer argumenten altijd voorzichtig, omdat een model onjuiste of onverwachte JSON kan retourneren.
  4. Gestreamde toolaanroepen komen in fragmenten aan die je per index aan elkaar moet plakken.

Hoe de lus eruit ziet

Als je op zoek bent naar een werkend voorbeeld voor toolaanroep in de Ollama API tegen een gehoste endpoint, is de structuur de standaard OpenAI-structuur. Je beschrijft je functies in een tools-array. Het model antwoordt ofwel in tekst of retourneert een tool_calls-lijst met de naam van de functie en de argumenten. Het model voert niets zelf uit. Jouw code voert de functie uit, stuurt de uitvoer terug en het model schrijft het definitieve antwoord.

Dat resulteert in één rondreis voor toolgebruik, wat twee API-verzoeken zijn:

  1. Stuur het gebruikersbericht en de tool-definities.
  2. Ontvang een assistant-bericht met tool_calls. Voer elke aanroep lokaal uit.
  3. Stuur de originele berichten, het assistant-bericht en één role: "tool"-bericht per aanroep met het resultaat.
  4. Ontvang het definitieve tekstantwoord.

De voorbeelden gebruiken base URL https://api.ollamaapi.com/v1 en model ID uncensored. De endpoint is een gehoste service, dus de sleutel komt van de API-sleutelpagina. Dezelfde code draait ook tegen een lokale Ollama-server als je de base URL, de sleutel en de modelnaam aanpast, zolang het lokale model tools ondersteunt.

Een compleet Python-voorbeeld

Het voorbeeld geeft het model één functie, get_order_status, aangedreven door een kleine dictionary. Een echte applicatie zou op dat punt een database of een interne service bevragen.

import json
from openai import OpenAI

client = OpenAI(base_url="https://api.ollamaapi.com/v1", api_key="YOUR_KEY")

ORDERS = {"A1001": "shipped", "A1002": "processing", "A1003": "delivered"}

def get_order_status(order_id: str) -> str:
    return ORDERS.get(order_id, "not found")

tools = [{
    "type": "function",
    "function": {
        "name": "get_order_status",
        "description": "Look up the status of an order by its ID.",
        "parameters": {
            "type": "object",
            "properties": {
                "order_id": {"type": "string", "description": "Order ID such as A1001"}
            },
            "required": ["order_id"],
        },
    },
}]

messages = [{"role": "user", "content": "Where is order A1002?"}]

first = client.chat.completions.create(
    model="uncensored",
    messages=messages,
    tools=tools,
    tool_choice="auto",
)
msg = first.choices[0].message

if msg.tool_calls:
    messages.append(msg)  # keep the assistant turn with its tool_calls
    for call in msg.tool_calls:
        args = json.loads(call.function.arguments)
        result = get_order_status(**args)
        messages.append({
            "role": "tool",
            "tool_call_id": call.id,
            "content": result,
        })
    final = client.chat.completions.create(
        model="uncensored",
        messages=messages,
        tools=tools,
    )
    print(final.choices[0].message.content)
else:
    print(msg.content)

Drie details zijn hier belangrijk. Het assistantbericht met tool_calls moet vóór de toolresultaten in de geschiedenis worden opgenomen, anders wordt het volgende verzoek afgewezen of verward. Elk toolbericht moet de tool_call_id bevatten van de aanroep die het beantwoordt. En het tweede verzoek moet nog steeds tools bevatten, omdat het model misschien een andere functie wil aanroepen na het zien van het eerste resultaat.

Hetzelfde verzoek met curl

Testen met curl neemt de SDK uit de vergelijking, wat helpt als je niet zeker weet of een probleem in je code of in het verzoek zit.

curl https://api.ollamaapi.com/v1/chat/completions \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "uncensored",
    "messages": [{"role": "user", "content": "Where is order A1002?"}],
    "tools": [{
      "type": "function",
      "function": {
        "name": "get_order_status",
        "description": "Look up the status of an order by its ID.",
        "parameters": {
          "type": "object",
          "properties": {"order_id": {"type": "string"}},
          "required": ["order_id"]
        }
      }
    }],
    "tool_choice": "auto"
  }'

Een succesvol eerste antwoord heeft finish_reason ingesteld op tool_calls, een lege of null content, en een tool_calls-array. Het veld arguments is een string met JSON, geen genest object, vandaar dat het Python-voorbeeld json.loads aanroept.

Het veld tool_choice bepaalt hoe gedwongen het verzoek is. auto laat het model beslissen. Een object dat een functie benoemt vraagt om die specifieke functie. Gebruik auto tenzij je een reden hebt, omdat het afdwingen van een aanroep op een bericht dat dat niet nodig heeft, verzonnen argumenten oplevert.

Hetzelfde loop in Node

De Node-versie gebruikt het officiële openai-pakket en volgt dezelfde stappen. Deze versie behandelt meer dan één aanroep per beurt met Promise.all.

import OpenAI from "openai";

const client = new OpenAI({
  baseURL: "https://api.ollamaapi.com/v1",
  apiKey: process.env.API_KEY,
});

const handlers = {
  get_order_status: async ({ order_id }) =>
    ({ A1001: "shipped", A1002: "processing" })[order_id] ?? "not found",
};

const tools = [{
  type: "function",
  function: {
    name: "get_order_status",
    description: "Look up the status of an order by its ID.",
    parameters: {
      type: "object",
      properties: { order_id: { type: "string" } },
      required: ["order_id"],
    },
  },
}];

const messages = [{ role: "user", content: "Where is order A1001?" }];
const first = await client.chat.completions.create({
  model: "uncensored", messages, tools,
});
const msg = first.choices[0].message;

if (msg.tool_calls?.length) {
  messages.push(msg);
  const results = await Promise.all(msg.tool_calls.map(async (call) => {
    const args = JSON.parse(call.function.arguments);
    const out = await handlers[call.function.name](args);
    return { role: "tool", tool_call_id: call.id, content: String(out) };
  }));
  messages.push(...results);
  const final = await client.chat.completions.create({
    model: "uncensored", messages, tools,
  });
  console.log(final.choices[0].message.content);
} else {
  console.log(msg.content);
}

Streaming van toolaanroepen

Met stream: true komt het antwoord via SSE binnen en komen toolaanroepen in stukken. Elk fragment bevat een delta.tool_calls-entry met een index. Het eerste fragment voor een index bevat de call ID en de functienaam. Latere fragmenten voegen slices van de arguments-string toe. Je plakt de slices per index aan elkaar en parseert pas de JSON nadat de stream eindigt.

stream = client.chat.completions.create(
    model="uncensored",
    messages=[{"role": "user", "content": "Where is order A1003?"}],
    tools=tools,
    stream=True,
)

calls = {}
for chunk in stream:
    if not chunk.choices:
        continue
    delta = chunk.choices[0].delta
    for part in (delta.tool_calls or []):
        slot = calls.setdefault(part.index, {"id": "", "name": "", "args": ""})
        if part.id:
            slot["id"] = part.id
        if part.function and part.function.name:
            slot["name"] = part.function.name
        if part.function and part.function.arguments:
            slot["args"] += part.function.arguments

for slot in calls.values():
    print(slot["name"], json.loads(slot["args"]))

Onthoud dat een gestreamd verzoek zijn concurrency-slot vasthoudt tot de stream klaar is. Een volledige sleutel staat 8 gelijktijdige verzoeken en 300 per minuut toe, een proefsleutel 2 en 60. Als je tool-loops voor veel gebruikers parallel uitvoert, tel dan elk openstaande stream mee voor het eerste getal.

Valkuilen en hoe ze op te lossen

Ongeldige of ontbrekende argumenten

Het model is een open-weight model, en open modellen retourneren soms argumenten die geen geldige JSON zijn, slaan een verplicht veld over of verzinnen er een extra bij. Wikkel json.loads in een try-blok, valideer tegen je schema en stuur bij een fout de fout terug als een tool-bericht. Het model corrigeert zichzelf meestal in de volgende beurt. Stel een limiet in op het aantal iteraties zodat een hardnekkige fout niet oneindig kan draaien.

Te vage beschrijvingen

Het model kiest een functie door de naam en beschrijving te lezen. Een beschrijving zoals "haalt data op" geeft het niets om mee te werken. Zeg wat de functie doet, wat de inputs betekenen en voeg een voorbeeldwaarde toe in de parameterbeschrijving.

Te veel tools tegelijk

Elke tool-definitie telt mee als input tokens bij elk verzoek. Bij $0,15 per 1M input tokens voegen tien tools van elk 150 tokens 1.500 tokens toe, wat $0,000225 per verzoek is, klein maar niet nul bij volume. Belangrijker nog, een lang menu maakt selectie minder betrouwbaar. Bied alleen de tools aan die relevant zijn voor de huidige stap.

Te grote tool-resultaten

Wat je ook retourneert, wordt teruggevoerd als input. Knip grote database-rijen of webpagina's terug naar de velden die het model nodig heeft. Het contextvenster is in totaal 100.000 tokens, dus een zeer groot resultaat kan het gesprek verdringen.

Tools mengen met JSON-modus

JSON-modus (response_format met json_object) is beschikbaar, maar gebruik deze voor het eindantwoord, niet voor beurten waarin je het model een functie wilt laten aanroepen. Test de combinatie op je eigen prompts voordat je erop vertrouwt.

Van lokale Ollama naar wat je moet controleren

Als je je eerste tool-loop tegen een lokale Ollama-server hebt gebouwd, verandert er bijna niets. Vervang de base URL door https://api.ollamaapi.com/v1, gebruik een echte sleutel en stel het model in op uncensored. Native Ollama-routes zoals /api/chat bestaan hier niet. Alleen POST /v1/chat/completions en GET /v1/models zijn beschikbaar, dus code die voor de native API is geschreven, moet de OpenAI-stijl aanroep gebruiken.

Voordat je gaat deployen, loop deze korte checklist af:

  • Bevestig dat de sleutel werkt met GET /v1/models.
  • Log de X-Request-Id header voor elke aanroep, zodat een slechte run teruggevonden kan worden.
  • Houd X-Balance-USD in de antwoordheaders in de gaten als je tool-lussen onbeheerd uitvoert.
  • Verwerk HTTP 429: wacht op rate_limited of concurrency, en respecteer Retry-After: 5 bij overbelasting.
  • Stel een harde limiet in op het aantal loop-iteraties.

Een tool-lus maakt minimaal twee verzoeken per gebruikersvraag, dus de kosten schalen daarmee. Bijvoorbeeld: 2.000 lussen met 1.200 inputtokens en 150 outputtokens per verzoek, twee verzoeken elk, komen neer op 4.000 verzoeken, 4,8M inputtokens en 0,6M outputtokens. Dat is $0,72 plus $0,30, in totaal $1,02. De prijzenpagina heeft de opwaardeeropties, en de docs vermelden de ondersteunde verzoekvelden. Nieuwe gebruikers kunnen dit allemaal uitproberen met het gratis proeftegoed van $0,50, waar geen kaart voor nodig is.

Vragen en antwoorden

Ondersteunt de endpoint function calling?

Ja. Hij accepteert tools en tool_choice, retourneert tool_calls, streamt ze als deltas en neemt resultaten terug als berichten met role tool.

Waarom faalt het tweede verzoek na een toolaanroep?

De gebruikelijke oorzaak is een ontbrekend assistantbericht of een toolbericht zonder de bijpassende tool_call_id. Voeg eerst de assistant-beurt met tool_calls toe, gevolgd door één toolbericht per aanroep.

Kan het model meerdere tools in één turn aanroepen?

Het kan een lijst met meer dan één item retourneren, dus loop over alle items en stuur één resultaat voor elk. Ga er niet van uit dat de lijst slechts één item bevat.

Heb ik de Ollama-library hiervoor nodig?

Nee. De officiële OpenAI SDK's zijn genoeg. Wijs de basis-URL naar de endpoint en gebruik het model uncensored.

Je sleutel is nog maar één formulier verwijderd

Maak een account aan, kopieer de sleutel, pas de base URL aan. Dat is de hele configuratie.

API-sleutel ophalenDocumentatie lezen