AI-assisted debugging: usare Claude per analizzare stack trace e trovare la root cause in 20 minuti

AI-assisted debugging: usare Claude per analizzare stack trace e trovare la root cause in 20 minuti

Il 9 aprile 2026, alle 3:47 del mattino, il mio sistema di monitoring ha iniziato a registrare crash sporadici su un componente della mia pipeline personale: il worker Laravel Horizon che processa embedding di documenti finiva in OOM ogni 6-9 ore, casualmente, senza pattern temporale evidente. L'infrastruttura colpita era il solito Hetzner CCX33 con 32 GB RAM DDR5, PHP 8.3, Redis 7, PostgreSQL 16, Laravel 12. Lo stack trace raccolto aveva 38 frame di profondità, il log applicativo mostrava 420 righe nei 10 minuti prima del crash, il log Nginx 4.200 righe, il log PostgreSQL 1.100 righe. In un debugging tradizionale avrei impiegato 3-6 ore a ricostruire il contesto: leggere tutti i log, correlare timestamp, formulare ipotesi, verificare. Usando Claude Sonnet 4.6 con un workflow strutturato che ho affinato negli ultimi mesi, ho trovato la root cause in 22 minuti effettivi: un memory leak su un closure che catturava un grafo Eloquent con relazioni deep-nested, triggerato solo da documenti con più di 150 capitoli. Questo articolo racconta quel debug passo per passo - non come aneddoto, ma come case study narrativo del workflow riusabile. L'obiettivo è mostrare cosa rende un LLM strumento potente per debugging e cosa, invece, lo fa degenerare in tool che suggerisce ipotesi plausibili ma sbagliate sprecando ore.

Come si struttura il contesto per un LLM che deve fare debugging serio?

La risposta breve è che il contesto non si copia-incolla, si cura. Un developer frettoloso che paste-a 3.000 righe di log misti dentro un chat con Claude ottiene risposte plausibili ma statisticamente sbagliate nel 60-70% dei casi, perché il modello non ha modo di distinguere segnale da rumore nel mucchio. Un contesto curato - stack trace di 15 frame rilevanti, 50 righe di log applicativo attorno al crash, metadata di sistema al momento dell'errore, configurazione pertinente - porta il modello a ragionare in modo effettivo. La differenza non è nel modello: è nel rispetto per il segnale che il developer mostra nel preparare l'input.

La curazione è il 60% del tempo totale del workflow e il 100% della qualità del risultato. Se dedico 12 minuti a curare il contesto e 10 minuti a leggere la risposta del modello e verificare, uso il modello bene. Se dedico 2 minuti a copiare tutto e 30 minuti a discutere avanti e indietro, sto usando il modello come rubber duck costoso, non come debugging assistant.

Se vuoi vedere come integro Claude in workflow di sviluppo senior - dove il modello è acceleratore del ragionamento, non sostituto - nel mio hub sullo sviluppo AI per aziende trovo articoli su bot di code review, agent AI per assessment legacy, knowledge management AI-assisted, con filo conduttore comune di disciplina del contesto come prerequisito di qualità.

La curazione del contesto per il bug che avevo in mano

Il mio workflow di curazione ha quattro step deterministici. Step uno: il trace essenziale. Un'utility Python interna trace-prune che riceve uno stack trace completo e lo riduce tenendo solo i frame applicativi - tipicamente salta i 20-30 frame iniziali del framework (Laravel internals, middleware, container resolver) che raramente contengono la root cause. Lo stack trace del mio crash è passato da 38 a 14 frame rilevanti.

Step due: il log window. Uso journalctl con filtro temporale per estrarre log applicativo, Nginx e PostgreSQL nella finestra di 120 secondi prima del crash e 30 secondi dopo. Poi applico deduplication sui log ripetuti (molti crash producono migliaia di warning uguali che riempiono di nulla il contesto) e keyword filtering su identificatori rilevanti (in questo caso il worker_id e il documento_id che erano nel trace). I 5.720 righe totali sono diventate 340 righe dopo deduplication e keyword filtering.

Step tre: system metadata al momento del crash. Uso Prometheus con una query range sul minuto del crash per estrarre: utilizzo memoria del processo, load CPU, connessioni attive Redis e PostgreSQL, hit rate del cache HTTP Nginx, throughput Horizon. Dati scarni ma rivelatori: al momento del crash la memoria PHP era a 768 MB su 512 MB configurati nel memory_limit (fallimento classico), il PostgreSQL aveva 86 connessioni attive su 100 configurate, Redis respondeva a latenza 2,8 ms (normale).

