Cosa fa davvero il function calling negli LLM

Chiedi a un modello “che tempo fa ad Austin in questo momento” senza nessuna connessione con il mondo esterno, e hai due opzioni: dire che non lo sa, o inventare qualcosa. Il function calling offre una terza opzione. Il modello descrive cosa gli serve, passa quella descrizione al tuo codice e aspetta una risposta vera prima di rispondere.

Il nome è un po’ fuorviante. Il modello non chiama niente. Emette un blocco di dati strutturati che dice quale tool vuole e con quali argomenti, e la tua applicazione decide se eseguirlo e cosa fare con il risultato. “Tool use” è il termine più accurato, ed è quello usato nella documentazione di Anthropic; OpenAI e Google parlano quasi sempre di function calling. Il meccanismo è lo stesso sotto entrambi i nomi. Il modello produce solo una richiesta, mai l’effetto collaterale.

Perché il modello richiede i tool ma non li esegue mai

Immagina un cliente al bancone che può indicare un piatto sul menu e dire quante porzioni vuole, ma non entra mai dietro il bancone. Il modello è il cliente: legge il menu che gli hai dato (le definizioni dei tool), sceglie una voce e dice quante porzioni (gli argomenti). Il tuo codice è la persona dietro il bancone che prepara l’ordine, e che può rifiutarne uno che non ha senso.

Questa separazione è l’intero modello di sicurezza. Una tool call è un suggerimento che il tuo codice può validare o rifiutare, esattamente come qualsiasi altro input proveniente da una fonte non attendibile, perché è esattamente ciò che è. Un modello che ha subito un prompt injection da una pagina web ostile può solo chiedere una tool call. Non può saltare la tua validazione e arrivare direttamente al database.

Come definire un tool con JSON Schema

Ogni provider vuole le stesse tre cose per ogni tool: un nome, una descrizione che il modello usa per decidere quando il tool è pertinente, e uno JSON Schema per gli argomenti. Ecco un tool per il meteo, scritto per la OpenAI Chat Completions API:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
weather_tool = {
    "type": "function",
    "function": {
        "name": "get_weather",
        "description": "Get the current temperature and conditions for a city.",
        "parameters": {
            "type": "object",
            "properties": {
                "city": {"type": "string", "description": "City name, e.g. 'Austin, TX'"},
                "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
            },
            "required": ["city"],
        },
    },
}

Dedica più tempo alla descrizione che allo schema. Il modello sceglie un tool confrontando la richiesta dell’utente con le descrizioni della lista che gli hai passato. Una descrizione vaga (“roba sul meteo”) viene scartata a favore di un tool descritto meglio che fa qualcos’altro, oppure si attiva su richieste che non dovrebbe gestire. Scrivila come se dovessi spiegare la funzione a un collega che leggerà esattamente una frase su di essa.

Come funziona il ciclo richiesta-risposta del function calling

Una tool call richiede almeno due richieste. Ecco il giro completo con la API di OpenAI:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from openai import OpenAI
import json

client = OpenAI()
messages = [{"role": "user", "content": "What's the weather in Austin?"}]

response = client.chat.completions.create(
    model="gpt-4o",
    messages=messages,
    tools=[weather_tool],
)
message = response.choices[0].message
messages.append(message)

if message.tool_calls:
    for call in message.tool_calls:
        args = json.loads(call.function.arguments)
        result = get_weather(args["city"], args.get("unit", "celsius"))
        messages.append({
            "role": "tool",
            "tool_call_id": call.id,
            "content": json.dumps(result),
        })

    final = client.chat.completions.create(model="gpt-4o", messages=messages)
    print(final.choices[0].message.content)

La prima risposta non risponde alla domanda. Arriva con finish_reason impostato su tool_calls e nessun contenuto testuale, perché il modello ha deciso che gli serve get_weather prima di poter dire qualcosa di utile. call.function.arguments arriva come una stringa JSON, non come un oggetto già parsato, quindi json.loads non è opzionale. get_weather lo esegui tu (è una normale funzione Python; il modello non ha modo di eseguirla), poi accodi il risultato come messaggio con role: "tool" e il tool_call_id corrispondente.

Solo la seconda richiesta, che ora porta quel risultato, ti restituisce una frase vera e propria. Se accodi il messaggio dell’assistente con il suo tool_calls ma lasci fuori il risultato del tool corrispondente, l’API rifiuta la richiesta successiva invece di indovinare.

In cosa il tool use di Anthropic differisce dal function calling di OpenAI

Stesso meccanismo, nomi dei campi diversi. Se passi da un provider all’altro o li supporti entrambi, i tuoi bug nasceranno da qui:

