Vai al contenuto principale

Il fix definitivo... per la quarta volta. Due sessioni di debugging con un agente AI

11 min di lettura
Claude Code AI Pipeline n8n Airtable Automazione AI Tools

Ho passato due sessioni con Claude Code su una pipeline reale.

Non un esperimento da demo. Un workflow n8n che fa ingestion di annunci di lavoro, li filtra, li classifica con un LLM e salva i risultati in Airtable.

Il sintomo iniziale era semplice: lo scanner girava da giorni, ma la dashboard restava vuota.

Quello che è venuto fuori è diviso in due livelli distinti.

Il primo livello era architetturale: la forma del sistema produceva race condition strutturali, e l’agente continuava a correggere sintomi locali senza uscire dal loop.

Il secondo livello, nella sessione successiva, era diverso: l’architettura giusta era al suo posto, ma i default nascosti dei tool continuavano a far girare il sistema in modo diverso da come pensavamo.

I due livelli richiedono diagnosi diverse. E richiedono un tipo diverso di intervento umano.


Parte 1: Il problema aveva la forma del sistema

Il primo errore: cercare “il bug”

La prima parte della sessione sembrava normale debugging.

Claude ha controllato i run, i workflow n8n, i nodi critici, i file di stato e le metriche. Ogni volta emergeva un problema plausibile.

Prima il parser delle fonti monitorate.

Poi la cache seen_hashes.json, che aveva accumulato migliaia di hash e bloccava quasi tutti i job prima ancora che arrivassero alla classificazione.

Poi il lock file dentro il loop per azienda: la prima azienda creava il lock, le successive venivano bloccate.

Poi save-draft-jobs, che invece di accumulare job sovrascriveva il file.

Poi il finalizzatore che leggeva il file prima che tutte le scritture fossero completate.

Ogni diagnosi era tecnicamente sensata. Il problema è che ognuna era anche parziale.

La frase che si ripeteva era sempre una variante di: ora è chiaro, questo è il vero problema.

Ma dopo ogni “vero problema” compariva un altro livello.

Race condition, timeout e stato condiviso

Il sistema aveva una combinazione poco amichevole:

  • workflow n8n con rami asincroni;
  • scritture su filesystem condiviso tra host e container;
  • file JSON usati come stato intermedio;
  • Code node con timeout del task runner;
  • chiamate LLM lente;
  • scanner che processava molte aziende;
  • classificazione fatta in blocco.

Finché guardi un singolo nodo, riesci a correggere qualcosa.

Sposti il lock.

Aggiungi un delay.

Fai polling sulla dimensione del file.

Introduci un progress counter.

Poi scopri che il contatore non viene incrementato per le aziende senza job.

Lo aggiungi anche in un altro ramo.

Poi scopri che incrementa due volte.

Metti un mutex.

Poi scopri che partono trigger multipli.

Poi scopri che le scritture concorrenti su JSON fanno read-modify-write e si sovrascrivono.

Passi a JSONL con append atomico.

A quel punto sembra di avere risolto la race condition, ma il classify entra in timeout.

Questo è il punto in cui il debug smette di essere un problema di sintassi, regex o nodo sbagliato. Diventa un problema di forma del sistema.

Il momento in cui ho fermato il loop

A un certo punto ho scritto, in sostanza: qui serve una coda.

Non si possono lavorare i job in sync.

Non ha senso processare migliaia di item, aspettare che il sistema cada in timeout e poi capire postmortem dove si è rotto.

E soprattutto non ha senso ripetere ingestion e rielaborazione completa ogni volta.

La soluzione non era un altro delay. Era separare le fasi:

  • ingestion;
  • persistenza dei job raccolti;
  • coda;
  • classificazione a batch;
  • offset per riprendere;
  • test isolati.

La coda file-based con JSONL, offset e file risultati era molto più semplice di Redis per quel caso. Non perché Redis fosse sbagliato in assoluto, ma perché il volume era piccolo e il problema reale era rendere esplicito lo stato.

Ogni batch diventa una nuova esecuzione. Il timeout non grava più sull’intero lotto. Se qualcosa fallisce, non devo rifare tutto lo scan.

Questo è un principio che vale oltre n8n.

Quando un agente AI continua a correggere sintomi locali, il ruolo umano è chiedere se il sistema ha ancora la forma giusta.


Parte 2: L’architettura giusta non basta - poi arrivano i default nascosti

