L'agente ha scritto "fatto" ma la funzionalità non funziona: come verificare il lavoro di un agente AI
L'agente scrive "fatto" perché valuta da solo il proprio lavoro e vede soltanto ciò che ha verificato: di solito gli unit test, che non vedono i collegamenti tra moduli, le migrazioni, la configurazione e lo scenario end-to-end. La cura è strutturale, non un prompt: una Definition of Done in AGENTS.md, una verifica E2E obbligatoria, uno script di verifica che sia l'unico a poter mettere il segno di superamento e un valutatore indipendente con contesto pulito.
Perché succede
È una situazione nota a chiunque lavori con agenti AI: l'agente ha scritto "fatto", ma la funzionalità non funziona. L'agente non mente di proposito. Riferisce ciò che è riuscito a verificare e crede sinceramente che basti. Lo scarto tra dichiarazione e realtà è una proprietà dello schema stesso "chi esegue valuta il proprio lavoro", non un difetto di un modello specifico.
Le ragioni sono tre. La prima: i modelli tendono alla sicurezza eccessiva, la fiducia della risposta supera la sua precisione. La seconda: l'agente valuta il lavoro nel contesto stesso in cui l'ha svolto. Si è già convinto che la soluzione sia corretta, e chiedergli "sii più obiettivo" non cambia il contesto. La terza, la più pratica: all'agente è accessibile solo una verifica parziale. Gli unit test diventano verdi in fretta, mentre lo scenario end-to-end non l'ha mai eseguito nessuno.
La cronaca di un "fatto" sbagliato
All'agente è stato affidato il reset della password. Ha cambiato lo schema del database, aggiunto un endpoint, collegato il template dell'email e lanciato gli unit test: tutto verde. In chat è comparso un sicuro "Fatto".
In realtà era accaduto questo:
- la migrazione è caduta a metà e ha lasciato lo schema in uno stato incoerente;
- il template dell'email c'è, ma il server di posta (SMTP) non è configurato nell'ambiente, quindi la mail non parte;
- lo scenario "l'utente clicca il link → inserisce la nuova password → accede" non è stato eseguito nemmeno una volta.
Nessuno di questi problemi poteva essere individuato dagli unit test: il database era mockato, l'invio della posta era mockato e un'esecuzione end-to-end non c'era affatto.
Perché i test verdi non dimostrano che la funzionalità funziona
Uno unit test verifica un modulo in isolamento: le dipendenze sono sostituite da mock, il mondo esterno è disattivato. Questo dà velocità e precisione, ma crea anche un cieco sistematico. Il cieco non è un difetto dei test, ma del loro disegno. Test unitari verdi dimostrano solo che ogni parte funziona da sola, non che le parti funzionano insieme.
Ci sono quattro classi di difetti che uno unit test non vede in nessun caso:
| Classe di difetto | Esempio | Perché lo unit test non lo vede |
|---|---|---|
| Interfacce non compatibili | Un componente restituisce un percorso relativo, l'altro ne richiede uno assoluto | Ciascuno è testato con il proprio mock, che accetta qualsiasi formato |
| Propagazione dello stato | Lo schema è cambiato, ma la cache dell'ORM è ancora quella vecchia | Nello unit test non ci sono né un database reale né una cache |
| Ciclo di vita delle risorse | Descrittori di file e connessioni non si chiudono, con file grandi la memoria perde | Il test lavora su dati piccoli e finisce prima che le risorse si esauriscano |
| Ambiente e configurazione | Manca una variabile d'ambiente, SMTP non è impostato, la migrazione non è stata applicata | Il test non avvia l'applicazione con la configurazione reale |
I livelli di verifica sono annidati: tutto ciò che catturano gli unit test lo catturano anche i test di integrazione, e tutto ciò che catturano i test di integrazione lo cattura anche un'esecuzione end-to-end (E2E). Ogni livello superiore vede strettamente di più. Per questo il diritto di dire "fatto" spetta solo al livello più alto.
Come misurare il problema: il verification gap
Prima di correggere, conta. Prendi gli ultimi 10 compiti chiusi dall'agente. Per ciascuno rispondi a due domande: l'agente ha detto "fatto"? E una verifica indipendente lo ha confermato (hai percorso tu lo scenario, il test E2E è verde)? La quota di disaccordi è il tuo verification gap.
Questo numero è utile per due motivi. Mostra la dimensione del problema senza discussioni del tipo "sì, sembra funzionare". E fornisce una base di partenza: dopo ogni cambiamento nel processo lo ricalcoli su compiti simili e vedi se è migliorato.
Come verificare davvero: tre livelli di validazione
Un paragone utile: un'auto passa per tre stazioni di un'officina e non se ne può saltare nessuna.
- Diagnostica (secondi): sintassi, tipi, linter. Cattura refusi ed errori grossolani.
- Banco di prova (minuti): unit e integration test, l'applicazione si avvia. Il motore gira, ma l'auto è ancora ferma.
- Prova su strada: lo scenario end-to-end così come lo percorre l'utente: via API, browser o CLI, con database e configurazione reali.
Regola di avanzamento: non si va alla prova su strada finché la diagnostica segnala errori, e non si consegna l'auto senza aver superato la prova su strada. Gli unit test verdi sono il superamento della seconda stazione, non della terza.
È comodo raccogliere tutto in un unico punto di ingresso:
# Makefile
check:
ruff check .
mypy app
pytest tests/unit tests/integration
e2e:
docker compose up -d db mailhog
alembic upgrade head
pytest tests/e2e
verify: check e2e
Qui l'E2E avvia un database reale e un server di posta di prova, applica le migrazioni ed esegue lo scenario per intero. Una migrazione rotta o una posta non configurata emergono subito, non presso il cliente.
Definition of Done in AGENTS.md
L'agente non deve decidere da solo che cosa significhi "fatto". Il criterio va scritto nel file di istruzioni, in forma verificabile:
## Definition of Done
Una funzionalità è pronta solo se sono soddisfatti TUTTI i punti:
1. make check è terminato con codice 0
2. make e2e è terminato con codice 0, lo scenario della funzionalità è in tests/e2e
3. Le migrazioni si applicano su un database pulito e si possono annullare
4. Le nuove variabili d'ambiente sono descritte in .env.example
5. In PROGRESS.md è scritto che cosa è stato fatto e come è stato verificato
NON costituiscono prova di completamento:
- "Il codice è scritto e sembra corretto"
- "Gli unit test passano"
- "In locale funziona"
Elencare esplicitamente che cosa non conta come completamento funziona meglio di quanto sembri: l'agente smette di fermarsi alla prima esecuzione verde.
Chi mette il segno di "passing"
Il passo successivo è togliere del tutto all'agente il segno di completamento. Le funzionalità sono conservate in un elenco (per esempio feature_list.json), con i campi "comportamento", "comando di verifica", "stato". Una funzionalità può passare a passing solo uno script: esegue il make check comune, poi il comando di verifica di quella funzionalità e, in caso di successo, registra lo stato insieme all'hash del commit come prova. Se non passa, la funzionalità resta attiva: non esiste un "quasi fatto".
{
"id": "F04",
"behavior": "L'utente riceve un'email con un link e cambia la password",
"verification": "pytest tests/e2e/test_password_reset.py",
"state": "active"
}
C'è una trappola: lo script verifica esattamente ciò che è scritto nel campo verification. Se un controllo architetturale fa parte della Definition of Done ma non del comando della funzionalità, quella falla resta invisibile. Per questo il make check comune deve includere tutti i controlli obbligatori, non solo i test.
Il valutatore indipendente
L'autovalutazione dell'esecutore non è mai una fonte indipendente di verità. La soluzione strutturale è separare i ruoli:
- il pianificatore scompone il requisito in funzionalità con criteri di accettazione e non scrive codice;
- il generatore implementa una funzionalità ed esegue i test, ma non decide se è completa;
- il valutatore emette il verdetto secondo una checklist di criteri.
Il valutatore non deve vedere il ragionamento del generatore, altrimenti eredita la sua sicurezza. Riceve solo il requisito, il diff e i risultati delle esecuzioni. In Claude Code è semplice realizzarlo come subagent, che ha un proprio contesto pulito. Ancora meglio se il valutatore è un altro modello, perché i suoi punti ciechi sono diversi. Il giudizio "sembra a posto" è vietato: ogni conclusione deve rimandare a un file, a una riga o a un log.
Messaggi di errore che l'agente può correggere da solo
Una verifica è utile solo se il suo output porta alla correzione. "Test failed" o un semplice AssertionError non dicono nulla all'agente. Un buon messaggio contiene che cosa si è rotto, perché la regola esiste e come correggerlo:
ERROR arch-check: app/services/refund.py importa app/db/session.py
WHY: i servizi non parlano direttamente con il database, solo tramite i repository
FIX: usa RefundRepository da app/repositories/refund.py
Questo formato trasforma la verifica da divieto a suggerimento: l'agente corregge la violazione senza il tuo intervento.
Dove serve ancora la verifica manuale
L'automazione non elimina la persona, ne restringe il ruolo. La verifica manuale serve dove il criterio è difficile da formalizzare: l'aspetto dell'interfaccia, i testi, la comodità di uno scenario. Falla con una checklist di criteri di accettazione, non con un "clicca un po' e vedi". Se scrivi all'agente lo stesso commento per la seconda volta, è il segnale di trasformarlo in una verifica automatica: un test, una regola del linter o un grep-guard nel make check.
Come l'E2E cambia il comportamento dell'agente prima ancora dell'esecuzione
Il valore di un test end-to-end non sta solo nei bug catturati. Se l'agente sa che il suo lavoro sarà verificato da uno scenario E2E, scrive il codice in modo diverso: pensa al contratto con il componente vicino e non solo alla propria funzione, gestisce i percorsi di errore e non viola i confini dell'architettura, perché la violazione emergerebbe nell'esecuzione. La verifica funziona come un vincolo in ingresso, non solo come un filtro in uscita.
C'è una condizione: l'E2E ha senso quando il sistema ha confini chiari. Su un'architettura a spaghetti un test end-to-end dimostra solo che gli spaghetti funzionano, senza mostrare dove si è rotto il disegno. Per questo prima si fissano i livelli e le regole di dipendenza, e solo dopo si costruiscono gli scenari end-to-end. Gli agenti copiano volentieri i pattern esistenti del repository, anche quelli cattivi, quindi i vincoli è meglio introdurli fin dal primo giorno.
Errori frequenti
- Chiedere all'agente di "controllare con più attenzione". Il contesto resta lo stesso e la valutazione resterà gonfiata.
- Considerare la copertura al 100% una garanzia. La copertura unitaria non copre i collegamenti.
- Rifattorizzare prima di confermare la funzionalità. L'ordine è rigido: correttezza → prestazioni → stile.
- Mockare nell'E2E proprio ciò che si rompe più spesso. Database, migrazioni e configurazione vanno eseguiti davvero nel test end-to-end; le API esterne a pagamento possono essere sostituite da un server di test.
Conclusione
Quando l'agente scrive "fatto" e la funzionalità non funziona, il problema è quasi sempre nel processo di verifica, non nel modello. Misura il verification gap, scrivi la Definition of Done in AGENTS.md, rendi obbligatorio l'E2E, affida il segno di completamento a uno script e aggiungi un valutatore indipendente. Questo schema è illustrato in dettaglio in un corso pratico su un progetto reale.
Domande frequenti
Posso far eseguire all'agente i test E2E da solo?
Sì, se il comando è scritto in AGENTS.md come parte obbligatoria della Definition of Done e l'agente ha accesso agli strumenti necessari. Per affidabilità conviene eseguirlo anche con un hook o in CI, così non si dipende dalla memoria dell'agente.
I test E2E sono lenti: vanno eseguiti a ogni modifica?
No. Lint, tipi e unit test sempre; gli E2E quando una modifica coinvolge più di uno strato e prima di dichiarare completa una funzionalità.
Come si verifica il frontend realizzato dall'agente?
Con un test end-to-end nel browser (per esempio Playwright) che clicca sull'interfaccia reale; l'aspetto visivo con una verifica manuale secondo checklist o con screenshot che il valutatore esamina.
Non basta una revisione del codice al posto del valutatore?
La revisione umana è utile, ma è costosa e non scala sul lavoro autonomo di ore. Un valutatore con rubriche elimina i problemi ovvi prima che arrivino alla persona, che decide sulle questioni controverse.
Questo argomento è trattato in modo approfondito, con pratica, nel corso «Harness Engineering: инженерия надёжности AI-агентов».
- Агент написал «готово», а фича не работает: verification gap и Definition of Done
- Все тесты зелёные, а фича сломана: E2E-тесты и архитектурные правила для AI-агента