IT ▾

Esempio di chiamata di funzioni API Ollama con SDK OpenAI

La chiamata di funzioni permette al modello di chiedere al tuo codice di eseguire una funzione e poi usare il risultato nella sua risposta. Di seguito trovi un esempio completo con SDK OpenAI contro un endpoint ospitato, più lo streaming e gli errori che causano la maggior parte dei fallimenti.

Aggiornato

Illustrazione di un modello che affida un compito a una funzione e riceve il risultato in un ciclo

Punti chiave

  1. Invia la tua funzione come schema JSON in tools; il modello risponde con tool_calls invece di testo.
  2. Esegui la funzione, aggiungi il risultato come messaggio con role tool e richiama l'API.
  3. Analizza sempre gli argomenti in modo difensivo, perché un modello può restituire JSON non valido o inaspettato.
  4. Le chiamate di funzioni in streaming arrivano in frammenti che devi assemblare per indice.

Com'è fatto il ciclo

Se cerchi un esempio di chiamata di funzioni API Ollama che funzioni contro un endpoint ospitato, la struttura è quella standard OpenAI. Descrivi le tue funzioni in un array tools. Il modello o risponde in testo o restituisce una lista tool_calls che indica la funzione e i suoi argomenti. Il modello non esegue mai nulla. È il tuo codice a eseguire la funzione, inviare l'output e il modello scrive la risposta finale.

Questo comporta un solo round trip di utilizzo di strumenti pari a due chiamate API:

  1. Invia il messaggio utente e le definizioni degli strumenti.
  2. Ricevi un messaggio dell'assistant con tool_calls. Esegui ogni chiamata localmente.
  3. Invia i messaggi originali, il messaggio dell'assistant e un messaggio role: "tool" per ogni chiamata con il relativo risultato.
  4. Ricevi la risposta testuale finale.

Gli esempi usano l'URL di base https://api.ollamaapi.com/v1 e l'ID modello uncensored. L'endpoint è un servizio ospitato, quindi la chiave proviene dalla pagina delle chiavi API. Lo stesso codice funziona anche verso un server Ollama locale se modifichi l'URL di base, la chiave e il nome del modello, purché il modello locale supporti gli strumenti.

Un esempio completo in Python

L'esempio fornisce al modello una funzione, get_order_status, supportata da un piccolo dizionario. Un'applicazione reale interrogherebbe un database o un servizio interno in quel punto.

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)

Tre dettagli sono importanti qui. Il messaggio dell'assistant che contiene tool_calls deve tornare nella cronologia prima dei risultati degli strumenti, altrimenti la richiesta successiva verrà rifiutata o confusa. Ogni messaggio di strumento deve contenere il tool_call_id della chiamata a cui risponde. E la seconda richiesta deve ancora includere tools, perché il modello potrebbe voler chiamare un'altra funzione dopo aver visto il primo risultato.

La stessa richiesta con curl

Il test con curl rimuove l'SDK dalla scena, il che aiuta quando non sei sicuro se un problema sia nel tuo codice o nella richiesta.

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

Una prima risposta riuscita ha finish_reason impostato su tool_calls, un content vuoto o nullo e un array tool_calls. Il campo arguments è una stringa che contiene JSON, non un oggetto annidato, motivo per cui l'esempio Python chiama json.loads.

Il campo tool_choice controlla quanto è forzata la richiesta. auto lascia decidere al modello. Passare un oggetto che nomina una funzione chiede quella specifica funzione. Usa auto a meno che tu non abbia una ragione, perché forzare una chiamata su un messaggio che non ne ha bisogno produce argomenti inventati.

Lo stesso ciclo in Node

La versione Node usa il pacchetto ufficiale openai e segue gli stessi passaggi. Questa gestisce più chiamate per turno con 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 delle chiamate di funzioni

Con stream: true la risposta arriva via SSE e le chiamate di strumenti arrivano a pezzi. Ogni chunk contiene un entry delta.tool_calls con un index. Il primo frammento per un indice contiene l'ID della chiamata e il nome della funzione. I frammenti successivi aggiungono porzioni della stringa arguments. Concatena le porzioni per indice e analizza il JSON solo alla fine dello streaming.

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

Ricorda che una richiesta in streaming mantiene il suo slot di concorrenza fino al termine dello streaming. Una chiave completa permette 8 richieste simultanee e 300 al minuto, una chiave di prova 2 e 60. Se esegui cicli di strumenti per molti utenti in parallelo, conta ogni stream aperto contro il primo numero.