La sessione successiva è partita con la coda al suo posto. L’architettura reggeva.

I problemi di questa seconda sessione erano diversi: non più logica rotta, ma configurazione invisibile, strumenti che mentono silenziosamente, e controllo sui costi che emerge solo a consuntivo.

La distinzione è importante per chi usa agenti AI su sistemi reali:

  • i bug architetturali si vedono quando il sistema non funziona del tutto;
  • i bug di configurazione si vedono quando il sistema sembra funzionare ma non funziona come ci si aspetta.

I secondi sono più insidiosi perché richiedono di sapere cosa cercare.

Chi aveva deciso quale provider stava usando il sistema?

La coda girava. I job venivano classificati. L’architettura reggeva.

Poi ho guardato i log e ho trovato il file anthropic_calls.jsonl: centinaia di chiamate riuscite verso Anthropic Haiku.

Il sistema avrebbe dovuto usare Ollama locale. Zero costi per chiamata, dati nel perimetro locale, architettura coerente con la scelta originale.

Come era successo: durante il debugging dei timeout nella sessione precedente, l’agente aveva sostituito Ollama con Haiku come via rapida. Il campo della request era rimasto llmBody (costruito per Ollama), ma la destinazione era diventata https://api.anthropic.com/v1/messages. Il cambio non era stato surfaced come una decisione.

Quando ho chiesto il conto, la stima dell’agente era stata: circa 20 chiamate.

I log ne mostravano centinaia riuscite. Un fattore 15x.

Il punto non sono solo il costo, in questo caso irrilevante.

Il punto è non sapere cosa sta succedendo dentro il sistema:

  • quale provider sto usando?
  • chi ha deciso il cambio?
  • quante chiamate sto facendo?
  • cosa succede se domani il volume cresce di 100 volte?

Questo è un problema di controllo, non di costo.

Test isolati come igiene mentale

Dopo il rollback completo a Ollama, l’agente ha proposto di verificare che la classificazione funzionasse rilasciando lo scanner completo.

Ho risposto, in sostanza: non ha senso processare 6000 item in loop per scoprire dopo una serata che c’era un problema di timeout.

Se il problema è lo scraping di un portale, analizziamo quel portale.

Se il problema è il parser, usiamo una fixture.

Se il problema è la classificazione, iniettamo job noti nella coda.

Se il problema è la coda, controlliamo offset, batch e risultati.

Questo ha prodotto due script isolati.

test-classify-queue.py: inietta 3 job hardcoded nella coda senza rilanciare lo scanner. Un job IT Manager full remote (atteso: rilevante), uno Technical Account Manager ibrido (atteso: ambiguo), uno Help Desk L1 on-site (atteso: irrilevante).

test-scrape-company.py: testa Jina su una singola career page senza toccare n8n.

La parte più costosa della prima sessione non era stata Haiku. Era stata l’assenza di test isolati. Per troppo tempo il modo di verificare un fix era rilanciare l’intera pipeline, che mescolava tutto: ingestion, normalizzazione, deduplica, geo-filter, persistenza, coda, chiamata LLM, parsing, salvataggio Airtable. Quando il risultato era vuoto, bisognava ricostruire a posteriori dove si fosse perso il dato.

Con gli agenti AI, il costo percepito di un ciclo sbagliato scende abbastanza da sembrare accettabile. Ma proprio per questo rischi di farne molti di più. Se il ciclo è sbagliato, l’agente non ti salva tempo. Automatizza lo spreco.

Docker mentiva in silenzio

Con Ollama rollbackato e i test isolati pronti, lo script di test ha scritto job_classify_queue.jsonl sull’host Linux come utente dedicato. Il volume Docker mappava ./logs/ verso /home/node/logs/ dentro il container. n8n girava come utente node.

Risultato: i file scritti dall’host erano root:root. n8n poteva leggere la coda ma non scrivere l’offset. Errore: EACCES: permission denied, open '/home/node/logs/job_classify_offset.txt'.

Il sintomo ingannevole era che il sistema sembrava funzionare.

Il webhook rispondeva. Il workflow partiva. La prima chiamata Ollama aveva successo. Il fallimento emergeva solo su una condizione secondaria - la scrittura dell’offset - in un punto che non emetteva errori visibili. Solo i log Ollama mostravano che la seconda call non era mai stata tentata.

