Costruire un agente non vuol dire scrivere mille righe di codice: vuol dire dare al modello la possibilita' di chiamare funzioni vere – API, database, comandi del sistema – e poi reagire in modo intelligente al risultato. OpenAI lo permette dal 2023 con il function calling; con GPT-5.5 e le structured outputs in modalita' strict il meccanismo e' diventato finalmente affidabile per la produzione.

In questo tutorial costruiamo passo passo, in Python, un piccolo agente che risponde a domande sul meteo italiano. L'agente decide quando chiamare l'API meteo, la chiama davvero, e poi formatta la risposta in linguaggio naturale. E' il pattern che sta dietro ad Operator, ai copilot di voli, alla maggior parte degli assistenti AI moderni.

A chi serve, cosa otterrai, prerequisiti

Questa guida e' pensata per chi:

  • Sa scrivere Python a livello base (variabili, funzioni, dizionari).
  • Ha un account OpenAI con chiave API attiva (anche tier gratuito iniziale).
  • Vuole andare oltre la "chat in Python" e costruire qualcosa di interattivo con il mondo esterno.

Al termine avrai un agente CLI funzionante che risponde a frasi come "Che tempo fa a Bologna oggi?" chiamando davvero un'API meteo pubblica. Lo stesso schema si applica a qualsiasi altra integrazione: API aziendali, database SQL, ticketing, calendari.

Quale modello scegliere e perche'

OpenAI oggi offre tre modelli principali per function calling:

  • GPT-5.5 (consigliato): il top, ragionamento esteso, structured outputs nativo, ottimo a usare 5-20 tool insieme. Ideale se vuoi affidabilita' e qualita'. Costo: ~5$ / 1M input.
  • GPT-5 mini: piu' veloce e barato, perfetto per agenti single-tool o per ambienti latency-sensitive.
  • GPT-4o: ancora valido, ma sostituito gradualmente da GPT-5.5 sui nuovi progetti.

Per questo tutorial useremo GPT-5.5 via Responses API – l'endpoint moderno suggerito da OpenAI, che semplifica gestione di tool e stato.

Le alternative? Claude Sonnet 4.6 e Gemini 2.5 supportano tool calling con schemi compatibili: lo stesso codice si porta con poche modifiche. Per chi vuole zero costi e dato locale, Llama 4 Scout o Mistral Medium 3.5 via Ollama funzionano bene su tool semplici.

Preparazione: chiave API e setup

pip install openai requests

Imposta la chiave API in modo sicuro come variabile d'ambiente:

export OPENAI_API_KEY="sk-..."

Su Windows in PowerShell: $env:OPENAI_API_KEY = "sk-...".

Passo 1: definire la funzione vera

Prima di parlare al modello dobbiamo avere una funzione Python che davvero fa qualcosa. Useremo Open-Meteo, gratis e senza chiave, per evitare complicazioni.

import requests

def get_meteo(citta: str, giorno: str = "oggi") -> dict:
    """Restituisce previsioni meteo per una citta italiana."""
    geo = requests.get(
        "https://geocoding-api.open-meteo.com/v1/search",
        params={"name": citta, "country": "IT", "count": 1},
        timeout=10,
    ).json()
    if not geo.get("results"):
        return {"errore": f"Citta {citta} non trovata"}
    lat, lon = geo["results"][0]["latitude"], geo["results"][0]["longitude"]
    meteo = requests.get(
        "https://api.open-meteo.com/v1/forecast",
        params={
            "latitude": lat,
            "longitude": lon,
            "current": "temperature_2m,weather_code,wind_speed_10m",
            "timezone": "Europe/Rome",
        },
        timeout=10,
    ).json()["current"]
    return {
        "citta": citta,
        "giorno": giorno,
        "temperatura_c": meteo["temperature_2m"],
        "vento_kmh": meteo["wind_speed_10m"],
        "codice_meteo": meteo["weather_code"],
    }

Passo 2: descrivere la funzione al modello

Il modello non legge il codice Python: legge una descrizione JSON Schema. Definiamola in modalita' strict, che obbliga il modello a rispettare lo schema senza inventare campi.

tools = [
    {
        "type": "function",
        "name": "get_meteo",
        "description": "Ottiene il meteo corrente per una citta italiana.",
        "strict": True,
        "parameters": {
            "type": "object",
            "properties": {
                "citta": {"type": "string", "description": "Nome della citta"},
                "giorno": {"type": "string", "description": "oggi o domani"}
            },
            "required": ["citta", "giorno"],
            "additionalProperties": False
        }
    }
]

I tre dettagli importanti per strict mode:

  • strict: true attiva il rispetto rigido dello schema.
  • Ogni proprieta' va elencata in required.
  • additionalProperties deve essere false.

Passo 3: il ciclo agentico