ConcettoOpenAIAnthropic
Campo con la lista dei tooltools, ogni tool incapsulato in {"type": "function", "function": {...}}tools, ogni tool in piano: {"name", "description", "input_schema"}
Chiave dello schema degli argomentiparametersinput_schema
Richiesta del modelloarray tool_calls sul messaggio, arguments come stringa JSONblocco di contenuto tool_use, input già come oggetto parsato
Invio del risultatonuovo messaggio, role: "tool", tool_call_idnuovo messaggio utente, blocco di contenuto type: "tool_result", tool_use_id
Segnalare una chiamata fallitanessun campo dedicato: metti l’errore in contentis_error: true sul blocco tool_result

La trappola è il formato degli argomenti. Anthropic ti passa input già parsato in un dict Python o in un oggetto JS; OpenAI ti passa una stringa che devi passare tu stesso a json.loads. Codice scritto per uno e puntato sull’altro si importa senza problemi e poi fallisce alla prima tool call. Se supporti entrambi, scrivi una sola funzione di normalizzazione che entrambi i percorsi attraversano prima che le tue implementazioni dei tool vedano gli argomenti.

Come funzionano le chiamate parallele e i cicli multi-step

Entrambe le API possono restituire più di una tool call in un’unica risposta. Chiedi il meteo per tre città e ottieni tre voci in tool_calls, non tre andata-e-ritorno separate. Eseguile (in concorrenza, se sono indipendenti) e accoda tutti e tre i risultati prima della richiesta successiva. Ogni ID di chiamata emesso dal modello ha bisogno di un risultato; ne invii uno su tre e la richiesta fallisce.

Alcuni task richiedono più round: cerca un utente, poi la sua cronologia ordini, poi segnala l’ordine in ritardo. Scrivilo come un ciclo che continua a chiamare il modello ed eseguire quello che chiede finché non arriva una risposta senza tool call. Poi mettigli un tetto. Niente nell’API impedisce a un modello di richiedere lo stesso tool nove volte di fila quando i risultati non lo portano più vicino a una risposta. Un limite fisso, per esempio 10 o 20 iterazioni, trasforma questo in un errore chiaro invece che in una bolletta fuori controllo.

Gli errori più comuni nel function calling

Trattare gli argomenti come input affidabile. args["city"] esce da un modello, il che significa che esce da qualsiasi testo abbia alimentato la conversazione, inclusa una pagina web raschiata o un documento che il modello doveva riassumere. Validalo prima che tocchi una query al database o un comando shell. Un tool che costruisce SQL interpolando gli argomenti del modello ha la stessa superficie di injection di uno che costruisce SQL da un form web.

Fidarsi dello schema come di una garanzia. Il modello è bravo a seguire required ed enum, ma non è perfetto. Prima o poi passerà "unit": "F" invece di "fahrenheit", o salterà un campo obbligatorio. Valida, e restituisci un messaggio di errore chiaro come risultato del tool invece di andare in crash. Dì al modello cosa non andava e spesso può riprovare con argomenti corretti al round successivo.

Lasciare che il modello scelga da solo i tool distruttivi. Se un tool elimina dati o invia un’email, lasciare tool_choice su automatico è la scelta di default sbagliata. Tieni i tool distruttivi fuori dalla lista finché un umano non ha approvato l’azione specifica, oppure rendi l’effetto reversibile (una bozza invece di un’email inviata) e lascia che un passaggio separato la confermi.

Quando non usare il function calling

Ogni tool call aggiunge un andata-e-ritorno al modello più qualsiasi latenza abbia il tool. Se il “tool” restituirebbe solo un fatto statico, metti quel fatto nel prompt o nel contesto recuperato e salta l’andata-e-ritorno; è a questo che serve il retrieval del RAG. E se la decisione è in realtà una classificazione con un insieme piccolo e fisso di esiti (instrada questo ticket a una di quattro code), una chiamata tipizzata e non generativa risponde più in fretta e a costo minore che chiedere a un modello di chat generico di scegliere un tool. È il caso per cui sono costruiti i modelli system one.

Il function calling ripaga il suo costo quando l’azione dipende da dati a runtime che il modello non può avere nel prompt, e l’insieme delle azioni possibili è troppo aperto per codificarlo come una macchina a stati. È anche il ciclo sotto ogni agente CLI di coding: leggi un file, esegui un test, decidi cosa fare dopo, chiama un altro tool.

Inizia con un tool, una funzione Python hardcoded dietro di esso, e un ciclo che si ferma dopo un solo andata-e-ritorno. Metti a posto la validazione degli argomenti e il percorso d’errore prima di aggiungere un secondo tool. La maggior parte dei bug in un setup multi-tool si rivela essere il primo tool che gestisce male argomenti che non si aspettava.

Articoli correlati