DE ▾

Ollama API Tool Calling-Beispiel mit dem OpenAI SDK

Tool Calling ermöglicht es dem Modell, deinen Code aufzurufen, eine Funktion auszuführen und das Ergebnis in seiner Antwort zu verwenden. Unten findest du ein vollständiges Beispiel mit dem OpenAI SDK gegen einen gehosteten Endpunkt, inklusive Streaming und der Fehler, die die meisten Ausfälle verursachen.

Aktualisiert

Abbildung eines Modells, das eine Aufgabe an eine Funktion übergibt und das Ergebnis in einer Schleife empfängt

Wichtige Punkte

  1. Sende deine Funktion als JSON-Schema in tools; das Modell antwortet mit tool_calls statt Text.
  2. Du führst die Funktion aus, hängst das Ergebnis als role tool Nachricht an und rufst die API erneut auf.
  3. Parst Argumente immer defensiv, da ein Modell fehlerhaftes oder unerwartetes JSON zurückgeben kann.
  4. Gestreamte Tool Calls kommen fragmentiert an und müssen über den Index zusammengesetzt werden.

So sieht die Schleife aus

Wenn du nach einem Beispiel für Tool-Calling in der Ollama-API suchst, das gegen einen gehosteten Endpunkt funktioniert, ist die Struktur die Standard-OpenAI-Struktur. Du beschreibst deine Funktionen in einem tools-Array. Das Modell antwortet entweder mit Text oder gibt eine tool_calls-Liste zurück, die die Funktion und ihre Argumente benennt. Das Modell führt nichts aus. Dein Code führt die Funktion aus, sendet die Ausgabe zurück und das Modell schreibt die finale Antwort.

Das bedeutet, dass eine Roundtrip-Nutzung zwei API-Aufrufe umfasst:

  1. Sende die Benutzer-Nachricht und die Tool-Definitionen.
  2. Erhalte eine Assistant-Nachricht mit tool_calls. Führe jeden Aufruf lokal aus.
  3. Sende die ursprünglichen Nachrichten, die Assistant-Nachricht und eine role: "tool"-Nachricht pro Aufruf mit dem Ergebnis.
  4. Erhalte die finale Textantwort.

Die Beispiele verwenden die Base URL https://api.ollamaapi.com/v1 und die Modell-ID uncensored. Der Endpunkt ist ein gehosteter Dienst, also stammt der Schlüssel von der API-Schlüssel-Seite. Der gleiche Code läuft auch gegen einen lokalen Ollama-Server, wenn du die Base URL, den Schlüssel und den Modellnamen änderst, solange das lokale Modell Tools unterstützt.

Ein vollständiges Python-Beispiel

Das Beispiel gibt dem Modell eine Funktion, get_order_status, die auf einem kleinen Dictionary basiert. Eine echte Anwendung würde an dieser Stelle eine Datenbank oder einen internen Dienst abfragen.

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)

Drei Details sind hier wichtig. Die Assistant-Nachricht, die tool_calls enthält, muss vor den Tool-Ergebnissen wieder in den Verlauf aufgenommen werden, sonst wird die nächste Anfrage abgelehnt oder verwirrt. Jede Tool-Nachricht muss die tool_call_id des Aufrufs enthalten, auf den sie antwortet. Und die zweite Anfrage sollte immer noch tools enthalten, da das Modell nach dem ersten Ergebnis möglicherweise eine weitere Funktion aufrufen möchte.

Die gleiche Anfrage mit curl

Das Testen mit curl entfernt das SDK aus dem Bild, was hilft, wenn du unsicher bist, ob ein Problem in deinem Code oder in der Anfrage liegt.

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"
  }'

Eine erfolgreiche erste Antwort hat finish_reason auf tool_calls gesetzt, einen leeren oder null content und ein tool_calls-Array. Das Feld arguments ist eine Zeichenkette, die JSON enthält, kein verschachteltes Objekt, weshalb das Python-Beispiel json.loads aufruft.

Das Feld tool_choice steuert, wie forciert die Anfrage ist. auto lässt das Modell entscheiden. Ein Objekt, das eine Funktion benennt, fordert genau diese Funktion an. Verwende auto, es sei denn, du hast einen Grund, da das Erzwingen eines Aufrufs bei einer Nachricht, die keinen benötigt, erfundene Argumente erzeugt.

Die gleiche Schleife in Node

Die Node-Version nutzt das offizielle openai-Paket und folgt denselben Schritten. Diese Version verarbeitet mehrere Aufrufe pro Anfrage mit 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);
}

Gestreamte Tool-Aufrufe

Mit stream: true kommt die Antwort über SSE an, und Tool-Calls kommen stückweise an. Jeder Chunk enthält einen Eintrag mit delta.tool_calls und einem index. Das erste Fragment für einen Index enthält die Call-ID und den Funktionsnamen. Spätere Fragmente fügen Teile der arguments-Zeichenkette hinzu. Du verknüpfst die Fragmente pro Index und parst das JSON erst, nachdem das Streaming endet.

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"]))

Denke daran, dass eine gestreamte Anfrage ihren Concurrency-Slot bis zum Stream-Ende belegt. Ein voller Schlüssel erlaubt 8 parallele Anfragen gleichzeitig und 300 pro Minute, ein Trial-Schlüssel 2 und 60. Wenn du Tool-Schleifen für viele Benutzer parallel ausführst, zähle jeden offenen Stream gegen die erste Zahl.

