NotizieMacroUn esperimento di loop engineering mostra che cicli di feedback e verificatori possono fallire in modo silenzioso

Un esperimento di loop engineering mostra che cicli di feedback e verificatori possono fallire in modo silenzioso

Autore: Towards AI·

Punti chiave

  • L’esperimento ha valutato due componenti di loop engineering: un ciclo di feedback basato su fallimenti reali dei test e una configurazione maker/checker che separava la generazione del codice dal giudizio finale.
  • Un primo run di test non ha mostrato alcun beneficio dal feedback reale perché l’harness dei test nascosti restituiva semplici errori di assertion senza dettagli utili sul fallimento.
  • Dopo aver aggiunto all’harness input falliti, output attesi e risultati effettivi, il feedback reale ha risolto un problema che un loop di nuovo tentativo generico non aveva risolto.
  • Il verificatore che eseguiva test ha accettato erroneamente 3 candidati non corretti su 8, un tasso più alto rispetto a due checker basati su opinioni nel confronto misurato.
  • Il sistema combinato finale ha migliorato i risultati principalmente grazie al loop di feedback, mentre il verificatore ha prodotto una falsa accettazione intercettata solo dalla valutazione sui test nascosti.
Un esperimento di loop engineering mostra che cicli di feedback e verificatori possono fallire in modo silenzioso

Ultimo aggiornamento il 27 luglio 2026 a cura dell’Editorial Team. Pubblicato originariamente su Towards AI.

“Il mio lavoro è scrivere loop.”

La dichiarazione è di Boris Cherny, responsabile di Claude Code presso Anthropic. Cherny ha detto di aver smesso di dare prompt direttamente a Claude e di dedicare ora il suo tempo a progettare i loop che lo sollecitano al posto suo [1]. L’osservazione, insieme ad altri commenti simili, ha contribuito a innescare quest’anno una serie di spiegazioni sul loop engineering [1][2]. Dopo averne lette sei, ne ho costruito uno.

Più precisamente, ho costruito i due componenti che quasi ogni spiegazione descrive, ma che poche sembrano eseguire end to end. Il primo era un ciclo run-until-done: invece di chiedere al modello di fare un nuovo tentativo dopo un errore, gli fornisce i suoi reali fallimenti nei test. Il secondo era una configurazione maker/checker: il modello che scrive il codice non può essere il giudice finale della correttezza del codice.

Questa distinzione conta perché i workflow degli agenti di coding spesso dipendono dalla stessa separazione: un sistema propone una modifica, mentre un altro strumento, una suite di test o un modello decide se la modifica è accettabile. Se il canale di feedback o il verificatore è debole, il loop può apparire automatizzato senza diventare significativamente più sicuro.

Ho implementato entrambi da zero in circa 600 righe di Python, li ho collegati a claude-opus-4-8 e li ho valutati su MBPP+ [3]. MBPP+ è un benchmark EvalPlus di piccoli task di programmazione in Python, utile per isolare il comportamento del loop senza introdurre complicazioni su scala di repository. Il costo totale di tutti gli esperimenti discussi qui è stato inferiore a due dollari. Il secondo componente — quello spesso considerato la metà più sicura perché “esegue davvero i test” invece di fidarsi della parola del modello — ha prodotto nelle mie misurazioni risultati di cui quelle spiegazioni non avevano avvertito.

In sintesi: i due componenti fondamentali del loop engineering sono semplici da collegare e facili da sbagliare in modo silenzioso. Il mio loop di “feedback reale” inizialmente sembrava indistinguibile da tentativi casuali finché non ho trovato un bug nel mio harness di test. Il mio verificatore “sicuro” che eseguiva test aveva un tasso di false accettazioni più alto di un checker che si limitava a chiedere al modello quanto si sentisse sicuro. Costruire il loop è il 20% facile.

Collegare un loop al nulla

Gran parte di ciò che viene scritto sul loop engineering si ferma allo schema di collegamento. Presenta un trigger, un obiettivo verificabile, strumenti, stato e regole di arresto: cinque riquadri con frecce tra loro. Il messaggio implicito è che, una volta collegati i riquadri, il loop funziona.

È come installare un rilevatore di fumo e dichiarare sicura la casa perché il dispositivo è montato sul soffitto e collegato, senza controllare se abbia una batteria funzionante.

