Come assegnare i compiti all'agente AI: una funzione alla volta, lista delle funzioni e criteri di completamento
Il compito si assegna all'agente AI uno alla volta: in lavorazione c'è sempre una sola funzione, con un comportamento osservabile e un comando di verifica eseguibile, mentre tutto ciò che l'agente nota per strada va nel backlog e non viene sistemato subito. Lo stato di tutte le funzioni è conservato in un elenco leggibile dalle macchine (per esempio feature_list.json), e solo lo script di verifica, non l'agente, può portare una funzione allo stato «fatto».
Perché l'agente prende tutto insieme
Chiedete all'agente di realizzare la registrazione. Dopo un paio d'ore il diff contiene dodici file modificati e ottocento righe nuove: il modello dell'utente, la verifica dell'email, il servizio di posta collegato, l'hashing delle password, un middleware rifattorizzato e dei test spostati. Tutto è fortemente collegato, e nessuno scenario funziona fino in fondo. La sessione successiva apre questo codice e non capisce da dove cominciare.
Il problema non è un «modello scadente». Una persona ha il senso del «per oggi basta» e la normale stanchezza: sono questi a limitare la portata del lavoro. L'agente non ha né l'uno né l'altra. Aggiungere «e intanto sistemo la gestione degli errori» gli costa qualche decina di token, ma il costo per il progetto è reale: l'attenzione si disperde, l'accoppiamento cresce e nulla viene completato.
C'è anche un'aritmetica semplice. Sia C la capacità utile del contesto e k il numero di compiti che l'agente tiene aperti contemporaneamente. A ogni compito spetta circa C/k di capacità di ragionamento. Quando questo valore scende sotto quanto serve per completare un compito, non ne viene completato nessuno. Per questo la risposta a come assegnare i compiti all'agente AI non inizia dalla formulazione del prompt, ma da un vincolo: un solo compito in lavorazione.
La regola di una funzione (WIP=1)
WIP sta per work in progress, cioè il numero di compiti in lavorazione. Per l'agente lo si fissa a uno: in ogni momento è attiva esattamente una funzione, e la successiva inizia solo quando quella corrente ha superato la verifica. La regola si scrive nel file di istruzioni del progetto (AGENTS.md o CLAUDE.md), perché l'agente la veda in ogni sessione:
## Regole di lavoro
- In ogni momento nello stato `active` si trova ESATTAMENTE UNA funzione.
- La funzione successiva si inizia solo dopo che quella corrente è passata a `passing`.
- È vietato «di passaggio» rifattorizzare, rinominare, riorganizzare le cartelle
e sistemare bug non correlati. Trovato un problema: annotatelo in `BACKLOG.md` e andate avanti.
- Le modifiche fuori dallo scope della funzione attiva sono una violazione, anche se utili.
L'ultimo punto conta più di quanto sembri. L'agente considera sinceramente utili i suoi «di passaggio», e spesso lo sono. Ma una modifica utile fuori scope mescola due compiti nello stesso diff, e a quel punto non è più possibile annullarli o verificarli separatamente.
BACKLOG.md: dove mettere il «di passaggio»
L'impulso di «sistemare già che ci sono» non si può semplicemente vietare: serve una via d'uscita legittima. Questa è il file BACKLOG.md: l'agente vi annota il problema notato in una o due righe e torna alla funzione corrente. L'informazione non va persa e lo scope non cresce. Senza backlog, la regola di una funzione prima o poi verrà aggirata, perché lasciare in silenzio un bug sembra all'agente peggio di una violazione.
# BACKLOG.md
- [ ] middleware/errors.py: formato degli errori diverso in /auth e /orders
- [ ] tests/: le fixture dell'utente sono duplicate in tre file
- [ ] serve la verifica dell'email in registrazione? — chiedere
Criteri di completamento: «fatto» è un comando
La seconda parte del problema è che l'agente decide da solo quando il compito è finito. «Il codice sembra a posto», «la funzione esiste», «non ci sono errori di compilazione»: sono sensazioni, non fatti. Il criterio di completamento di un compito per l'agente deve essere un comando eseguibile con un esito univoco: passa oppure no.
F01: registrazione utente
verification: pytest tests/e2e/test_auth.py::test_register_flow
state: active
La differenza tra un criterio debole e uno rigoroso si vede bene negli esempi:
| Criterio debole | Criterio rigoroso |
|---|---|
| Il codice compila | pytest tests/e2e/test_cart.py -q passa |
| Non ci sono errori di sintassi | curl a /cart restituisce il codice 200 |
La funzione addToCart esiste | Dopo aver aggiunto un prodotto, GET /cart lo mostra con il totale corretto |
| Sembra corretto | npx playwright test cart.spec.ts passa |
Un buon comando di verifica è deterministico (stesso codice, stesso risultato), si esegue senza l'intervento di una persona e controlla il comportamento dall'esterno, così come lo vedrebbe l'utente o il client dell'API. Se non riuscite a scrivere un comando del genere, è il segnale che il compito è formulato in modo poco chiaro.
Scomposizione: quanto deve essere grande una funzione
La domanda più frequente dopo «una funzione alla volta» è che cosa considerare una funzione. Un compito troppo ampio non si chiude in una sola sessione, uno troppo piccolo si perde nei costi di avvio, di contesto e di verifica.
- Troppo ampio: «implementare il carrello». È una settimana di lavoro e decine di scenari.
- Giusto: «l'utente può aggiungere un prodotto al carrello e vederlo lì». Un solo comportamento osservabile, un solo scenario di verifica.
- Troppo piccolo: «creare il campo quantity nel modello CartItem». È un passo di implementazione, non un comportamento; verificarlo da solo non ha senso.
Un test pratico di grandezza: una funzione = una sessione dell'agente = un commit sensato = uno scenario end-to-end (E2E). Se la funzione non sta in una sessione, si divide ancora. Se per essa non si riesce a scrivere uno scenario separato, è troppo piccola e va unita a quella vicina.
Esempio di scomposizione del compito «carrello»
Il requisito ampio si divide in unità atomiche, ciascuna con un solo comportamento e un proprio comando di verifica:
- Aggiungere un prodotto al carrello e vederlo nell'elenco.
- Modificare la quantità del prodotto: il totale viene ricalcolato.
- Rimuovere un prodotto dal carrello.
- Il carrello dell'ospite viene conservato dopo l'accesso all'account.
- Non si può aggiungere un prodotto non disponibile: deve comparire un errore chiaro.
Controllo finale della scomposizione: nessuna unità deve richiedere di toccare «di passaggio» un'altra. Se per il punto 4 bisogna riscrivere il punto 1, l'ordine o i confini sono stati scelti male.
Lista delle funzioni: feature_list.json
La regola di una funzione mantiene l'ordine dentro la sessione. Ma chi ricorda tra una sessione e l'altra che cosa è già pronto? Una nota in chat del tipo «auth e catalogo fatti, il carrello è quasi pronto ma ci sono bug» alla nuova sessione non dice nulla: che cosa vuol dire «quasi», quali test sono passati, che cosa blocca il pagamento, non è noto. Ogni nuova sessione è come un nuovo guidatore della stessa auto: se gli appunti vivevano solo nella testa del precedente, il nuovo non sa niente.
Per questo lo stato si porta fuori, in un file leggibile dalle macchine. Nel corso è .agent/feature_list.json:
[
{
"id": "F01",
"behavior": "L'utente si registra con e-mail e password e subito entra",
"verification": "pytest tests/e2e/test_auth.py::test_register_flow",
"state": "passing",
"evidence": "a3f9c12: 1 passed in 2.31s"
},
{
"id": "F02",
"behavior": "L'utente aggiunge un prodotto al carrello e lo vede nell'elenco",
"verification": "pytest tests/e2e/test_cart.py::test_add_item -q",
"state": "active"
},
{
"id": "F03",
"behavior": "Pagamento dell'ordine con carta",
"verification": "pytest tests/e2e/test_payment.py -q",
"state": "blocked",
"blocker": "mancano le chiavi di test del gateway di pagamento"
}
]
Ogni voce ha tre campi obbligatori: la descrizione del comportamento (osservabile dall'esterno), il comando di verifica (eseguibile senza persone) e lo stato. Il quarto campo, facoltativo, è evidence: l'hash del commit e l'output della verifica, come prova che il passaggio a passing c'è stato davvero.
Perché JSON e non un elenco Markdown? Lo leggono anche gli script: il pianificatore sceglie la funzione successiva in not_started, il verificatore prende il comando di verifica, il rapporto di sessione si compila automaticamente e la distribuzione degli stati fornisce le metriche di salute del progetto. L'elenco delle funzioni è la spina dorsale dell'harness, non un promemoria.
Quattro stati e transizioni consentite
| Transizione | Quando |
|---|---|
not_started → active | La funzione viene presa in lavorazione; non ce n'è un'altra attiva |
active → passing | Lo script di verifica termina con codice 0 |
active → blocked | Un blocco esterno; la causa è scritta in blocker |
blocked → active | Il blocco è stato rimosso |
Non esistono altre transizioni. In particolare, non c'è un percorso verso passing che aggiri la verifica, né uno stato «quasi pronto».
Il gate passing: l'agente può chiedere, ma non dichiarare
La regola centrale dello schema: l'agente non modifica a mano il campo state. Può lanciare la verifica, ma l'esito lo dichiara lo script. La differenza tra «l'agente ha riferito» e «lo script ha confermato» è la differenza tra un'affermazione e un fatto. Anche queste regole si scrivono nel file di istruzioni:
## Regole dell'elenco delle funzioni
- File: `.agent/feature_list.json`. È l'unica fonte di verità sullo scope.
- Esattamente una funzione nello stato `active`.
- Tu NON modifichi a mano il campo `state`. Lo cambia `scripts/verify.sh`.
- Per portare una funzione in `passing`, esegui `./scripts/verify.sh F02` e ottieni codice 0.
- Se la funzione è bloccata, passala a `blocked` e indica obbligatoriamente `blocker`.
- Le nuove funzioni si aggiungono solo in `not_started` e solo con il comando di verifica.
Lo script stesso controlla su due livelli: prima l'esecuzione generale del progetto (la funzione non deve rompere il resto), poi il comando della singola funzione. Un successo locale con una regressione globale non conta come successo.
#!/usr/bin/env bash
# L'unico componente che ha il diritto di impostare passing.
set -uo pipefail
FID="$1"
FILE=".agent/feature_list.json"
CMD=$(jq -r --arg id "$FID" '.[] | select(.id==$id) | .verification' "$FILE")
if [ -z "$CMD" ] || [ "$CMD" = "null" ]; then
echo "FAIL: nessun comando di verifica per $FID"; exit 2
fi
echo "== controllo globale =="
make check || { echo "FAIL: controllo generale rosso, $FID non può essere passing"; exit 1; }
echo "== controllo della funzione: $CMD =="
if OUT=$(eval "$CMD" 2>&1); then
SHA=$(git rev-parse --short HEAD)
LAST=$(printf '%s\n' "$OUT" | tail -n 1)
TMP=$(mktemp)
jq --arg id "$FID" --arg ev "$SHA: $LAST" \
'map(if .id==$id then .state="passing" | .evidence=$ev else . end)' \
"$FILE" > "$TMP" && mv "$TMP" "$FILE"
echo "PASS: $FID"
else
printf '%s\n' "$OUT"
echo "FAIL: $FID"; exit 1
fi
La regola «non modificare state» scritta nel testo è ancora una richiesta. Per trasformarla in un vincolo, la scrittura nel file si può chiudere in modo meccanico: con un controllo in CI che confronta evidence con la cronologia dei commit, oppure con i permessi dell'agente nello strumento che usate. Il principio è lo stesso di tutto l'harness: una regola importante va rafforzata con un meccanismo.
Come misurare che l'agente porta a termine i compiti
La metrica allettante è il volume di codice per sessione. In pratica funziona al contrario: un diff grande indica più spesso che l'agente si è disperso in più direzioni. Contate le funzioni nello stato passing, non le righe.
Una metrica utile del corso è VCR (Verified Completion Rate): il numero di compiti verificati diviso per il numero di compiti attivati. La regola è semplice: finché VCR è minore di uno, non si attivano nuovi compiti. È una pressione inversa che l'harness esercita sull'agente, e un modo per rendere visibile il non finito con un numero, non con una sensazione.
Errori tipici nell'assegnare compiti all'agente
- Compito senza comando di verifica. «Fai la registrazione» senza scenario: l'agente decide da solo dove fermarsi, e decide male.
- Più funzioni in una sola richiesta. «Fai il carrello e il pagamento» sono due compiti attivi, e nessuno dei due verrà portato a termine.
- Stato solo in chat. Il progresso scritto nella conversazione scompare alla fine della sessione. Deve vivere in un file.
- Autodichiarazione al posto della verifica. «L'agente ha detto che i test passano» non equivale al codice di uscita dello script.
- Nessun backlog. Il divieto del «di passaggio» senza un posto dove annotare ciò che si nota viene aggirato dall'agente.
Quando una funzione alla volta non basta più
Lo schema «una funzione, una sessione, una verifica» funziona bene per un agente solo in un ciclo. Quando i compiti diventano numerosi e compaiono rami indipendenti, lo si estende a un sistema di più cicli: un nodo di pianificazione, nodi di implementazione, un nodo di verifica e archi tra di loro, paralleli, condizionali, con ritorno all'implementazione se la verifica fallisce. Anche lì, però, ogni nodo di implementazione segue la stessa regola: un compito, un criterio di completamento eseguibile, uno stato al di fuori della testa dell'agente. Il grafo non annulla la disciplina di una funzione alla volta, ma si costruisce sopra di essa.
Un avvertimento dello stesso approfondimento: il ciclo ottimizza onestamente la metrica che gli è stata data. Se il criterio di completamento è scelto male, per esempio «ticket chiuso» invece di «problema risolto», l'agente impara a soddisfare il criterio, non il compito. Per questo la formulazione del comportamento e del comando di verifica merita più tempo di quanto sembri.
In breve
In sintesi, come assegnare i compiti all'agente AI: uno alla volta, con un comportamento osservabile e un comando di verifica eseguibile, con un backlog per tutto ciò che si nota per strada e con un elenco delle funzioni in un file, dove il passaggio a «fatto» lo compie lo script, non l'agente. Il freno all'agente lo mette l'harness: da solo non lo inserirà. La regola WIP=1, feature_list.json e verify.sh sono illustrati in pratica nel corso sull'harness engineering, con un progetto end-to-end.
Domande frequenti
Posso dare all'agente più compiti insieme se sono piccoli?
Puoi raccoglierli in un elenco delle funzioni, ma in lavorazione deve esserci un solo compito alla volta. L'agente prende il successivo solo quando quello corrente ha superato la verifica, così ogni compito viene portato a termine.
Che cosa fare se per un compito non è possibile scrivere una verifica automatica?
Di solito significa che il compito è formulato come un'azione e non come un comportamento. Riformulatelo in base a ciò che vedrà l'utente o il client dell'API; per la parte visiva vanno bene i test e2e nel browser.
Chi scrive feature_list.json, la persona o l'agente?
La lista iniziale conviene prepararla insieme: l'agente propone la scomposizione, la persona la corregge. Poi l'agente può aggiungere nuove funzioni, ma solo nello stato not_started e con un comando di verifica.
In che cosa feature_list.json differisce dai task di un tracker?
Il tracker è pensato per le persone, la lista delle funzioni per la lettura da parte delle macchine: ogni voce ha un comando eseguibile e uno stato rigoroso che cambia solo tramite script. Possono essere sincronizzati, ma all'agente serve il file nel repository.
La regola di una funzione alla volta non rallenta il lavoro?
Le sessioni diventano più brevi, ma cresce la quota di compiti portati a termine. Si risparmia il tempo che prima andava nell'analisi di codice a metà fatto che mescolava tre funzioni.
Questo argomento è trattato in modo approfondito, con pratica, nel corso «Harness Engineering: инженерия надёжности AI-агентов».
- feature_list.json для AI-агента: WIP=1, статусы фич и верификация через verify.sh
- Graph Engineering для AI-агентов: когда одного цикла мало и нужен граф задач