Il problema non è ricordare una regola vecchia
Un agente prepara una richiesta in due passaggi. Prima controlla se la quantità è ammessa, poi calcola una proposta usando un coefficiente. Fra i due passaggi il catalogo delle regole cambia. Ogni lettura usa la versione più recente disponibile, ma il risultato può combinare un’approvazione vecchia con un calcolo nuovo: una risposta che nessuna versione completa delle regole avrebbe prodotto. In questo articolo capiremo perché “leggi sempre l’ultima versione” non equivale a “svolgi un compito coerente”. Un piccolo workflow eseguito ci permetterà di distinguere il problema dalle regole scadute e dai permessi di esecuzione.
Abstract. Consideriamo una quantità sintetica di 900 unità. La versione v1 ammette fino a 1000 e usa coefficiente 1; v2 ammette fino a 600 e usa coefficiente 0,5. Enumeriamo tre collocazioni di un aggiornamento rispetto a due letture, confrontando accessi alla versione corrente e uno snapshot copiato alla prima lettura. Il primo metodo produce un risultato ibrido in una delle tre sequenze; il secondo non lo produce in nessuna. Questi sono conteggi di sequenze costruite, non tassi di errore osservati. Un controllo separato mostra che una versione coerente può comunque non essere più valida al momento di un’azione.
Tre significati diversi di “corretto”
Chiamiamo coerenza di lettura l’uso di una medesima versione delle regole pertinenti durante il compito. Chiamiamo aggiornamento il rapporto con la versione attualmente pubblicata. Chiamiamo autorizzazione la condizione che permette di eseguire una determinata azione in un momento e con certi permessi. Sono proprietà diverse: un calcolo può essere coerente ma vecchio, oppure usare dati recenti e tuttavia combinarli male. Anche un risultato coerente e recente può essere vietato per altri motivi. Tenere distinti questi livelli evita di attribuire a uno snapshot una garanzia che non possiede.
Per rendere l’esempio verificabile eliminiamo la generazione linguistica: il nostro “agente” è un workflow deterministico con due funzioni. Non interroga un LLM, non usa una base dati e non invia richieste a servizi esterni. Questo non riproduce tutte le cause di errore di un agente reale; isola l’interazione fra letture e aggiornamenti. I valori non rappresentano una politica aziendale o finanziaria: sono unità convenzionali e coefficienti sintetici. Possiamo quindi calcolare l’intera tabella dei risultati senza inventare clienti, operazioni o percentuali di affidabilità.
Due versioni complete e una combinazione inesistente
La regola è una coppia: limite L e coefficiente f. Il primo passaggio restituisce un booleano, vero se q≤L. Il secondo calcola un valore proposto y=qf, anche quando il controllo è negativo: in quel caso y è soltanto un intermedio diagnostico, non un ordine da eseguire. Per q=900, v1 produce (vero,900) e v2 produce (falso,450). Scrivere la coppia intera conta, perché ciascuna componente da sola sembra plausibile. Il valore 450 esiste sotto v2, ma sotto quella versione la richiesta da 900 non è ammessa.
Immaginiamo che la prima lettura avvenga quando è corrente v1. Il controllo 900≤1000 restituisce vero. Subito dopo viene pubblicata v2; il secondo passaggio legge il nuovo coefficiente 0,5 e calcola 450. Ogni funzione ha applicato correttamente la regola che ha letto, ma il risultato congiunto (vero,450) non appartiene né a v1 né a v2. Non serve un’allucinazione del modello per ottenere l’errore: basta una politica di lettura che consenta di cambiare base normativa fra due passaggi dipendenti.
La proprietà che desideriamo è più chiara se la scriviamo come esistenza di una versione unica. Per i dati usati nelle decisioni deve esistere un identificatore v tale che entrambe le letture siano state risolte su v. Nel nostro esempio possiamo anche controllare se la coppia appartiene all’insieme {(vero,900),(falso,450)}, ma questo test sui valori non basta in generale: due versioni diverse potrebbero produrre casualmente lo stesso numero. Per un sistema reale serve provenienza delle letture, non soltanto confrontare l’output con alcuni valori attesi.
Enumerare l’aggiornamento, non attribuirgli una probabilità
Con due letture e un aggiornamento consideriamo tre sequenze seriali: aggiornamento prima della prima lettura, fra le due letture, dopo la seconda ma prima di una eventuale azione. Se leggiamo sempre il corrente, otteniamo rispettivamente v2-v2, v1-v2 e v1-v1. Soltanto la sequenza centrale è ibrida. Non diciamo che l’agente abbia probabilità di errore del 33%: non abbiamo definito una distribuzione dei tempi e non abbiamo osservato traffico reale. Abbiamo contato un caso su tre posizioni scelte, che è sufficiente a mostrare l’esistenza del problema.
Questa enumerazione ha confini precisi. Assumiamo una pubblicazione atomica della coppia limite-coefficiente, letture seriali e nessuna modifica ai contenuti di una versione già pubblicata. Se l’editore pubblicasse i due campi separatamente, potrebbe introdurre uno stato incoerente già all’origine. Se consentisse di cambiare il contenuto di v1 mantenendo lo stesso identificatore, “fissare v1” non fisserebbe nulla. Prima di parlare di snapshot del compito occorrono quindi unità di pubblicazione complete e identificatori che si riferiscano stabilmente a contenuti immutabili.
Fissare uno snapshot alla prima lettura
Nel secondo metodo, alla prima lettura copiamo la versione corrente e i suoi due valori. Tutti i passaggi successivi del medesimo compito usano quella copia, anche se il puntatore globale passa a v2. Quando l’aggiornamento è precedente alla prima lettura fissiamo v2; negli altri due casi fissiamo v1. La sequenza centrale produce ora (vero,900), coerente con v1. Non ha prodotto (falso,450), che sarebbe il risultato aggiornato. Abbiamo ottenuto una proprietà specifica, la coerenza della lettura, senza risolvere automaticamente quella diversa dell’aggiornamento.
La copia nel programma è profonda: copy.deepcopy evita che la nostra variabile sia soltanto un altro riferimento allo stesso dizionario modificabile. Nel caso semplice potremmo conservare direttamente un riferimento a una versione veramente immutabile, risparmiando la duplicazione; qui rendiamo visibile la separazione con una copia. In una base documentale grande non è necessario copiare tutti i testi a ogni compito: un manifesto di versioni, un indice identificato e contenuti indirizzati stabilmente possono definire lo stesso insieme di letture. Queste sono architetture possibili, non sistemi implementati o misurati nel nostro esperimento.
| Aggiornamento prima di | Metodo | Versioni lette | Ammesso | Proposta | Coerente |
|---|---|---|---|---|---|
| 1 | current | v2 / v2 | False | 450 | True |
| 1 | snapshot | v2 / v2 | False | 450 | True |
| 2 | current | v1 / v2 | True | 450 | False |
| 2 | snapshot | v1 / v1 | True | 900 | True |
| 3 | current | v1 / v1 | True | 900 | True |
| 3 | snapshot | v1 / v1 | True | 900 | True |
Nella tabella 1 e 2 indicano i due passaggi, mentre 3 indica dopo entrambe le letture e prima dell’ipotetica azione. True e False sono i booleani del programma: vero e falso, non livelli di confidenza. Guardiamo la riga centrale del metodo current: v1/v2 produce proposta 450 con ammesso vero. La riga snapshot immediatamente successiva mantiene v1/v1 e proposta 900. Le righe senza interferenza mostrano invece gli stessi risultati nei due metodi. Il vantaggio dello snapshot in questo esperimento è selettivo: elimina la combinazione intermedia, non cambia arbitrariamente tutte le risposte.
La versione coerente può essere quella sbagliata per agire
Dopo aver calcolato tutto con v1, prima dell’azione troviamo corrente v2. Il risultato è perfettamente coerente, ma la richiesta da 900 non sarebbe ammessa sotto le nuove regole. Quale comportamento sia corretto dipende dal contratto operativo: alcuni processi consentono di terminare con la versione iniziale, altri richiedono una rivalidazione, altri revocano immediatamente le autorizzazioni precedenti. Non possiamo scegliere questa politica guardando soltanto il risultato numerico. Deve essere esplicita prima dell’esecuzione e tracciata insieme alla versione, invece di diventare una giustificazione successiva.
Nel programma calcoliamo anche un controllo di versione: tutte le letture devono provenire dallo stesso identificatore e quell’identificatore deve essere ancora corrente al punto osservato. È soltanto un filtro diagnostico. Per esempio, la sequenza v2-v2 supera il filtro ma ha ammesso falso: non autorizza l’azione. Una reale autorizzazione richiederebbe anche il risultato dei controlli, i permessi applicabili, lo stato della risorsa e le condizioni specifiche dell’operazione. Un booleano di coerenza non può sostituire questo insieme di decisioni.
C’è poi una seconda corsa temporale: se controlliamo la versione corrente e agiamo molto dopo, potrebbe cambiare nel frattempo. Il nostro esperimento si ferma a un punto di osservazione e non esegue effetti, perciò non dimostra di aver eliminato questa finestra. Un’implementazione dovrebbe legare rivalidazione e registrazione dell’azione con il meccanismo atomico o di concorrenza appropriato, oppure definire un protocollo esplicito fra servizi. Scrivere due istruzioni “controlla, poi esegui” non basta a dimostrarne l’atomicità. La distinzione impedisce di trasformare il piccolo esempio in una promessa di sicurezza.
Il parallelo con le transazioni ha un limite
La documentazione primaria di PostgreSQL 18, sezione 13.2, offre un confronto concreto: in Read Committed due SELECT successive possono vedere snapshot diversi; in Repeatable Read condividono lo snapshot della prima istruzione non di controllo della transazione. Abbiamo letto anche limiti e anomalie di serializzazione. Questo chiarisce la distinzione fra lettura recente e lettura stabile, ma il nostro codice non usa PostgreSQL né misura transazioni. Uno snapshot applicativo distribuito fra strumenti non eredita automaticamente isolamento, rollback o serializzabilità da un database menzionato nella bibliografia.
Un agente potrebbe cercare documenti in un indice, estrarre un campo da un’API e leggere un’autorizzazione da un altro servizio. Un singolo identificatore “versione 7” non rende coerenti queste tre sorgenti se ciascuna evolve indipendentemente. Occorre specificare il manifesto che identifica le versioni compatibili o spiegare quale sorgente stabilisce il confine della decisione. Anche data e ora non bastano se non definiscono come ricostruire una vista effettivamente disponibile. Questa complessità è esclusa dal nostro modello a un solo catalogo: va affrontata prima di estenderne la conclusione a un sistema distribuito.
Ripresa del compito e provenienza delle prove
Se il compito si interrompe dopo il primo passaggio, riprenderlo caricando l’ultima versione può ricreare proprio la miscela che volevamo evitare. Un checkpoint dovrebbe includere l’identificatore fissato, gli input pertinenti e la politica di rivalidazione. Questo non significa conservare indiscriminatamente tutti i dati: bisogna scegliere quanto serve per ricostruire la decisione e rispettare i vincoli applicabili. Se la versione originaria non è più disponibile, il sistema non deve fingere di averla riusata: può ricominciare esplicitamente sotto una nuova versione oppure interrompersi secondo la politica definita.
La provenienza dovrebbe accompagnare anche i frammenti passati al generatore: quale documento, quale revisione, quale indice e quali trasformazioni hanno prodotto il contenuto usato? Fissare i documenti non garantisce che il modello li interpreti bene o che produca lo stesso testo a ogni esecuzione. La riproducibilità dell’output linguistico richiede altri dati e può dipendere dall’infrastruttura. Il nostro risultato riguarda esclusivamente il catalogo delle regole e la sequenza deterministica dei due calcoli. È una proprietà utile proprio perché circoscritta, non una spiegazione universale di tutti gli errori degli agenti.

