Torna al blog

AI per principiantiGuida

Come funziona un agente AI: il ciclo, eseguito senza chiave API

Un agente AI è un ciclo: il modello chiede un tool, il tuo codice lo esegue, il risultato torna indietro. Esegui una demo Python senza chiave, poi puntala a un modello reale.

Di CheapTokens TeamPubblicato il 7 min di lettura

Copertina: il ciclo dell'agente dal modello alla chiamata tool al risultato
In questa pagina
  1. Il ciclo in un'immagine
  2. Eseguilo senza chiave
  3. Punta lo stesso ciclo a un modello reale
  4. Perché ogni giro costa più del precedente
  5. Da dove andare dalle 40 righe

Chiedi a un agente di coding di correggere un refuso e guardi il log scorrere: leggi un file, cerca, leggi un altro file, modifica, esegui i test. Una piccola richiesta, una dozzina di chiamate al modello. Da dove vengono?

Un agente AI è un ciclo. Il tuo programma invia la conversazione al modello; il modello o risponde o chiede un tool ("leggi questo file"); il tuo programma esegue il tool, aggiunge il risultato alla conversazione e chiama di nuovo il modello. Si ripete finché il modello non risponde in testo semplice. Gli agenti di coding come Claude Code, Codex e la modalità agente di Cursor sono costruiti attorno a questo stesso ciclo, con più tool e più attenzione. Qui sotto eseguirai una versione di 40 righe senza alcuna chiave, poi la punterai a un modello reale.

Il ciclo in un'immagine#

Pensa a un manager che non può toccare nulla. Il manager scrive solo note: "per favore portami todo.txt". Un assistente (il tuo programma) va allo scaffale, prende il file e lo mette sulla scrivania. Il manager lo legge e scrive la nota successiva, o la risposta finale.

Questo è tutto il trucco. Il modello non apre mai un file né esegue un comando. Scrive testo che dice quale tool chiamare e con quali argomenti. Il tuo codice fa il lavoro e riporta indietro. Se hai visto function calling, è lo stesso meccanismo, ripetuto finché il lavoro non è finito.

Il ciclo dell'agente: i messaggi vanno al modello, il modello chiede un tool, il programma lo esegue e il risultato torna nei messaggi
Il ciclo: messaggi → modello → chiamata tool → risultato tool → di nuovo il modello

Riesci a mettere un giro del ciclo nell'ordine giusto?

Metti in ordine

Metti in ordine un giro del ciclo dell'agente

  1. Invia il compito e la lista dei tool al modello
  2. Il modello risponde con una chiamata a un tool, non con testo
  3. Il tuo programma esegue quel tool da solo
  4. Il risultato entra nei messaggi come messaggio "tool"
  5. Il modello viene chiamato di nuovo con il risultato, finché non risponde in testo semplice

Eseguilo senza chiave#

Supponiamo che il compito sia: "Quanti elementi ci sono in todo.txt?". Il tool è una funzione read_file che cerca in un piccolo dizionario invece che su disco. E invece di un modello reale c'è uno scriptato che fa sempre le stesse due cose: chiede il file, poi risponde. Basta per vedere ogni componente in movimento. Salvalo come agent_loop.py (Python 3.10+, solo libreria standard):

python
import json, os, urllib.request

NOTES = {"todo.txt": "buy milk\ncall Anna\nsend invoice"}

def read_file(name):
    return NOTES.get(name, "no such file")

TOOLS = {"read_file": read_file}
TOOL_SPECS = [{"type": "function", "function": {
    "name": "read_file", "description": "Read a note by file name",
    "parameters": {"type": "object",
                   "properties": {"name": {"type": "string"}},
                   "required": ["name"]}}}]

def scripted_model(messages):
    if messages[-1]["role"] == "user":      # first lap: ask for the file
        return {"role": "assistant", "content": None, "tool_calls": [{
            "id": "call_1", "type": "function",
            "function": {"name": "read_file",
                         "arguments": "{\"name\": \"todo.txt\"}"}}]}
    return {"role": "assistant", "content": "The file has 3 items."}

