Vai al contenuto
New

DeepSeek V4.1 Flash free for 7 days: 10M tokens a day after a top-up of $10 or more. Until Nov 30.

How it works

Agenti AI e tool use: come un modello chiama le funzioni

Come funziona il tool use: il modello restituisce una chiamata strutturata, il tuo programma host esegue il tool e le regole di approvazione decidono cosa è permesso.

Base12 min di letturaDi NotHoly

Un modello da chat produce testo. Un agente è un sistema che gli permette di fare di più: legge una richiesta, decide che gli serve un'informazione o un'azione esterna alla conversazione, chiede a un programma host di fare il lavoro e prosegue con il risultato. Quasi ogni agente si regge su un solo meccanismo, il tool use, chiamato anche function calling.

Questo articolo spiega il meccanismo passo per passo, con la struttura di richieste e risposte della documentazione pubblica delle API. Mostra perché a decidere cosa viene eseguito davvero è il programma host e non il modello, perché le azioni con effetti collaterali richiedono una decisione umana e cosa l'elenco OWASP chiama excessive agency.

Non serve un lab. Verso la fine, un piccolo script locale esegue il ciclo con uno stub al posto del modello, così puoi vedere il flusso di controllo senza inviare nulla a un'API.

Di cosa è fatto un agente

Un agente ha tre parti.

Il modello prende testo in ingresso e produce testo in uscita. Nel tool use può anche produrre una richiesta strutturata che dice "chiama questa funzione con questi argomenti".

L'host è il programma che scrivi o esegui. Invia le richieste al modello, conserva la conversazione, legge l'output e decide cosa farne. L'host contiene il codice reale di ogni tool.

I tool sono le funzioni che l'host è disposto a eseguire per conto del modello: cercare un file, interrogare un database, scaricare una pagina, inviare un messaggio. Ogni tool ha un nome, una descrizione e uno schema per gli argomenti.

Il modello propone e l'host dispone. Quasi tutte le questioni di sicurezza che vedrai più avanti nascono da questa divisione.

Perché serve il ciclo

Un modello senza tool può rispondere solo in base al suo contesto. Chiedigli il tasso di cambio di oggi e tirerà a indovinare o dirà che non lo sa. Dagli un tool meteo e potrà chiedere un valore all'host, riceverlo e rispondere partendo da quel valore.

Il ciclo permette anche più passaggi. Il modello può cercare, leggere un risultato, decidere che non basta, aprire un link trovato e poi riassumere. Ogni passaggio è un'andata e ritorno separata, e l'host li controlla tutti: quanti sono permessi, quali tool vengono proposti e cosa succede quando una chiamata fallisce.

Come funziona una chiamata a un tool

Ogni scambio di tool use segue la stessa sequenza, qualunque sia l'API.

  1. L'host invia al modello la conversazione e un elenco di definizioni di tool.
  2. Il modello risponde con del testo, oppure restituisce una chiamata strutturata a un tool e segna la risposta come ferma in attesa del tool.
  3. L'host legge la chiamata, la controlla ed esegue la funzione corrispondente, se esiste e se la policy lo permette.
  4. L'host aggiunge il risultato alla conversazione, indicando la chiamata tramite il suo ID.
  5. L'host rimanda la conversazione aggiornata. Il modello chiama un altro tool o scrive la risposta finale.

Il ciclo finisce quando il modello restituisce testo semplice, quando l'host raggiunge un limite di turni o quando l'host lo interrompe.

Le definizioni dei tool sono JSON Schema

Una definizione di tool ha un nome, una descrizione scritta per il modello e uno schema di input. Lo schema usa JSON Schema per dichiarare il tipo di ogni argomento e quali argomenti sono obbligatori. Il modello sceglie i tool leggendo le descrizioni, quindi una descrizione vaga produce chiamate sbagliate.

Il modello restituisce una chiamata strutturata

Il modello non esegue nulla. Restituisce dati che indicano la funzione e i suoi argomenti in un oggetto JSON. L'output è strutturato, quindi l'host non deve indovinare il nome di una funzione da un testo libero.

Strutturato non vuol dire corretto. Il modello può nominare un tool che non esiste, o passare un valore che rispetta lo schema ma è sbagliato per la situazione.

L'host esegue il tool

Questo passaggio spetta solo all'host. L'host cerca il nome nel proprio registro. Se il nome manca, restituisce un errore. Se il nome esiste, l'host deve ancora decidere se questa chiamata, con questi argomenti, per questo utente, può partire. Solo allora esegue il codice.

