Messaggi: nessuno stato dalla parte del server

Il punto di partenza è una frase sola: «The Messages API is stateless, which means that you always send the full conversational history to the API.» Non esiste un identificatore di conversazione da riprendere: la cronologia la conservi tu e la rispedisci intera a ogni turno. Da qui discende tutto il resto, dalla crescita dei costi turno dopo turno all’importanza del caching.

Il contenuto di un messaggio è un array di blocchi tipizzati, non una stringa: testo, immagini, blocchi di ragionamento, chiamate a tool. Le istruzioni globali vanno nel campo system di primo livello. Sui modelli che lo supportano puoi anche inserire un messaggio con ruolo system a metà conversazione — non può essere il primo elemento di messages — con lo stesso peso del campo di primo livello e un vantaggio operativo: essendo in coda alla storia, non invalida il prefisso già in cache.

La risposta porta uno stop_reason, ed è il campo su cui si costruisce il flusso di controllo. Il modo tipico di sbagliare è ignorarlo e leggere solo il testo: una risposta troncata a max_tokens sembra una risposta finita, e finisce in produzione come tale.

Tool: il modello chiede, l’esecuzione resta tua

«The model never executes anything on its own.» Il modello emette un blocco tool_use con nome e argomenti JSON; tu esegui e rispondi con un blocco tool_result che porta il tool_use_id corrispondente. Il ciclo è un while sullo stop reason:

1. richiesta con l'array tools
2. risposta con stop_reason "tool_use" e uno o più blocchi tool_use
3. esegui, costruisci i tool_result
4. rispedisci: messaggi originali + risposta assistant + tool_result
5. torna a 2 finché stop_reason resta "tool_use"

Tre categorie, e cambiano chi fa il lavoro. I tool definiti da te e quelli con schema Anthropic (fra cui memory, bash, text_editor) li esegui tu. I tool eseguiti dal server — web_search, web_fetch, code_execution, tool_search — girano dall’altra parte e per loro non costruisci mai un tool_result; se il ciclo interno raggiunge il limite di iterazioni ricevi stop_reason: "pause_turn", e la continuazione consiste nel rispedire la conversazione compresa la risposta in pausa.

Un segnale che serviva un tool e non l’hai usato: se stai scrivendo un’espressione regolare per estrarre una decisione dall’output, quella decisione doveva essere una chiamata a tool.

Streaming, thinking e i due dettagli che rompono le integrazioni

Con stream attivo la risposta arriva come server-sent events, in una sequenza fissa: message_start, poi per ogni blocco una tripletta content_block_start, uno o più content_block_delta e content_block_stop, poi uno o più message_delta e infine message_stop. Possono comparire eventi ping ovunque, e i conteggi di token in message_delta sono cumulativi. Attenzione al fatto che un errore può arrivare dentro lo stream dopo un HTTP 200 — per esempio un overloaded_error — quindi il codice di stato da solo non dice se la generazione è riuscita.

Lo streaming non è solo esperienza utente: per richieste lunghe è una scelta di robustezza. La documentazione avverte di preferire streaming o batch per le richieste che superano i dieci minuti, perché le connessioni inattive possono cadere.

Il thinking arriva come blocchi thinking prima dei blocchi di testo, ciascuno con un campo signature. Il vincolo che spezza più integrazioni è questo: «With tool use, every thinking and redacted_thinking block from the assistant turn must be passed back exactly as received, including blocks whose thinking field is empty.» Un client che filtra i blocchi per tipo prima di rispedire — tenendo solo text e tool_use — ottiene un errore 400. E i token di ragionamento sono fatturati come output e contano dentro max_tokens, quindi un max_tokens stretto li fa mangiare dalla risposta.

Caching: la gerarchia decide cosa sopravvive

I prefissi di cache si formano nell’ordine tools, system, messages, e ogni livello costruisce sul precedente: una modifica invalida quel livello e tutti i successivi. Cambiare una definizione di tool butta via tutto, anche con il prompt di sistema identico. Puoi definire fino a quattro punti di interruzione per separare parti che cambiano con frequenze diverse. La durata predefinita è cinque minuti, con l’opzione da un’ora a costo maggiore; le scritture costano 1,25 volte l’input base a cinque minuti e 2 volte a un’ora, le letture un decimo.

Il difetto silenzioso: ogni modello ha una soglia minima di token sotto la quale il blocco non viene messo in cache, e la richiesta viene servita senza caching senza che venga restituito alcun errore. Nessun errore, nessun risparmio. Verificalo leggendo cache_read_input_tokens nella risposta, non guardando il codice.

Realtime o batch, e attraverso quale vendor

La scelta fra sincrono e batch è economica prima che tecnica: la Message Batches API sconta il 50% su ogni token. In cambio: «You can access batch results when all messages have completed or after 24 hours, whichever comes first», i lotti che non completano entro 24 ore scadono, i risultati restano disponibili 29 giorni e ogni richiesta porta un custom_id che è l’unico modo di riappaiare risultato e input. Un lotto vale al massimo 100.000 richieste o 256 MB.

Regola pratica: se nessuno sta aspettando la risposta — valutazioni, arricchimenti notturni, riclassificazioni — passa dal batch. Ma verifica prima il canale: attraverso Amazon Bedrock il corpo delle richieste è lo stesso della Messages API di prima parte, mentre gli endpoint Message Batches non sono supportati. La decisione realtime/batch non è indipendente dalla decisione sul vendor.