Tool-Nutzung — auch Function Calling genannt — macht aus einem Chat-Modell etwas, das Dinge tun kann: einen Datensatz nachschlagen, eine API ansprechen, eine Berechnung ausführen, deine Datenbank abfragen. Das Modell führt den Code nicht selbst aus; es sagt dir, welche Funktion mit welchen Argumenten aufzurufen ist, du führst sie aus und reichst das Ergebnis zurück, damit es die Antwort abschließen kann. Die gute Nachricht: Die OpenAI-tools-Form ist der De-facto-Standard, und über Brievio steuert exakt derselbe Code sowohl Claude als auch Gemini hinter einer einzigen base_url. Ändere den model-String; lass alles andere unberührt.
Dieser Beitrag ist die praktische Variante: ein Tool definieren, tool_calls auslesen, die Multi-Turn-Schleife durchgehend fahren und parallele Calls behandeln. Jedes Snippet ist gegen https://api.brievio.com/v1 mit dem OpenAI-Python-SDK lauffähig. Ich markiere die wenigen Stellen, an denen sich das Verhalten zwischen den Modellfamilien wirklich unterscheidet, damit dich nichts in der Produktion überrascht.
Das mentale Modell: die Schleife, kein Zaubercall
Function Calling ist ein Gespräch, kein One-Shot. Es folgt immer denselben vier Schritten:
- Du sendest die User-Nachricht plus eine Liste von Tools, die das Modell verwenden darf.
- Das Modell entscheidet. Entweder es antwortet in Prosa, oder es gibt einen oder mehrere
tool_callszurück — einen Funktionsnamen und einen JSON-String mit Argumenten — und stoppt. - Du führst die Funktion in deinem eigenen Code aus und hängst das Ergebnis als
tool-Nachricht wieder an die Nachrichtenliste an. - Du rufst das Modell erneut auf mit der längeren Historie. Es liest das Ergebnis und antwortet entweder oder fordert ein weiteres Tool an. Wiederhole das, bis keine Tool-Calls mehr kommen.
Das Modell rührt deine Systeme nie an. Es schlägt nur vor; dein Code entscheidet. Diese Grenze ist die gesamte Sicherheitsgeschichte der Tool-Nutzung — behandle jedes Argument, das das Modell sendet, als nicht vertrauenswürdige Eingabe und validiere es wie ein Formularfeld.
Schritt 1 — ein Tool definieren und den Call auslesen
Ein Tool ist ein JSON Schema, verpackt in {"type": "function", ...}. Die description-Felder sind keine Deko — sie sind das Einzige, was das Modell liest, um zu entscheiden, wann und wie es aufruft. Schreib sie so, als verfasstest du einen Docstring für einen Junior-Entwickler:
# Definiere ein Tool mit dem Standard-OpenAI-"function"-Schema und lies dann
# die tool_calls des Modells wieder aus. Identische Form für Claude und Gemini.
from openai import OpenAI
import json
client = OpenAI(
api_key="sk-brievio-...",
base_url="https://api.brievio.com/v1",
)
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get the current weather for a city.",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "e.g. 'Tokyo'"},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "Temperature unit.",
},
},
"required": ["city"],
},
},
}
]
messages = [{"role": "user", "content": "What's the weather in Tokyo?"}]
resp = client.chat.completions.create(
model="claude-sonnet-4-6", # tausche gegen "gemini-2.5-pro" — gleicher Code unten
messages=messages,
tools=tools,
tool_choice="auto", # das Modell entscheiden lassen, ob es ruft
)
msg = resp.choices[0].message
# Das Modell hat nicht in Prosa geantwortet — es bittet dich, eine Funktion auszuführen.
if msg.tool_calls:
for call in msg.tool_calls:
print(call.function.name) # "get_weather"
print(call.function.arguments) # '{"city": "Tokyo", "unit": "celsius"}'
args = json.loads(call.function.arguments) # immer ein JSON-STRING — parse ihn
else:
print(msg.content) # einfache Antwort, kein Tool nötigZwei Dinge beißen hier viele. Erstens ist function.arguments ein JSON-String, kein Dict — du json.loads ihn immer. Zweitens kann sich das Modell entscheiden, kein Tool aufzurufen; dann ist tool_calls leer und content enthält eine normale Antwort. Verzweige auf beides. Das ist identisch, ob du model auf claude-sonnet-4-6 oder gemini-2.5-pro setzt; Brievio reicht den Request an das echte First-Party-Modell durch und gibt native Tool-Calls zurück — es formt sie nicht um und täuscht sie nicht vor.
Schritt 2 — die Multi-Turn-Schleife
Jetzt verdrahte den Hin- und Rückweg. Worauf es bei der Form ankommt: hänge die Assistant-Nachricht genau so an, wie sie zurückkam (sie trägt die Call-IDs), dann pro Call eine tool-Nachricht, die jeweils ihre tool_call_id zurückspiegelt. Stimmt eine ID nicht, gibt der nächste Request einen 400 zurück. Hier ist die gesamte Schleife, lauffähig für beide Anbieter aus einer einzigen Funktion:
# Die Multi-Turn-Schleife: Modell fragt -> du führst die Funktion aus ->
# du speist das Ergebnis zurück -> Modell schreibt die finale Antwort.
def run_get_weather(city: str, unit: str = "celsius") -> dict:
# Deine echte Implementierung: ein HTTP-Call, ein DB-Lookup, was auch immer.
return {"city": city, "temp": 18, "unit": unit, "sky": "clear"}
TOOL_IMPLS = {"get_weather": run_get_weather}
def answer(question: str, model: str) -> str:
messages = [{"role": "user", "content": question}]
while True:
resp = client.chat.completions.create(
model=model,
messages=messages,
tools=tools,
tool_choice="auto",
)
msg = resp.choices[0].message
# Kein Tool angefordert -> das ist die finale Antwort. Fertig.
if not msg.tool_calls:
return msg.content
# 1. Hänge den Assistant-Turn GENAU SO an, wie er zurückkam (er trägt die
# tool_call-IDs, auf die sich die nächsten Nachrichten beziehen müssen).
messages.append(msg)
# 2. Führe jede angeforderte Funktion aus und hänge pro Call eine tool-Nachricht
# an, die die passende tool_call_id zurückspiegelt.
for call in msg.tool_calls:
fn = TOOL_IMPLS[call.function.name]
args = json.loads(call.function.arguments)
result = fn(**args)
messages.append({
"role": "tool",
"tool_call_id": call.id, # MUSS zur ID des Calls passen
"content": json.dumps(result), # Ergebnis als String
})
# 3. Schleife: das Modell sieht nun die Tool-Ausgabe und macht weiter.
# Dieselbe Funktion arbeitet für beide Anbieter hinter der einen base_url:
print(answer("What's the weather in Tokyo?", "claude-sonnet-4-6"))
print(answer("What's the weather in Tokyo?", "gemini-2.5-pro"))Dieses while True ist der Motor jedes Agenten, den du je benutzt hast. Ein Modell kann Tools verketten — search aufrufen, das Ergebnis lesen, dann get_details auf den besten Treffer aufrufen, dann antworten — und die Schleife handhabt beliebige Tiefe ohne Sonderfälle. Füge einen Turn-Zähler als Leitplanke hinzu, damit ein verwirrtes Modell nicht ewig kreisen kann; 8–10 Runden sind für die meisten Apps eine vernünftige Obergrenze.
Eine ehrliche Einschränkung zur Portabilität: Das Protokoll ist bei Claude und Gemini identisch, aber das Verhalten ist kein Klon. Verschiedene Modellfamilien wählen unterschiedliche Tools, formulieren Argumente anders und unterscheiden sich darin, wie bereitwillig sie aufrufen, statt aus vorhandenem Wissen zu antworten. Der Code ändert sich nicht; das Urteilsvermögen schon. Teste deine Prompts gegen jedes Modell, auf dem du ausliefern willst, statt anzunehmen, dass sich eines perfekt auf das andere überträgt.
Schritt 3 — parallele Tool-Calls
Wenn eine Frage mehrere unabhängige Lookups braucht — das Wetter von drei Städten, der Bestand von fünf SKUs — kann ein leistungsfähiges Modell alle Calls in einem einzigen Assistant-Turn zurückgeben. Du führst sie aus (nebenläufig, falls die Arbeit I/O-gebunden ist) und gibst pro ID eine tool-Nachricht zurück, bevor du erneut fragst:
# Parallele Tool-Calls: ein Assistant-Turn kann mehrere Funktionen auf einmal
# anfordern. Du führst sie aus (gern nebenläufig) und gibst pro Call-ID eine
# tool-Nachricht zurück. Ob ein Modell Calls bündelt, variiert — iteriere also
# immer über die Liste, statt genau einen Call anzunehmen.
messages = [{"role": "user",
"content": "Compare the weather in Tokyo, Paris and Cairo."}]
resp = client.chat.completions.create(
model="claude-sonnet-4-6",
messages=messages,
tools=tools,
tool_choice="auto",
)
msg = resp.choices[0].message
messages.append(msg)
# msg.tool_calls kann nun DREI get_weather-Calls mit verschiedenen IDs enthalten.
from concurrent.futures import ThreadPoolExecutor
def handle(call):
args = json.loads(call.function.arguments)
result = TOOL_IMPLS[call.function.name](**args)
return {
"role": "tool",
"tool_call_id": call.id,
"content": json.dumps(result),
}
with ThreadPoolExecutor() as pool:
tool_msgs = list(pool.map(handle, msg.tool_calls or []))
messages.extend(tool_msgs) # ALLE Ergebnisse anhängen, bevor der nächste Request geht
final = client.chat.completions.create(
model="claude-sonnet-4-6", messages=messages, tools=tools,
)
print(final.choices[0].message.content)
# Hinweis: Gibt ein Modell Calls einzeln statt gebündelt zurück, erledigt die
# Schleife aus dem vorherigen Snippet das gratis — sie läuft einfach mehr Runden.Hier unterscheiden sich Modellfamilien am stärksten, also verdrahte keine Annahme fest. Ob ein bestimmtes Modell parallele Calls in einem Turn ausgibt oder sie über mehrere Turns einzeln abarbeitet, variiert nach Familie und manchmal nach Request. Die Lösung ist simpel und steht schon im Code oben: iteriere über tool_calls und lass die Schleife bei Bedarf mehr Runden laufen. Code, der über die zurückgegebene Liste iteriert, ist in beiden Fällen korrekt; Code, der genau einen Call annimmt, ist der Bug. Ebenso ist die strikte Schema-Durchsetzung (garantiert gültiges JSON, abgelehnte Extra-Keys) nicht einheitlich — validiere Argumente serverseitig weiter, egal welches Modell sie erzeugt hat.
Warum eine base_url der eigentliche Gewinn ist
Ohne Gateway bedeutet die Unterstützung von Claude und Gemini zwei SDKs, zwei Auth-Schemata, zwei Payload-Formen und zwei Sätze von Tool-Ergebnis-Verkabelung — Anthropics tool_use/tool_result-Content-Blocks auf der einen Seite, Googles function-call-Parts auf der anderen. Hinter Brievios OpenAI-kompatiblem Endpunkt sprechen beide den Chat-Completions- tools-Dialekt, den du oben gesehen hast, sodass ein A/B-Test zwischen Modellen ein Ein-Zeilen-Diff ist und deine Tool-Schicht nur einmal geschrieben wird. Der vollständige Request/Response-Vertrag — inklusive der Tool-Felder — steht in den Chat-Completions-Docs, und die Live-Liste der Modelle mit den exakten IDs findest du auf der Modelle-Seite.
Es lohnt sich, das klar zu sagen: Der Wert hält nur, wenn das Modell am anderen Ende das echte ist. Tool-Calling ist tatsächlich ein nützliches Echtheitssignal — ein echtes Flaggschiff erzeugt zuverlässig wohlgeformte tool_calls mit sinnvollen Argumenten auf nicht-trivialen Schemata, wo ein billiger Ersatz das JSON eher verpfuscht oder das Tool ignoriert. Brievio liefert die echten First-Party-Modelle aus (Claude Sonnet 4.6, Opus 4.7, Gemini 2.5 Pro/Flash und weitere), respektiert natives Tool-Calling und meldet ehrliche Token-Zahlen; willst du das selbst bestätigen, siehe wie du prüfst, ob dein Claude wirklich Claude ist.
Eine kurze Praxis-Checkliste
- Parse die Argumente, immer.
function.argumentsist ein String;json.loadsihn und validiere vor der Nutzung. - Spiegle die IDs zurück. Hänge die Assistant-Nachricht wortgetreu an, dann pro Call eine
tool-Nachricht mit der passendentool_call_id. Alle davon vor dem nächsten Request. - Iteriere über die Liste. Nimm nie einen Call pro Turn an — handhabe null, einen und viele. Diese eine Gewohnheit lässt parallele und sequenzielle Modelle beide einfach funktionieren.
- Deckle die Runden. Ein Turn-Zähler verhindert eine endlose Tool-Calling-Spirale und begrenzt deine Kosten.
- Vertraue nichts. Argumente sind Modell-Output. Validiere Typen, Wertebereiche und Berechtigungen genau so wie Benutzereingaben.
Bekommst du diese fünf richtig hin, hast du einen Tool-nutzenden Agenten, der unverändert über Claude und Gemini läuft, mit der Option, pro Request nach Kosten oder Fähigkeit zu routen. Beachte, dass fehlgeschlagene 4xx/5xx-Calls auf Brievio nicht abgerechnet werden, sodass die unvermeidlichen Schema-Tuning-Iterationen, während du Tool-Definitionen geradeziehst, kostenlos sind. Wenn du bereit bist auszuwählen, welche Modelle du hinter deine Tools stellst, geht der Leitfaden zur Gateway-Auswahl die Abwägungen durch, die in der Produktion wirklich zählen.