Il risultato torna indietro

Il risultato viene inviato come messaggio che porta l'ID della chiamata originale. Il modello lo legge come dato e decide il passo successivo. Trattalo come dato e non come istruzioni. Una pagina web scaricata può contenere frasi che sembrano comandi. È il cuore della prompt injection, e l'host non deve lasciare che i contenuti restituiti cambino le sue regole.

Struttura di richieste e risposte, con commenti

Le strutture qui sotto seguono due riferimenti pubblici sul tool use, controllati il 2026-11-08. Gli identificatori dei modelli sono segnaposto. I nomi dei campi sono esatti.

Una richiesta con un tool

json
{
  "model": "<model-id>",
  "max_tokens": 1024,
  "tools": [
    {
      "name": "get_weather",
      "description": "Get the current weather for a given location.",
      "input_schema": {
        "type": "object",
        "properties": {
          "location": {
            "type": "string",
            "description": "City and state, e.g. San Francisco, CA"
          }
        },
        "required": ["location"]
      }
    }
  ],
  "messages": [
    { "role": "user", "content": "What's the weather in San Francisco?" }
  ]
}
  • tools è l'elenco completo delle funzioni che il modello può chiamare. Per il modello non esiste nient'altro.
  • name è ciò che il modello restituirà e ciò che l'host usa per scegliere la funzione.
  • description viene letto dal modello, quindi dovrebbe dire cosa fa il tool e quando usarlo.
  • input_schema è JSON Schema. required dice al modello quali argomenti deve fornire.

Una risposta che chiede un tool

json
{
  "stop_reason": "tool_use",
  "content": [
    {
      "type": "tool_use",
      "id": "toolu_01A09q90qw90lq917835lq9",
      "name": "get_weather",
      "input": { "location": "San Francisco, CA" }
    }
  ]
}
  • stop_reason impostato a tool_use significa che il modello aspetta un risultato. Un host che legge solo il testo perde la chiamata.
  • Un blocco con type impostato a tool_use è la chiamata strutturata. Il suo id va rimandato indietro.
  • input dovrebbe rispettare lo schema. Validalo comunque, perché il modello non è un motore di schemi.

Il seguito con il risultato

json
{
  "role": "user",
  "content": [
    {
      "type": "tool_result",
      "tool_use_id": "toolu_01A09q90qw90lq917835lq9",
      "content": "15 degrees Celsius, partly cloudy"
    }
  ]
}
  • tool_use_id deve essere uguale all'id della chiamata. Se non coincidono, l'abbinamento si rompe.
  • Tieni content breve. I dump grandi sprecano contesto e possono portare testo iniettato.

Lo stesso ciclo nel formato function calling

Un secondo riferimento usa nomi diversi per la stessa idea. Il tool ha type impostato a function, e il modello restituisce un elemento function_call il cui campo arguments è una stringa JSON che l'host deve analizzare.

json
{
  "type": "function",
  "name": "get_weather",
  "description": "Retrieves current weather for the given location.",
  "parameters": {
    "type": "object",
    "properties": {
      "location": { "type": "string" }
    },
    "required": ["location"],
    "additionalProperties": false
  },
  "strict": true
}
json
{
  "type": "function_call",
  "id": "fc_12345xyz",
  "call_id": "call_12345xyz",
  "name": "get_weather",
  "arguments": "{\"location\":\"Paris, France\"}"
}
json
{
  "type": "function_call_output",
  "call_id": "call_12345xyz",
  "output": "{\"temperature\":\"25\",\"unit\":\"C\"}"
}

Quel riferimento enuncia la regola in modo diretto: quando il modello chiama una funzione, è la tua applicazione a doverla eseguire e a restituire il risultato. I nomi dei campi cambiano tra endpoint e versioni, quindi controlla prima di copiare una struttura.

Perché è l'host a decidere cosa viene eseguito

Viene facile immaginare il modello che "usa" un tool come una persona usa una calcolatrice. Quell'immagine nasconde il fatto importante. Il modello scrive un testo che descrive una chiamata. L'host decide se quel testo diventa un'azione.

Da qui i tre compiti dell'host. È l'unico posto in cui il codice viene eseguito, quindi una funzione di cancellazione parte solo se la fa partire l'host. È l'unico posto in cui si può far rispettare una policy: una riga nel prompt che dice "non scrivere file" è una richiesta, mentre un tool di scrittura assente o un ruolo di database in sola lettura sono un controllo. E controlla il perimetro, perché sceglie quali tool proporre a ogni turno. Proporre meno tool è il controllo più economico che hai.