Punti critici e come risolverli

Argomenti non validi o mancanti

Il modello è un modello a pesi aperti e i modelli open a volte restituiscono argomenti che non sono JSON valido, omettono un campo obbligatorio o ne inventano uno extra. Avvolgi json.loads in un blocco try, valida contro il tuo schema e quando fallisce, invia l'errore indietro come messaggio di strumento. Il modello di solito si corregge al turno successivo. Limita il ciclo a poche iterazioni così un fallimento ostinato non può durare per sempre.

Descrizioni troppo vaghe

Il modello sceglie una funzione leggendone nome e descrizione. Una descrizione come "recupera dati" non gli dà nulla su cui lavorare. Indica cosa fa la funzione, cosa significano gli input e includi un valore di esempio nella descrizione del parametro.

Troppi strumenti contemporaneamente

Ogni definizione di strumento conta come token di input su ogni richiesta. A $0,15 per 1M token di input, dieci strumenti da 150 token ciascuno aggiungono 1.500 token, che sono $0,000225 per richiesta, piccoli ma non zero su larga scala. Più importante, un menu lungo rende la selezione meno affidabile. Offri solo gli strumenti rilevanti per il passaggio corrente.

Risultati degli strumenti troppo grandi

Qualunque cosa tu restituisca viene riacquisita come input. Taglia le righe del database o le pagine web grandi ai campi che il modello necessita. La finestra di contesto è di 100.000 token in totale, quindi un risultato molto grande può affollare la conversazione.

Mescolare strumenti con la modalità JSON

La modalità JSON (response_format con json_object) è disponibile, ma usala per la risposta finale, non per i turni in cui vuoi che il modello chiami una funzione. Testa la combinazione sui tuoi prompt prima di farvi affidamento.

Passare da Ollama locale e cosa controllare

Se hai creato il tuo primo ciclo di strumenti verso un server Ollama locale, quasi nulla cambia. Sostituisci l'URL di base con https://api.ollamaapi.com/v1, usa una chiave reale e imposta il modello su uncensored. Gli endpoint nativi Ollama come /api/chat non esistono qui. Solo POST /v1/chat/completions e GET /v1/models sono disponibili, quindi il codice scritto per l'API nativa deve usare la chiamata in stile OpenAI.

Prima di rilasciare, esegui questa breve lista di controllo:

  • Conferma che la chiave funziona con GET /v1/models.
  • Registra l'intestazione X-Request-Id per ogni chiamata, così un'esecuzione errata può essere tracciata.
  • Monitora X-Balance-USD nelle intestazioni di risposta se esegui loop di strumenti in automatico.
  • Gestisci l'HTTP 429: attendi rate_limited o concurrency, e rispetta Retry-After: 5 in caso di sovraccarico.
  • Imposta un limite massimo alle iterazioni del ciclo.

Un loop di strumenti richiede almeno due richieste per domanda dell'utente, quindi i costi aumentano di conseguenza. Ad esempio, 2.000 loop con 1.200 token di input e 150 token di output per richiesta, due richieste ciascuno, equivalgono a 4.000 richieste, 4,8M token di input e 0,6M token di output. Questo è $0,72 più $0,30, per un totale di $1,02. La pagina prezzi ha le opzioni di ricarica, e la documentazione elenca i campi di richiesta supportati. I nuovi utenti possono provare tutto questo con il credito di prova da $0,50, senza bisogno di carta.

Domande e risposte

L'endpoint supporta la chiamata di funzioni?

Sì. Accetta strumenti e tool_choice, restituisce tool_calls, li invia in streaming come delta e accetta i risultati come messaggi con ruolo "tool".

Perché la seconda richiesta fallisce dopo una chiamata di funzioni?

La causa più comune è un messaggio assistant mancante o un messaggio di strumento senza il tool_call_id corrispondente. Aggiungi prima il turno assistant che contiene tool_calls, poi un messaggio di strumento per ogni chiamata.

Il modello può chiamare più tool in un unico turno?

Può restituire un elenco con più di un elemento: esegui un ciclo su tutti gli elementi e invia un risultato per ciascuno. Non dare per scontato che l'elenco contenga un solo elemento.

Mi serve la libreria Ollama per questo?

No. Gli SDK ufficiali di OpenAI sono sufficienti. Indirizza il base URL all'endpoint e usa il modello uncensored.

La tua chiave è a un modulo di distanza

Crea un account, copia la chiave, modifica l'URL di base. È tutta la configurazione.

Ottieni chiave APILeggi la documentazione