def real_model(messages):
    body = {"model": os.environ["LLM_MODEL"], "messages": messages,
            "tools": TOOL_SPECS}
    req = urllib.request.Request(
        os.environ["LLM_BASE_URL"] + "/chat/completions",
        data=json.dumps(body).encode(),
        headers={"Content-Type": "application/json",
                 "User-Agent": "agent-loop-demo/1.0",
                 "Authorization": "Bearer " + os.environ["LLM_API_KEY"]})
    with urllib.request.urlopen(req) as resp:
        data = json.load(resp)
    u = data["usage"]
    print("  usage: in", u["prompt_tokens"], "out", u["completion_tokens"])
    return data["choices"][0]["message"]

def run(task, model, max_steps=8):
    messages = [{"role": "user", "content": task}]
    for step in range(1, max_steps + 1):
        reply = model(messages)
        messages.append(reply)              # keep the model's turn in history
        calls = reply.get("tool_calls")
        if not calls:
            print(f"step {step}: answer -> {reply['content']}")
            return
        for call in calls:
            name = call["function"]["name"]
            args = json.loads(call["function"]["arguments"])
            result = TOOLS[name](**args)    # YOUR code does the work
            print(f"step {step}: tool {name}({args}) -> {result!r}")
            messages.append({"role": "tool", "tool_call_id": call["id"],
                             "content": result})
    print("stopped: max_steps reached")

model = real_model if os.environ.get("LLM_API_KEY") else scripted_model
run("How many items are in todo.txt?", model)

Esegui python agent_loop.py. Senza LLM_API_KEY nel tuo ambiente ottieni il modello scriptato, e lo schermo mostra esattamente due giri:

Output del terminale della demo scriptata: il passo 1 esegue il tool read_file, il passo 2 stampa la risposta
Due giri: una chiamata tool, poi la risposta

Guarda tre righe, perché contengono l'intera idea:

  • messages.append(reply) conserva il turno del modello stesso. Saltalo, e la chiamata successiva contiene un risultato tool che risponde a una richiesta che l'API non ha mai visto; la maggior parte delle API lo rifiuta con un 400.
  • arguments arriva come stringa JSON, quindi passa attraverso json.loads prima di chiamare la funzione.
  • max_steps è il freno di emergenza. Un modello può continuare a chiedere tool all'infinito, quindi ogni agente reale ha un limite come questo.

Punta lo stesso ciclo a un modello reale#

Cambia solo una cosa: chi interpreta il modello. Lo script legge tre variabili d'ambiente, e se è presente una chiave invia lo stesso messages a un endpoint compatibile OpenAI. L'indirizzo CheapTokens è https://api.cheaptokens.dev/v1 (compatibile con OpenAI Chat Completions e Anthropic Messages), e l'id del modello deve essere quello completo dalla documentazione: deepseek-ai/DeepSeek-V4-Flash-0731.

Esegui il ciclo in tre modi

Nessuna chiave

bash
python agent_loop.py

Il modello scriptato recita entrambi i giri. Nulla esce dalla tua macchina.

DeepSeek-V4-Flash

bash
export LLM_BASE_URL=https://api.cheaptokens.dev/v1
export LLM_API_KEY=<your key>
export LLM_MODEL=deepseek-ai/DeepSeek-V4-Flash-0731
python agent_loop.py

Una finestra di contesto di 380K e fino a 32.768 token di output per risposta: spazio per un ciclo lungo.

GLM-5.3-Flash

bash
export LLM_BASE_URL=https://api.cheaptokens.dev/v1
export LLM_API_KEY=<your key>
export LLM_MODEL=zai-org/GLM-5.3-Flash
python agent_loop.py

Un modello leggero con una finestra di contesto di 390K per una prima esecuzione di un ciclo multi-step.

Quello che dovresti vedere è la stessa forma di prima: step 1: tool read_file(...), poi step 2: answer -> ..., con una riga usage: sotto ogni chiamata al modello. La formulazione della risposta sarà diversa da quella scriptata, e un modello chiacchierone può fare un giro in più. È normale: il modello decide, il ciclo obbedisce. Tieni la chiave fuori dallo script stesso; una variabile d'ambiente è il posto sicuro (ecco come memorizzarla).

Le righe usage: sono la parte utile. Aggiungi i numeri in di un'esecuzione più lunga e vedrai dove vanno i soldi.

Perché ogni giro costa più del precedente#

Il modello non ricorda nulla tra una chiamata e l'altra. Ogni giro invia di nuovo l'intera lista messages: il compito, ogni chiamata tool precedente, ogni risultato tool. (La stessa regola rende costosa una chat lunga; chat multi-turno lo mostra su una conversazione semplice.)