Controllare codice e limiti della dimostrazione
La funzione run riceve la posizione dell’aggiornamento e un booleano pinned. Parte sempre da v1, cambia il puntatore quando previsto e registra gli identificatori effettivamente usati. Lo snapshot viene creato soltanto alla prima lettura. Il programma prova i sei casi, controlla l’unico risultato ibrido del metodo corrente e verifica l’assenza di ibridi nei tre casi fissati. Salva valori, versioni, stato al punto di azione e controllo di versione nel JSON. Non ci sono thread o attese temporali: la sequenza rende ripetibile un’interferenza che nella realtà sarebbe concorrente.
Per n letture su una copia di S dati, costruire lo snapshot materializzato richiede tempo e memoria proporzionali a S; poi il workflow esegue n accessi. Il nostro esempio contiene due campi e due letture, quindi questi costi sono minimi, ma non li trasformiamo in millisecondi. Un riferimento a oggetti immutabili cambierebbe il costo della copia senza cambiare necessariamente la proprietà logica. In un sistema reale andrebbero misurati memoria delle versioni conservate, tempo di acquisizione della vista, ritardi di pubblicazione e costo delle rivalidazioni, mantenendo separati questi costi dalla qualità delle risposte.
Estendere la prova significa cercare controesempi, non ripetere soltanto il caso favorevole. Con più letture si possono collocare aggiornamenti in ogni intervallo; si possono eliminare versioni, introdurre un indice incoerente o revocare un permesso durante la preparazione. Questi scenari aggiuntivi sono proposte di verifica, non risultati eseguiti qui. La distinzione conta: il JSON contiene soltanto le sei esecuzioni dichiarate. Non abbiamo misurato latenza di un database, accuratezza di un LLM, sicurezza di strumenti o comportamento di utenti reali.
La risposta: stabilità durante il ragionamento, controllo prima dell’azione
Sì, una risposta costruita con letture ciascuna aggiornata può essere incoerente: nel nostro esempio combina un’approvazione di v1 con un coefficiente di v2 e produce una coppia inesistente. Uno snapshot del compito impedisce questo specifico cambio di base e rende ricostruibile il ragionamento. Non decide però se sia lecito usare ancora quella versione quando si passa all’azione. Per questo coerenza, aggiornamento e autorizzazione devono restare tre domande separate. Il risultato non dimostra una funzionalità già disponibile in EL-AI: offre un esperimento riproducibile e una distinzione architetturale da verificare in sistemi concreti.
from experiment import run
for when in [1,2,3]:
for pinned in [False,True]:
x=run(when,pinned)
print(when,pinned,x['read_versions'],x['approved'],x['quote_units'],x['coherent'])
Codice, dati e istruzioni · JSON. Calcoli didattici eseguiti con Python 3.14.0; figure con Matplotlib 3.11.2. Analisi con assistenza AI, senza dichiarare peer review o revisione umana. Copertina originale ImageGen, illustrativa: non documenta persone, sedi o installazioni EL-AI. Fonti consultate il 16 ottobre 2026.