Step quattro: il codice sospetto. Il primo frame del trace pruned puntava a app/Jobs/IndexDocumentJob.php:189. Apro quel file, estraggo il metodo handle() dove stava la riga 189, e la catena di 3 metodi chiamanti. Totale: 140 righe di codice commentato. Questo è il ground truth che il modello deve ragionare sopra.

Il risultato aggregato della curazione è un prompt di circa 4.500 token - stack ridotto + log filtrato + metrics + codice sospetto. Claude Sonnet 4.6 riceve questo invece di 25.000 token di spazzatura indistinta.

Il prompt di debugging: istruzioni strette per evitare "plausible-but-wrong"

Il prompt che uso ha tre pezzi strutturali. Preamble: "sei un senior software engineer che sta facendo debugging. Analizza i dati forniti e proponi una root cause hypothesis con confidence score. Se il contesto è insufficiente, chiedi esattamente quali informazioni aggiuntive ti servirebbero". Corpo: il contesto curato nei quattro step sopra. Output constraint: "rispondi in JSON strutturato con campi hypothesis, confidence_0_to_1, reasoning, next_steps_to_verify, information_gaps".

PROMPT (struttura):

Sei un senior software engineer specializzato in debugging di applicazioni PHP/Laravel.
Il tuo task è identificare la root cause di un crash dato il contesto curato.

REGOLE:
1. NON speculare oltre i dati forniti. Se il contesto è insufficiente per una hypothesis
   robusta (confidence < 0.6), chiedi le informazioni specifiche che ti servono.
2. Una root cause plausibile con bassa evidenza è PEGGIO di un "non so".
3. Distingui fra "fatto osservato", "inferenza ragionevole", "speculation".

CONTESTO:

[STACK TRACE] (14 frame rilevanti)
...

[LOG WINDOW] (finestra 120s prima, 340 righe dopo filtering)
...

[SYSTEM METRICS al momento del crash]
- php memory_usage: 768 MB (limit 512 MB)
- postgres connections: 86/100
- redis latency: 2.8 ms
- horizon queue depth: 412

[CODICE SOSPETTO]
app/Jobs/IndexDocumentJob.php (metodo handle + 3 chiamati, 140 righe)
...

Emetti risposta JSON:
{
  "hypothesis": "<descrizione 100-200 parole della root cause più probabile>",
  "confidence_0_to_1": <0.0-1.0>,
  "reasoning": "<catena di inferenza: quali fatti osservati portano alla hypothesis>",
  "next_steps_to_verify": ["<azione 1>", "<azione 2>", ...],
  "information_gaps": ["<cosa non sai che miglioreresti se sapessi>"]
}

Il vincolo "non speculare oltre i dati" è il più importante. Un LLM debugging senza questo vincolo produce sempre una risposta - anche quando non dovrebbe averne. La risposta sembra ragionevole ma è costruita su inferenze vaghe, e il developer spende ore a verificare un'ipotesi che il modello ha inventato. Il vincolo forzato a chiedere info quando confidence è bassa trasforma il modello in collaboratore che sa quando non sa. Empiricamente, questa modifica al prompt ha aumentato la accuracy delle mie sessioni di debugging dal 58% al 84%.

La risposta di Claude e il primo pass di verifica

Per il crash specifico che stavo debugging, la risposta di Claude dopo 18 secondi è stata:

hypothesis: "Memory leak nel job IndexDocumentJob::handle(). Il closure passato
a collect()->map() alla riga 189 cattura $this implicitamente, mantenendo in
memoria l'intero grafo Eloquent del Document caricato con eager loading.
Documenti con alta cardinalità di relazioni (>150 capitoli associati, visibile
nel log a riga 2145 che menziona document_id=8821 con grande numero di chapters)
producono grafo memoria proporzionale. Il memory_limit PHP 512 MB viene saturato
quando il grafo supera ~100k nodi Eloquent."

confidence: 0.78

reasoning: "
- FATTO OSSERVATO: il log mostra 'processing document_id=8821' al timestamp t-45s
  dal crash.
- FATTO OSSERVATO: il stack trace culmina in collect()->map() a IndexDocumentJob:189.
- INFERENZA: il codice a riga 189 usa un closure che riferisce $this->documentModel,
  catturando il grafo.