La specifica MCP descrive la stessa divisione in termini di protocollo. L'host è l'applicazione LLM, i client sono connettori al suo interno e i server espongono i tool. La specifica dice che gli host devono ottenere il consenso esplicito dell'utente prima di invocare qualsiasi tool, e che le descrizioni dei tool, per esempio le annotazioni, vanno trattate come non attendibili a meno che non arrivino da un server fidato. La pagina della specifica che ho controllato il 2026-11-08 rimanda a una revisione datata 2026-07-28, quindi leggi la revisione attuale prima di costruirci sopra.

Perché gli effetti collaterali richiedono approvazione

Alcune chiamate si limitano a leggere. Una ricerca o una consultazione non cambia nulla fuori dall'host. Altre cambiano qualcosa: inviano un messaggio, modificano un record, spendono denaro o cancellano dati.

Una lettura sbagliata dà una risposta sbagliata che qualcuno può controllare. Una scrittura sbagliata può essere permanente, può raggiungere altre persone e può passare inosservata per un po'. Un pattern sensato quindi divide i tool per effetto e richiede una decisione esplicita di una persona prima di ogni chiamata con effetti collaterali. L'host mostra l'azione in parole semplici con i suoi argomenti reali e aspetta un sì.

Approva la singola chiamata, non la sessione. Un sì dato una volta per "invia email" non dovrebbe coprire in silenzio i cento messaggi successivi con destinatari diversi. L'approvazione fallisce anche quando chi la dà non guarda con attenzione, quindi abbinala ad argomenti visibili, limiti su cosa può fare una singola chiamata e log.

Excessive agency secondo OWASP

La OWASP Top 10 for LLM Applications elenca l'excessive agency come LLM06:2025. La voce descrive una vulnerabilità che lascia partire azioni dannose in risposta a un output del modello inatteso, ambiguo o manipolato, qualunque sia la causa. Allucinazioni e prompt injection, diretta o indiretta, sono inneschi comuni.

Indica tre cause radice: excessive functionality (tool che il compito non richiede), excessive permissions (tool che possono fare più di quanto serve) e excessive autonomy (azioni ad alto impatto senza alcun controllo). Le mitigazioni derivano da queste cause. Proponi solo i tool necessari al compito. Preferisci tool specifici a shell generiche o a fetcher di URL arbitrari. Applica l'accesso minimo nel sistema a valle, per esempio con ruoli di database o scope OAuth ristretti. Esegui le azioni con l'autorizzazione dell'utente stesso. Richiedi l'approvazione umana per le azioni ad alto impatto. Applica l'autorizzazione a valle invece di fidarti della decisione del modello.

Logging, monitoraggio e limiti di richieste servono a contenere i danni. Non prevengono il problema.

Uno script locale del ciclo

Questo script non ha provider, chiamate di rete né segreti. Il "modello" è uno stub fisso. Al primo turno restituisce una chiamata a un tool e, dopo l'arrivo del risultato, del testo. Serve a mostrare dove sta l'host. Sostituisci lo stub con una vera chiamata API nel tuo ambiente e tieni la logica dell'host.

Salvalo come loop_demo.py:

python
# Local sketch: no provider, no network. The "model" is a fixed stub that
# returns the same shape a provider returns for a tool call.
import json

def get_weather(location: str) -> str:
    return json.dumps({"location": location, "temp_c": 15, "sky": "partly cloudy"})

def delete_file(path: str) -> str:
    return json.dumps({"deleted": path})  # never reached in this demo

# The host decides what may run. The model only names a tool.
REGISTRY = {"get_weather": get_weather}          # delete_file is NOT registered
REQUIRES_APPROVAL = {"delete_file"}

def fake_model(messages):
    # Stub: first turn asks for weather, second turn answers with text.
    last = messages[-1]
    if last["role"] == "user" and isinstance(last["content"], str):
        return {"stop_reason": "tool_use",
                "content": [{"type": "tool_use", "id": "toolu_demo_1",
                             "name": "get_weather", "input": {"location": "Lisboa, PT"}}]}
    return {"stop_reason": "end_turn", "content": [{"type": "text", "text": "done"}]}