Fallstricke und wie man sie behebt

Fehlerhaftes oder fehlendes Argument

Das Modell ist ein Open-Weight-Modell, und Open-Weight-Modelle geben manchmal Argumente zurück, die kein gültiges JSON sind, lassen ein erforderliches Feld weg oder erfinden ein zusätzliches. Packe json.loads in einen try-Block, validiere gegen dein Schema, und sende bei einem Fehler die Fehlermeldung als Tool-Nachricht zurück. Das Modell korrigiert sich in der Regel im nächsten Durchlauf. Begrenze die Schleife auf wenige Iterationen, damit ein hartnäckiger Fehler nicht endlos läuft.

Zu vage Beschreibungen

Das Modell wählt eine Funktion aus, indem es ihren Namen und ihre Beschreibung liest. Eine Beschreibung wie "Daten abrufen" gibt ihm nichts zu arbeiten. Sage, was die Funktion tut, was die Eingaben bedeuten, und füge einen Beispielwert in die Parameterbeschreibung ein.

Zu viele Tools gleichzeitig

Jede Tool-Definition zählt als Eingabe-Token bei jeder Anfrage. Bei $0,15 pro 1 Mio. Eingabe-Token fügen zehn Tools mit je 150 Token 1.500 Token hinzu, was $0,000225 pro Anfrage entspricht, klein, aber nicht null bei hohem Volumen. Wichtiger ist, dass ein langes Menü die Auswahl weniger zuverlässig macht. Biete nur die Tools an, die für den aktuellen Schritt relevant sind.

Zu große Tool-Ergebnisse

Was auch immer du zurückgibst, wird als Input zurückgespeist. Kürze große Datenbankzeilen oder Webseiten auf die Felder, die das Modell benötigt. Das Kontextfenster hat insgesamt 100.000 Token, also kann ein sehr großes Ergebnis das Gespräch verdrängen.

Tools mit JSON-Modus mischen

Der JSON-Modus (response_format mit json_object) ist verfügbar, aber nutze ihn für die Endergebnis-Antwort, nicht für Durchgänge, in denen du das Modell eine Funktion aufrufen lassen willst. Teste die Kombination an deinen eigenen Prompts, bevor du dich darauf verlässt.

Vom lokalen Ollama weg und was du prüfen solltest

Wenn du deine erste Tool-Schleife gegen einen lokalen Ollama-Server gebaut hast, ändert sich fast nichts. Ersetze die Base URL durch https://api.ollamaapi.com/v1, verwende einen echten Schlüssel und setze das Modell auf uncensored. Native Ollama-Routen wie /api/chat existieren hier nicht. Nur POST /v1/chat/completions und GET /v1/models sind verfügbar, daher benötigt Code, der für die native API geschrieben wurde, den OpenAI-Stil-Aufruf.

Bevor du veröffentlichst, führe diese kurze Checkliste aus:

  • Bestätige, dass der Schlüssel mit GET /v1/models funktioniert.
  • Logge den X-Request-Id-Header für jeden Aufruf, damit ein schlechter Lauf nachverfolgt werden kann.
  • Beobachte X-Balance-USD in den Antwort-Headern, wenn du Tool-Schleifen unbeaufsichtigt ausführst.
  • Behandle HTTP 429: warte auf rate_limited oder concurrency und beachte Retry-After: 5 bei Überlastung.
  • Setze eine harte Obergrenze für Schleifen-Iterationen.

Eine Tool-Schleife benötigt mindestens zwei Anfragen pro Benutzerfrage, daher skalieren die Kosten damit. Zum Beispiel: 2.000 Schleifen mit 1.200 Eingabe-Token und 150 Ausgabe-Token pro Anfrage, zwei Anfragen jeweils, ergeben 4.000 Anfragen, 4,8 Mio. Eingabe-Token und 0,6 Mio. Ausgabe-Token. Das sind $0,72 plus $0,30, also insgesamt $1,02. Die Preisseite zeigt die Auflade-Optionen, und die Dokumentation listet die unterstützten Anfragefelder auf. Neue Nutzer können all dies mit dem $0,50-Testguthaben ausprobieren, das keine Karte benötigt.

Fragen und Antworten

Unterstützt der Endpunkt Function Calling?

Ja. Er akzeptiert Tools und tool_choice, gibt tool_calls zurück, streamt sie als Deltas und nimmt Ergebnisse als Nachrichten mit der Rolle tool entgegen.

Warum schlägt die zweite Anfrage nach einem Tool-Aufruf fehl?

Die übliche Ursache ist eine fehlende Assistant-Nachricht oder eine Tool-Nachricht ohne die passende tool_call_id. Füge zuerst den Assistant-Turn mit den tool_calls ein, dann eine Tool-Nachricht pro Aufruf.

Kann das Modell mehrere Tools in einem Turn aufrufen?

Es kann eine Liste mit mehreren Einträgen zurückgeben. Schleife über alle Einträge und sende für jeden eine Ergebnis-Nachricht. Gehe nicht davon aus, dass die Liste nur ein Element enthält.

Brauche ich dafür die Ollama-Bibliothek?

Nein. Die offiziellen OpenAI SDKs reichen aus. Lege die Base URL auf den Endpunkt und verwende das Modell uncensored.

Dein Schlüssel ist nur ein Formular entfernt

Erstelle ein Konto, kopiere den Schlüssel, ändere die Basis-URL. Das ist die gesamte Einrichtung.

API-Schlüssel erhaltenLies die Docs