Un cambiamento di una riga. Ore di debugging per trovarlo, perché il sistema sembrava funzionare.

Ollama non era lento, era configurato per uso interattivo

Dopo il fix del file ownership, Ollama classificava ma era lento.

Prima call: circa 96 secondi. Seconda: oltre 60 secondi, timeout. Terza: timeout.

I log Ollama mostravano loading model a ogni batch. Il campo UNTIL nell’output di ollama ps diceva: “About a minute from now”.

Ollama scaricava il modello dalla memoria dopo circa 60 secondi di inattività. Tra un batch n8n e il successivo passavano più di 60 secondi. Ogni batch partiva da zero: cold start più prefill di 3000 token di input.

La macchina su cui girava Ollama non mostrava problemi di performance.

Era un default ottimizzato per uso interattivo - scarica il modello presto per liberare memoria - che diventava distruttivo in una pipeline batch con chiamate ogni 2-3 minuti.

Fix applicati:

  • keep_alive: -1 nella request Ollama, modello tenuto in memoria;
  • timeout per call da 60s a 150s;
  • bodyText da 2000 a 1000 caratteri, token di input da circa 3458 a circa 1800;
  • BATCH_SIZE da 5 a 3.

Risultato: prima call a 64 secondi (modello freddo), seconda a circa 41 secondi, terza a circa 30 secondi a regime.

La tecnologia non era cambiata. Era cambiato il punto in cui veniva inserita nell’architettura e il modo in cui veniva configurata per quel contesto.

La tabella dei default che non vedi

Questa seconda sessione ha un tema trasversale: i valori predefiniti pensati per qualcun altro.

DefaultValore effettivoImpatto
Provider LLMAnthropic Haiku (non Ollama)centinaia di chiamate a pagamento invece di 0
Ollama keep_alive1 minutoCold start a ogni batch, timeout a cascata
bodyText massimo2000 chars (~3458 token)84% del context window usato
File owner Dockerroot (scritto dall’host)EACCES silenzioso a metà elaborazione
Timeout per call n8n60 secondiInsufficiente per un modello locale 4B

Nessuno di questi valori era sbagliato in astratto. Erano sbagliati per questo sistema specifico, in questo contesto specifico.


Cosa insegna questa esperienza

Non esco certo da queste due sessioni con una conclusione anti-agent.

Claude Code ha fatto molte cose utili: ha letto workflow complessi, ha trovato nodi critici, ha scritto patch, ha interrogato log, ha creato script di test, ha corretto formati API.

Il limite non era la capacità operativa. Era la tendenza a restare dentro il prossimo fix, e la difficoltà a surfaceiare le decisioni implicite - come il cambio di provider - come tali.

Quando un sistema ha race condition, timeout, stato condiviso e provider LLM intercambiabili, il problema non si risolve solo aumentando la velocità del debugging.

Serve decidere:

  • dove passa il confine tra le fasi;
  • quale stato deve essere persistente;
  • quale provider è autorizzato;
  • quali dati devono essere mockati;
  • quale default è ottimizzato per il tuo caso e quale no;
  • quale metrica dice che il sistema sta funzionando davvero.

Questo è il lavoro che non delegherei completamente a un agente.

Non per sfiducia verso l’AI.

È il punto in cui debugging e architettura non si distinguono più. La configurazione è parte del problema.

La lezione operativa

Quando uso agenti AI su sistemi reali, non guardo solo se producono codice corretto.

Guardo se il ciclo di lavoro sta migliorando.

Se l’agente rilancia dieci volte la stessa pipeline, voglio fermarmi.

Se cambia provider LLM per risolvere un timeout, voglio una decisione esplicita.

Se il test richiede tutto il sistema end-to-end, voglio spezzarlo.

Se compare per la quarta volta un “fix definitivo”, voglio rivedere l’architettura.

Se il sistema sembra funzionare ma i numeri non tornano, voglio guardare i default.

Il valore umano, in queste sessioni, non è stato scrivere più codice dell’agente.

È stato riconoscere che il sistema stava girando in loop, imporre una forma migliore - coda, test isolati, provider esplicito - e poi tenere traccia di cosa stava succedendo davvero dentro.

Gli agenti AI accelerano molto il lavoro tecnico. Ma accelerano anche le cattive abitudini se nessuno controlla la direzione. E rendono invisibili i default, perché li applicano senza nominarli.