messages = [{"role": "user", "content": "Weather in Lisboa?"}]
for step in range(5):  # hard cap on turns
    reply = fake_model(messages)
    messages.append({"role": "assistant", "content": reply["content"]})
    if reply["stop_reason"] != "tool_use":
        print("final:", reply["content"][0]["text"])
        break
    results = []
    for block in reply["content"]:
        if block["type"] != "tool_use":
            continue
        name, args = block["name"], block["input"]
        if name not in REGISTRY:
            out, err = f"unknown tool {name}", True
        elif name in REQUIRES_APPROVAL:
            out, err = "approval required", True
        else:
            out, err = REGISTRY[name](**args), False
        print(f"host ran {name}({args}) -> {out}")
        results.append({"type": "tool_result", "tool_use_id": block["id"],
                        "content": out, "is_error": err})
    messages.append({"role": "user", "content": results})

Eseguilo da una cartella che contiene solo questo file:

bash
python -I loop_demo.py

Il flag -I avvia Python in modalità isolata. Ignora le variabili d'ambiente e tiene la cartella corrente fuori dal percorso di import, una buona abitudine per il codice che non hai scritto tu.

Su una macchina Windows 11 con Git Bash e Python 3.12.10, l'esecuzione ha stampato:

text
host ran get_weather({'location': 'Lisboa, PT'}) -> {"location": "Lisboa, PT", "temp_c": 15, "sky": "partly cloudy"}
final: done

Lo stub ha restituito uno stop reason tool_use e una chiamata a get_weather. L'host ha trovato il nome nel registro, l'ha eseguito e ha scritto un tool_result nella conversazione. Lo stub ha poi risposto con del testo e il ciclo è finito. delete_file non è mai partito perché l'host non l'ha mai registrato. Per vedere il ramo dell'approvazione, fai richiedere delete_file allo stub e guarda l'host che rifiuta.

Errori comuni

Proporre ogni tool del codebase. Il modello usa ciò che vede, e lo stesso vale per qualsiasi testo che legge. Parti da un elenco breve e aggiungi tool uno alla volta.

Scrivere descrizioni vaghe. "Fa cose con i file" invita a chiamate sbagliate. Di' cosa legge o scrive il tool e quando non usarlo.

Fidarsi degli argomenti perché hanno passato lo schema. I controlli di schema confermano la forma e non il significato. Un percorso ben formato può comunque puntare fuori dalla cartella che volevi permettere.

Fidarsi dei contenuti restituiti. Il risultato di un tool è un dato. Se lo incolli nel prompt successivo come istruzioni, dai a una pagina web il potere di guidare il tuo host.

Dimenticare il limite di turni. Un ciclo senza tetto può ripetere una chiamata che fallisce finché non costa denaro vero.

Approvare un'intera sessione. Approva le singole chiamate con gli argomenti visibili.

Registrare le cose sbagliate. Registra nomi dei tool, decisioni e risultati con attenzione ai dati personali. Prompt e risposte completi nei log possono contenere dati che non avresti dovuto conservare.

Per andare oltre

Costruisci un ciclo con una vera chiave API e un solo tool in sola lettura, come una funzione che restituisce l'ora attuale. Esamina i corpi di richiesta e risposta e individua il blocco tool_use e il tool_result che restituisci.

Poi aggiungi un tool che scrive su un file di prova in una cartella tua, e mettici davanti un passaggio di approvazione. Chiedi al modello un percorso fuori da quella cartella e controlla che l'host blocchi la chiamata.

Leggi le sezioni sui tool e sulla sicurezza della specifica MCP, e per intero la voce OWASP sull'excessive agency. Associa ogni mitigazione a una riga del tuo codice.

Fonti

Controllate il 2026-11-08. Le pagine di documentazione si spostano, quindi conferma la versione attuale prima di fare affidamento sul nome di un campo.

Fonti

  1. Tool use overview, public API documentation
  2. Function calling guide, public API documentation
  3. OWASP Top 10 for LLM Applications: LLM06:2025 Excessive Agency
  4. Model Context Protocol specification

Altro su Agenti di IA per la sicurezza

Agenti di IA per la sicurezza

Costruisci il tuo hackbot, parte 1: architettura

Il progetto di un agente di bug bounty: un LLM, dei tool, memoria e guardrail, in un loop che gira sulla tua macchina sotto la tua approvazione.

Intermedio3 min di lettura

Agenti di IA per la sicurezza

Costruisci il tuo hackbot, parte 2: scrivi l'agente

Un agente di bug bounty in Python con tool calling sull'API NotHoly: propone comandi di recon, aspetta l'approvazione, legge l'output e tiene le note.

Intermedio3 min di lettura