Mi sono imbattuto esattamente in questo errore con il mio loop di “feedback reale”. Era collegato all’output effettivo dei test, non a un prompt generico di nuovo tentativo. In teoria, avrebbe dovuto superare chiaramente un loop che riceveva solo il messaggio “era sbagliato, riprova”. Il mio primo run ha mostrato il contrario.

Il grader che valuta il grader

Prima di toccare il loop in sé, ho costruito il componente da cui dipendeva tutto il resto: uno scorer che esegue il codice candidato in un sottoprocesso isolato con un timeout rigido e lo valuta su test nascosti.

Non mi sono fidato dello scorer finché non ha valutato se stesso. Quando riceveva una soluzione notoriamente corretta, doveva passarla. Quando riceveva una soluzione notoriamente errata, doveva fallire e allegare l’errore di assertion. Quando riceveva un loop infinito, doveva essere terminato dal timeout invece di bloccarsi per sempre.

Ho poi validato l’intera pipeline su 75 soluzioni di riferimento MBPP+. Tutte e 75 sono passate. Solo dopo ho considerato affidabile qualunque numero prodotto dal loop.

Il loop che sembrava corretto ma non lo era

Il loop in sé era molto semplice. Generava una soluzione, la valutava e, in caso di fallimento, reinseriva il vero stderr — non un generico “riprova”, ma l’errore effettivo — per un massimo di tre tentativi.

Ho anche costruito un braccio di controllo perché non volevo fidarmi di un numero principale senza una baseline. Il controllo usava lo stesso identico loop, ma sostituiva l’errore reale con un’istruzione generica: “era sbagliato, scrivi una soluzione diversa”. Se il feedback reale non superava chiaramente questo approccio, allora qualcosa nel collegamento era rotto.

Nel primo run su 35 problemi, tutti e tre i bracci erano identici. Non era una prova di un loop funzionante. Era un segnale d’allarme con la forma di un loop.

Invece di concentrarmi sulla metrica principale, ho esaminato i fallimenti e ho trovato il problema: l’harness dei test nascosti di MBPP+ falliva con un semplice AssertionError. Non includeva l’input fallito, il valore atteso o il valore effettivo. Di conseguenza, il “feedback reale” era informativamente identico a “riprova”, perché il modello non riceveva nulla su cui potesse agire.

Ho strumentato l’harness affinché riportasse l’input fallito, l’output atteso e il valore effettivamente restituito dal codice. Sugli stessi 35 problemi in un secondo run, il feedback reale ha recuperato un problema che il braccio generico non riusciva a risolvere, al costo di circa 2.500 token di input aggiuntivi nell’intero run.

Il loop non era rotto. Il segnale a cui era collegato era vuoto. Solo il braccio di controllo lo ha messo in evidenza; la metrica principale da sola non lo avrebbe fatto.

Il verificatore ha fallito in un modo non previsto dalla teoria

Un loop ha bisogno di una regola di arresto, e “il modello dice che ha finito” non è una regola di arresto. Ho quindi costruito un checker che scrive i propri test a partire dalla specifica. Non vede mai i test nascosti e non vede mai il codice della soluzione che sta giudicando. Poi esegue quei test. La policy era accettare solo in caso di superamento completo e rifiutare per impostazione predefinita.

Ho confrontato quel verificatore con tre checker più deboli su 41 candidati prodotti dal mio loop: 33 corretti e 8 errati. La misura era il tasso di false accettazioni, cioè la frequenza con cui ogni checker lasciava passare codice in realtà difettoso.

CheckerFalse accettazioniFalsi rifiuti
Fidarsi di tutto8/8 — 100%0/33 — 0%
Chiedere al modello se è sicuro2/8 — 25%4/33 — 12%
Un secondo modello legge il codice2/8 — 25%5/33 — 15%
Scrive test e li esegue3/8 — 38%1/33 — 3%

Mi aspettavo che il checker che eseguiva i test vincesse nettamente sulle false accettazioni. Non è successo. Ha lasciato passare una frazione più alta di codice errato rispetto a entrambi i checker basati su opinioni.

La ragione contava più del numero. Tutti e otto i candidati errati provenivano da tre problemi con specifiche realmente ambigue. Il checker e il fixer erano lo stesso modello che leggeva la stessa frase ambigua. I test auto-scritti dal checker codificavano quindi la stessa interpretazione errata già presente nel codice sbagliato, permettendo a quel codice di superarli senza problemi.