Il giro 1 invia un messaggio. Se il ciclo continua, il giro 6 ne invia undici.

Il giro 1 invia solo il compito; il giro 6 invia il compito più cinque chiamate tool e cinque risultati tool
Cosa riceve il modello al giro 1 e al giro 6

Diciamo che un compito reale richieda 10 giri. Inizia con circa 2.000 token (istruzioni, lista dei tool, la tua richiesta), ogni giro aggiunge circa 1.500 (un risultato tool più la nota del modello), e il modello scrive circa 300 token per giro. Il giro 1 invia 2.000, il giro 2 invia 3.500, il giro 10 invia 15.500. La somma della prima parte è 10 × 2.000 = 20.000 token; la crescita aggiunge 1.500 × 45 = 67.500 (45 è 0+1+…+9), quindi l'input sul compito è 20.000 + 67.500 = 87.500 token. L'output è 10 × 300 = 3.000 token.

Il testo del compito è di poche frasi, eppure il lato input è di 87.500 token. A $0.015 per 1M token di input, l'intero compito su DeepSeek-V4-Flash arriva a $0.0016. Ecco lo stesso carico tra i modelli, con OpenRouter come riferimento. In totale, contando input e output insieme, CheapTokens risulta più economico di OpenRouter su questo carico:

Modello Costo OpenRouter
GLM-5.3-Flash $0.0019 $0.0038
DeepSeek-V4-Flash $0.0016 $0.0046
MiniMax-M2.7 $0.0048 $0.0299

87.500 token di input e 3000 token di output in totale. I prezzi per 1M di token sono attuali e cambiano insieme alle nostre tariffe; la colonna OpenRouter mostra i prezzi di riferimento per lo stesso carico di lavoro.

Un prezzo unico a qualsiasi ora del giorno aiuta anche qui: un agente che gira di notte costa come uno a mezzogiorno, e puoi vedere tutti i prezzi sulla pagina dei prezzi. Per una giornata di tali compiti, muovi gli slider:

Calcolatore di costi

Una giornata: 20 compiti da 10 giri ciascuno

Token di input per richiesta: 8750 · Token di output per richiesta: 300 · Richieste al giorno: 200

ModelloAl giornoAl mese
GLM-5.3-Flash$0.0374$1.12
DeepSeek-V4-Flash$0.0323$0.968
MiniMax-M2.7$0.0965$2.90

I campi sono per chiamata al modello: 8.750 è l'input medio di un giro dell'esempio sopra (87.500 ÷ 10), e 200 chiamate sono 20 compiti × 10 giri. Ecco perché le bollette degli agenti riguardano la cronologia, non la domanda. L'esempio a 20 passi approfondisce lo stesso effetto.

Verifica rapida

Il giro 3 di un ciclo dell'agente costa più del giro 1, anche se non hai chiesto nulla di nuovo. Perché?

Mostra risposta

Risposta: La lista dei messaggi ora contiene due chiamate tool e due risultati, e tutto viene inviato di nuovo

L'API è stateless. Ogni chiamata porta con sé la cronologia completa, quindi i token di input crescono a ogni giro mentre l'output resta più o meno lo stesso.

Da dove andare dalle 40 righe#

Lo script sopra è uno schizzo per imparare. Se vuoi la versione lunga, c'è un corso aperto su GitHub, justxor/agent-harness-course (README in russo, verificato il 31 ottobre 2026). È Python 3.10+ puro senza dipendenze, il codice è con licenza MIT, il testo è CC BY 4.0. Dopo il clone, python examples/02_agent_loop.py esegue il ciclo offline con un modello scriptato, e python -m unittest discover -s tests esegue i suoi 19 test senza rete. Il corso continua con la compattazione del contesto, la memoria, i subagenti, l'approvazione umana delle azioni rischiose e il monitoraggio dei costi. Per un modello reale legge LLM_PROVIDER, LLM_MODEL, LLM_API_KEY e, per gli endpoint compatibili OpenAI, LLM_BASE_URL: prendi il valore esatto di LLM_PROVIDER dal suo README.

Quando il ciclo funziona, il passo successivo è una chiave reale e un compito reale: la documentazione ha una prima richiesta in un minuto, e DeepSeek-V4-Flash è un buon modello su cui eseguire il ciclo.