Chiedete a un modello linguistico che tempo fa a Bologna e vi risponderà, nel migliore dei casi, che non ha accesso a dati in tempo reale. Nel peggiore, inventerà un meteo plausibile. Non è un limite aggirabile con un prompt più furbo: il modello è una funzione che trasforma testo in testo, sigillata rispetto al mondo esterno. Eppure oggi gli assistenti AI prenotano voli, interrogano database e mandano email. Il ponte tra le due cose ha un nome preciso — function calling, o tool use — ed è il mattone fondamentale su cui è costruito tutto ciò che chiamiamo «agente». In questo tutorial lo costruiamo da zero, in Python, senza framework: solo l’SDK del provider e una funzione nostra.
L’idea: il modello non esegue, chiede
Il malinteso più comune è immaginare che il modello esegua le funzioni. Non succede mai. Il meccanismo è più simile a un direttore d’orchestra che non suona nessuno strumento: legge la partitura (la vostra richiesta), decide che serve il violino (lo strumento giusto), e fa un gesto preciso al violinista (il vostro codice). Il suono lo produce il violinista; il direttore ascolta il risultato e decide come proseguire.
In concreto, il protocollo ha quattro passi:
- Voi inviate al modello la richiesta dell’utente più la lista degli strumenti disponibili, ognuno con nome, descrizione e schema dei parametri.
- Il modello, se decide che serve uno strumento, non risponde con testo: risponde con una richiesta strutturata — «chiama
get_weathercon{"city": "Bologna"}«. - Il vostro codice esegue davvero la funzione e rimanda il risultato al modello.
- Il modello legge il risultato e formula la risposta finale in linguaggio naturale — oppure chiede un altro strumento, e il giro ricomincia.
Il loop del function calling: il modello riceve la richiesta, emette una chiamata strutturata (tool_use), il vostro codice esegue e restituisce il risultato (tool_result), il modello compone la risposta. I passi 1 e 2 possono ripetersi più volte in un singolo turno.
Definire uno strumento
Uno strumento è descritto da tre cose: un nome, una descrizione in linguaggio naturale e uno schema JSON dei parametri. La descrizione non è un commento decorativo — è l’informazione che il modello usa per decidere quando chiamare lo strumento. Prepariamo l’ambiente (serve una API key di Anthropic nella variabile ANTHROPIC_API_KEY; il pattern è identico con gli altri provider, cambia solo la sintassi):
pip install anthropic
Poi definiamo lo strumento e la funzione Python che lo implementa. Per rendere il tutorial eseguibile senza chiavi di servizi meteo, usiamo dati finti — nel mondo reale qui ci sarebbe una chiamata HTTP:
import anthropic
client = anthropic.Anthropic()
# Lo schema che il modello vede: nome, descrizione, parametri
tools = [
{
"name": "get_weather",
"description": (
"Restituisce il meteo attuale per una città italiana. "
"Usala quando l'utente chiede che tempo fa o la temperatura."
),
"input_schema": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "Nome della città, es. 'Bologna'",
}
},
"required": ["city"],
},
}
]
# L'implementazione vera: il modello non la vede mai
def get_weather(city: str) -> str:
finti_dati = {
"bologna": "18 °C, sereno, umidità 60%",
"milano": "16 °C, nuvoloso, umidità 75%",
"palermo": "24 °C, soleggiato, umidità 50%",
}
return finti_dati.get(city.lower(), f"Nessun dato disponibile per {city}")
Notate la separazione: lo schema è il contratto pubblico, la funzione è l’implementazione privata. Il modello ragiona solo sul contratto.
Il primo giro: il modello chiede uno strumento
Inviamo la domanda insieme alla lista degli strumenti e osserviamo cosa torna indietro:
messages = [{"role": "user", "content": "Che tempo fa a Bologna?"}]
response = client.messages.create(
model="claude-opus-4-8",
max_tokens=4096,
tools=tools,
messages=messages,
)
print(response.stop_reason) # -> "tool_use"
for block in response.content:
if block.type == "tool_use":
print(block.name) # -> "get_weather"
print(block.input) # -> {'city': 'Bologna'}
Il campo chiave è stop_reason. Quando vale "tool_use", il modello si è fermato apposta: sta aspettando che voi eseguiate la funzione. Nel contenuto della risposta c’è un blocco strutturato con il nome dello strumento, i parametri già estratti dalla frase dell’utente e un identificativo (block.id) che servirà per collegare il risultato alla richiesta — come il numero d’ordine su uno scontrino.
Chiudere il cerchio: il loop completo
Ora la parte che trasforma il meccanismo in qualcosa di utile: un loop che continua finché il modello chiede strumenti e si ferma quando ha la risposta. È il cuore di ogni agente, e sta in una ventina di righe:
def esegui_tool(name: str, tool_input: dict) -> str:
if name == "get_weather":
return get_weather(tool_input["city"])
return f"Errore: strumento sconosciuto '{name}'"
messages = [{"role": "user", "content": "Che tempo fa a Bologna? E a Palermo?"}]
while True:
response = client.messages.create(
model="claude-opus-4-8",
max_tokens=4096,
tools=tools,
messages=messages,
)
# Il modello ha finito: nessun altro strumento richiesto
if response.stop_reason != "tool_use":
break
# 1. Aggiungiamo alla storia la risposta del modello (con i tool_use)
messages.append({"role": "assistant", "content": response.content})
# 2. Eseguiamo ogni strumento richiesto e raccogliamo i risultati
tool_results = []
for block in response.content:
if block.type == "tool_use":
risultato = esegui_tool(block.name, block.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id, # aggancia risultato e richiesta
"content": risultato,
})
# 3. I risultati tornano al modello come messaggio utente
messages.append({"role": "user", "content": tool_results})
# Risposta finale in linguaggio naturale
testo = next(b.text for b in response.content if b.type == "text")
print(testo)
Eseguendo lo script, il modello chiama get_weather due volte — spesso nello stesso turno, con due blocchi tool_use paralleli — e poi compone qualcosa come: «A Bologna ci sono 18 °C con cielo sereno, mentre a Palermo fa più caldo: 24 °C e soleggiato.» La frase finale è generata, ma i numeri dentro arrivano dalla vostra funzione: è questa la differenza tra chiedere al modello di ricordare e dargli il modo di consultare.
Tre dettagli del loop meritano attenzione. Primo: la risposta del modello va sempre riaggiunta alla conversazione prima dei risultati, altrimenti l’API rifiuta la richiesta — la storia deve contenere la domanda dello strumento seguita dalla sua risposta. Secondo: se il modello chiede più strumenti nello stesso turno, tutti i tool_result vanno rimandati in un unico messaggio. Terzo: il loop va sempre limitato in produzione (un contatore di iterazioni massime), perché un modello confuso può teoricamente continuare a chiamare strumenti all’infinito.
Gestire gli errori: dite al modello cosa è andato storto
Cosa succede se la funzione fallisce — città inesistente, API esterna giù, parametro assurdo? La tentazione è gestire l’errore in silenzio. La pratica corretta è l’opposto: raccontarlo al modello, marcando il risultato come errore:
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": "Errore: 'Bolonia' non trovata. Forse intendevi 'Bologna'?",
"is_error": True,
})
Un messaggio d’errore informativo permette al modello di correggersi da solo: ritenta con il parametro giusto o spiega il problema all’utente. È lo stesso principio di un buon collega: se gli dite solo «non funziona» resterà bloccato, se gli dite cosa non funziona troverà un’alternativa.
Due avvertenze di sicurezza, brevi ma non negoziabili. I parametri che arrivano dal modello sono input non fidato: vanno validati come validereste un form compilato da uno sconosciuto, perché il modello può sbagliare o essere manipolato dal testo che legge. E per strumenti con effetti irreversibili — inviare email, cancellare dati, muovere denaro — serve un passaggio di conferma umana prima dell’esecuzione: il modello propone, la persona dispone.
Perché vi riguarda
Il function calling è il punto in cui i modelli linguistici smettono di essere generatori di testo e diventano componenti software: sistemi che leggono i vostri dati veri, interrogano i vostri servizi veri, agiscono. Ogni agente di cui sentite parlare — dagli assistenti di programmazione ai sistemi che gestiscono ticket di supporto — è, spogliato del marketing, il loop che avete appena scritto: modello, strumenti, risultati, ripetere. Anche protocolli più recenti come MCP, che standardizzano il modo in cui gli strumenti vengono scoperti e condivisi tra applicazioni, poggiano esattamente su questo meccanismo. Le venti righe di questo tutorial non sono un giocattolo: sono l’unità minima di tutto quello che viene dopo. Sostituite get_weather con una query al vostro database, e avete già qualcosa che ieri non esisteva.
I commenti sono riservati agli iscritti.
Accedi per commentare