- INFERENZA: l'aumento graduale di memory_usage visibile nelle metriche suggerisce
  leak, non picco istantaneo.
- FATTO OSSERVATO: memory_usage 768 MB > limit 512 MB = OOM deterministic.
"

next_steps_to_verify: [
  "Controllare documenti con più di 100 chapters nella tabella: SELECT document_id, COUNT(*) FROM chapters GROUP BY document_id HAVING COUNT(*) > 100",
  "Riprodurre il bug localmente con un documento sintetico a 200 chapters",
  "Usare Xdebug memory profiling sul job per confermare il growth pattern"
]

information_gaps: [
  "Non ho il contenuto della classe Chapter e delle sue relazioni Eloquent",
  "Non so se ci siano observer Eloquent che potrebbero amplificare il leak",
  "Non so il PHP memory_limit della versione precedente pre-regression"
]

La confidence 0,78 è sopra la mia soglia di "azione" 0,7. Eseguo il primo next step: la query SQL. Result: 9 documenti con più di 100 chapters, il più grande era 224 chapters. La lista includeva document_id=8821 - il documento citato nel log al timestamp del crash. L'hypothesis si stava consolidando.

Il drilling aggiuntivo: dall'ipotesi alla prova

A questo punto ho un'ipotesi con confidence 0,78 e una conferma indiretta (documenti grandi esistono). Per passare a 0,95+ e dichiarare il bug risolto, devo riprodurre localmente. Setup: fixture di test con un Document sintetico che ha 224 Chapter collegati, 40 relazioni per ciascuno.

Eseguo il job IndexDocumentJob::dispatchSync($syntheticDoc) con Xdebug memory profiler attivo. Output: memoria sale monotonamente da 140 MB iniziali a 780 MB al culmine, prima del OOM. Pattern: crescita lineare con processing dei chapter. Il profiler dà il punto di allocazione: linea 189 di IndexDocumentJob.php, nel closure passato a map(). Hypothesis confermata al 99%.

Il fix è banale una volta identificata la root cause. Sostituisco il closure che cattura $this con una funzione namespacata pura:

// PRIMA (leak)
$embeddings = $chapters->map(function (Chapter $chapter) {
    return $this->embeddingService->embed($chapter->content);
});

// DOPO (no leak)
$service = $this->embeddingService;
$embeddings = $chapters->map(fn (Chapter $chapter) => $service->embed($chapter->content));
unset($service);

Il fn arrow function non cattura $this esplicitamente, e l'assegnazione del service a variabile locale prima del map permette l'unset esplicito dopo. Dal deploy del fix alle 11:23 del 9 aprile, zero OOM in 14 giorni di osservazione - bug risolto.

Tempo totale impiegato dal primo alert al commit del fix: 22 minuti. Lavoro suddiviso: 6 minuti di curazione contesto, 18 secondi di attesa Claude, 8 minuti di verifica locale con fixture sintetico, 3 minuti di implementation del fix, 5 minuti di deploy e verifica. Senza l'LLM, la stessa sequenza - formulare hypothesis, identificare il documento problematico, capire il closure capture pattern - sarebbe stata 2-4 ore di lavoro.

Dove l'LLM sbaglia in modo prevedibile: tre classi di falsa root cause

Non tutti i debug sono così lineari. Il workflow degenera in tre modi specifici che ho osservato ripetutamente.

Classe uno: caching issues male interpretati. Bug che coinvolgono stale cache (Redis con TTL scaduto, HTTP cache con vary header errato, browser cache aggressivo) sono frequentemente diagnosticati dal modello come "issue di coerenza del database" o "race condition". Il modello non ha intuizione su cache layer invisibili nel runtime; senza vedere esplicitamente le chiamate al cache nel contesto, non ci arriva. La contromisura: aggiungo al contesto curato una riga esplicita sul cache hit/miss del momento dell'incidente, quando rilevante.

Classe due: timing issues e race condition. Bug che dipendono da ordinamento temporale di operazioni concorrenti sono notoriamente difficili per il modello. Claude tende a ragionare su flussi singoli sequenziali; race condition multi-processo confondono sistematicamente. La contromisura: quando sospetto un timing issue, chiedo esplicitamente al modello di considerare la concorrenza e fornisco log da più processi con timestamp al millisecondo. Anche così, il tasso di root cause corretta scende al 40-50%.