I checker basati su opinioni hanno “vinto” quel confronto sulle false accettazioni soprattutto perché erano generalmente esitanti. La stessa esitazione è anche il motivo per cui hanno rifiutato erroneamente da quattro a cinque volte più codice corretto.

Eseguire test non è una garanzia automatica contro le false accettazioni. È un tipo diverso di evidenza: una evidenza che include un input specifico, un valore atteso e un valore effettivo invece di un’impressione generale. Questo rende utilizzabile come vero gate il suo tasso di falsi rifiuti del 3%. Un checker che rifiuta il 15% del lavoro valido può sommergere un workflow di nuovi tentativi prima ancora di impedire una cattiva merge.

Combinare i due componenti

La composizione finale esegue prima il loop di feedback. Se fallisce, campiona nuovi candidati e lascia che sia il verificatore — non la fiducia del modello — a decidere cosa inviare. Include anche un percorso esplicito di rinuncia se nulla supera la soglia.

Ho valutato questa configurazione su una porzione held-out di MBPP+ che non avevo usato durante la costruzione di nessuno dei componenti precedenti. La valutazione è stata eseguita sui test nascosti, indipendentemente da qualunque decisione del verificatore.

Il loop ha fatto sostanzialmente tutto il lavoro: ha aggiunto 14,2 punti, recuperato cinque fallimenti single-shot su sei e richiesto circa 10 chiamate API aggiuntive. La fase del verificatore si è attivata esattamente una volta, sull’unico problema che il loop non riusciva a risolvere. Il primo candidato campionato ha superato i propri test auto-scritti, ma ha comunque fallito i test nascosti.

Quella era una falsa accettazione reale, coerente con la modalità di fallimento prevista dalla tabella precedente. È stata intercettata solo perché il runner valutava gli invii rispetto a una fonte di verità che il verificatore non vede mai. Se il giudizio del verificatore fosse stato definitivo, quel bug sarebbe stato rilasciato.

Il costo totale di questa fase è stato di 45 chiamate, pari a circa tredici centesimi.

Dove l’approccio si rompe

Questo approccio funziona quando l’obiettivo è realmente testabile: una funzione con casi di test nascosti, uno schema che valida o non valida, oppure un oracle che il loop non può aggirare a parole. Non funziona quando la specifica stessa è ambigua, perché un checker basato sullo stesso modello può ereditare la stessa interpretazione errata del generatore. In quel caso, la soluzione non è necessariamente un checker più ingegnoso. È una specifica più chiara, oppure un oracle indipendente proveniente da una famiglia di modelli completamente diversa.

Ho inoltre testato tutto questo solo su piccole funzioni MBPP+ autonome. L’esperimento non ha affrontato una grande codebase con dipendenze tra file. Non ho costruito né testato l’isolamento dei worktree per eseguire più loop in parallelo. È un problema reale, ma non era il problema misurato da questo esperimento.

Il loop, inoltre, si ferma a “verificato”. Non decide se applicare automaticamente una modifica o inoltrarla a un essere umano. Questo diventa un problema separato e più difficile non appena il sistema tocca qualcosa con reale accesso in scrittura.

Da dove iniziare

Inizia dal grader. Scrivi l’autotest — soluzione notoriamente corretta, soluzione notoriamente errata e loop infinito — prima di scrivere qualunque logica di loop. Quello script da cinque minuti è la principale protezione contro un numero principale fittizio.

Per una codebase con anche solo un piccolo insieme di unit test, collega poi il loop di feedback e costruisci fin dal primo giorno il braccio di controllo con nuovo tentativo generico. Non fidarti del miglioramento finché non supera “riprova”.

Poi costruisci un secondo checker e confrontalo con il primo. Quando la tabella delle false accettazioni contraddirà ciò che suggeriva la teoria — e potrebbe accadere — il motivo mostrerà perché il loop conta più del modello che lo sostiene.

Riferimenti

[1] Rohan Mistry, “Prompt Engineering Is Dead. Loop Engineering Is Here.,” Towards AI, luglio 2026.

[2] Mehmet Özel, “Loop Engineering for AI Agents : Building Verifiable, Self-Correcting Coding Workflows,” Towards AI, giugno 2026.

[3] EvalPlus, “MBPP+ Dataset,” Hugging Face Datasets.