L'agente in Python diventa un piccolo loop: il modello propone una chiamata di funzione, noi la eseguiamo, gli rimandiamo il risultato, lui formula la risposta finale.

from openai import OpenAI
import json

client = OpenAI()

def chiedi_agente(domanda: str):
    risposta = client.responses.create(
        model="gpt-5.5",
        input=[{"role": "user", "content": domanda}],
        tools=tools,
    )

    while True:
        chiamate = [o for o in risposta.output if o.type == "function_call"]
        if not chiamate:
            return risposta.output_text
        nuovo_input = list(risposta.output)
        for call in chiamate:
            args = json.loads(call.arguments)
            risultato = get_meteo(**args)
            nuovo_input.append({
                "type": "function_call_output",
                "call_id": call.call_id,
                "output": json.dumps(risultato),
            })
        risposta = client.responses.create(
            model="gpt-5.5",
            input=nuovo_input,
            tools=tools,
        )

if __name__ == "__main__":
    print(chiedi_agente("Che tempo fa oggi a Bologna? E domani a Palermo?"))

Esegui:

python agente_meteo.py

Tipico output: "A Bologna oggi 18 gradi con vento leggero. A Palermo domani sono attesi 24 gradi e vento moderato dal mare."

Quello che e' successo dietro le quinte: il modello ha visto la domanda, ha capito che servivano due chiamate get_meteo diverse, le ha proposte, il nostro codice le ha eseguite, ha rimandato i due risultati, e il modello ha composto la risposta in italiano.

Passo 4: structured outputs per JSON garantito

A volte non vuoi una frase, vuoi un JSON pulito da passare al frontend o a un altro sistema. OpenAI lo chiama structured outputs. Aggiungi un parametro text.format con uno schema:

schema = {
    "type": "json_schema",
    "json_schema": {
        "name": "ReportMeteo",
        "strict": True,
        "schema": {
            "type": "object",
            "properties": {
                "citta": {"type": "string"},
                "temperatura_c": {"type": "number"},
                "consiglio": {"type": "string"}
            },
            "required": ["citta", "temperatura_c", "consiglio"],
            "additionalProperties": False
        }
    }
}

risposta = client.responses.create(
    model="gpt-5.5",
    input=[{"role": "user", "content": "Riassumi il meteo di Bologna oggi in JSON con un consiglio breve."}],
    tools=tools,
    text={"format": schema}
)
print(risposta.output_text)

Output garantito (sempre): {"citta":"Bologna","temperatura_c":18.4,"consiglio":"Porta una giacca leggera"}.

Varianti e casi avanzati

  • Tool multipli: aggiungi get_meteo, get_traffico, cerca_evento e lasci che il modello decida quale chiamare. Strict mode evita confusione fra parametri.
  • Parallel tool calls: di default il modello puo' chiamare piu' tool in parallelo. Disabilitalo con parallel_tool_calls: False se l'API a valle non e' idempotente.
  • Tool che ritornano grandi quantita' di testo: tronca prima di rispedire al modello, o usa la Files API per allegare risultati lunghi.
  • Logging: salva sempre arguments e output di ogni chiamata: e' il modo migliore per fare debug di agenti che si comportano in modo strano.

Errori comuni e soluzioni

  • "Invalid schema for function: additionalProperties must be false": nello strict mode tutti gli oggetti devono avere additionalProperties: false. Aggiungilo.
  • Il modello non chiama la funzione: la description e' troppo vaga. Riscrivila piu' specifica e cita esempi.
  • Il modello chiama la funzione con parametri sbagliati: usa strict: true e descrivi i valori accettati nel campo description.
  • Loop infinito di chiamate: imposta un contatore massimo (es. 5 iterazioni) e fallisci con grazia.
  • Latency elevata: passa a GPT-5 mini per i tool semplici, oppure usa streaming per mostrare subito qualcosa all'utente.

Quando NON usare function calling

Function calling e' bello, ma non e' la risposta a ogni problema. Se il tuo task e' "leggere un PDF e dirmi cosa contiene", non serve un agente: serve un prompt con il PDF allegato. Se devi fare classificazione strutturata su milioni di righe, conviene la Batch API con structured outputs senza tool. Se l'azione richiede approvazione umana, conviene una pipeline esplicita con una webapp, non un agente autonomo.

Come proseguire

Per andare oltre questa guida ti consiglio: la documentazione ufficiale function calling, il cookbook GPT-5 con esempi piu' complessi, e – se vuoi una orchestrazione piu' strutturata – il framework OpenAI Agents SDK che cuce piu' tool, retries e handoff fra agenti in poche righe. Una volta scritto questo agente meteo, il salto al tuo dominio – CRM, ERP, ticketing, fatture elettroniche – e' una questione di tempo, non di strumenti.