Classe tre: configurazione ambiente. Bug dovuti a differenza di configurazione fra staging e produzione, variabili d'ambiente mancanti, path filesystem diversi, permessi Linux. Il modello non ha accesso al config runtime e fatica a ragionare senza vederlo. La contromisura: quando sospetto un config issue, aggiungo un env diff fra gli ambienti al contesto - elenco di variabili d'ambiente differenti. Spesso la risposta diventa ovvia.

In tutti e tre i casi, il pattern comune è che il modello tende a ragionare sul codice visibile, e tutto ciò che non è nel codice visibile viene ignorato o inferito male. La contromisura generale è curazione del contesto che include l'invisibile - cache state, timing multi-processo, config, permessi. Questo è il 30% del mio lavoro di curazione che distingue debugging con AI efficace vs inefficace.

Il pattern "chiedi al modello cosa gli manca"

Un pattern che ho integrato dopo quattro mesi di uso: quando il modello risponde con confidence sotto 0,7, invece di provare a convincerlo, leggo la sua information_gaps e fornisco precisamente quei dati. Questo è il ciclo iterativo di debugging: io curo il contesto iniziale, il modello identifica cosa gli manca, io fornisco il delta, il modello ri-valuta. Quando questo ciclo funziona, il modello diventa collaboratore che guida il mio debugging - mi dice dove guardare.

Un esempio concreto: un crash che inizialmente il modello attribuiva a "memory leak generico" (confidence 0,4) ha identificato nell'information_gaps "non so se c'è un Observer Eloquent su questa classe". Ho aperto il codice, cercato Observer, trovato che sì, c'era un DocumentObserver::saving() che caricava relazioni aggiuntive. Ho aggiunto quello al contesto, il modello è salito a confidence 0,88 e ha identificato l'Observer come amplificatore del leak. La domanda giusta del modello ha risparmiato ore di esplorazione cieca da parte mia.

Quando NON usare Claude per debugging

Ci sono casi in cui l'LLM non aiuta, o addirittura ritarda la soluzione. Bug banali: tipo un try/catch che inghiotte l'eccezione - il log dice quale linea lancia, l'unica cosa da fare è guardare il codice. Usare l'LLM qui aggiunge setup senza valore. Bug profondamente internals: driver MySQL che ha comportamento strano su una versione specifica, bug nel kernel Linux, bug in un'estensione PHP compilata nativamente. Il modello qui non ha abbastanza training specifico sulle internals e tende ad inventare. Meglio leggere direttamente documentazione primaria e mailing list. Bug di sicurezza: analisi di possibili exploit su codice in audit richiedono accuracy assoluta, e il modello, anche se spesso corretto, ha tasso di falso negativo non accettabile per quello scope. La disciplina che applico negli audit di red team su RAG system usa l'LLM per generare payload di test, non per giudicare se un'implementazione è sicura.

Il workflow AI-assisted debugging si giustifica come pratica quotidiana quando lavori in un ambiente con: codebase complessa (50k+ righe), team piccolo che non può coprire conoscenza completa del sistema, logging strutturato che permette curazione efficace, budget mensile per AI di almeno 20-40 dollari. In quel punto, il tempo risparmiato settimanale in sessioni di debugging accelerato vale largamente il costo.

Il valore vero di Claude come strumento di debugging non sta nella sua capacità di risolvere bug complessi da solo - non lo fa. Sta nel comprimere il tempo che un senior engineer impiega dal sintomo alla root cause di un fattore 3-8x. È l'equivalente del 2026 del debugger interattivo del 2000: non sostituisce il ragionamento del developer, lo amplifica. Un developer che legge con disciplina lo stack trace + i log + il codice sospetto e ragiona insieme all'LLM produce output di qualità senior in tempo di metà di senior. Un developer che paste-a random lo stack trace all'LLM e spera che funzioni produce debugging teatrino. La differenza è tutta nel rispetto per il contesto - una disciplina vecchia di decenni che l'AI realm non ha inventato ma amplificato nel suo valore operativo.

Se gestisci applicazioni in produzione e passi troppo tempo a fare debugging reattivo su problemi complessi - e vuoi capire se il tuo workflow può essere accelerato da un uso disciplinato di Claude - il modulo di preventivo gratuito ti dà una prima lettura in 7 domande, 2 minuti. Ti dico se il tuo progetto rientra nelle cose che so fare bene e, se il caso richiede un profilo diverso, te lo dico e ti indico una direzione utile quando posso.

Ultima modifica: