L'architettura del codebase AI-friendly: deep modules, AI come dependency, e perché il debito non si refactora da solo

L'architettura del codebase AI-friendly: deep modules, AI come dependency, e perché il debito non si refactora da solo

C'è un'osservazione contro-intuitiva che continua a verificarsi nelle codebase che audito: le architetture pensate per umani aiutano l'AI molto più delle architetture pensate per l'AI. Il motivo è strutturale e merita di essere capito a fondo, perché ribalta il consiglio dominante del marketing AI 2026 ("ottimizza il codice per renderlo AI-friendly") e riporta il discorso dove dovrebbe essere stato dall'inizio: l'architettura del software è una disciplina con quarant'anni di letteratura ingegneristica alle spalle, e gli stessi principi che la rendono mantenibile per un team umano la rendono navigabile per un agente AI.

Nei tre articoli precedenti di questa serie ho misurato il debito di comprensione, ho mappato la cascata sistemica su sicurezza, produttività e formazione, e ho descritto le discipline di processo (specs critique, design concept, ubiquitous language, feedback loops, TDD) che invertono la traiettoria. In questo articolo passo all'architettura: cinque scelte strutturali che separano un codebase su cui l'AI può lavorare in produzione da un codebase su cui l'AI accelera il debito.

Cosa rende un codebase navigabile dall'AI?

Risposta secca: la profondità modulare, non la frammentazione. Un codebase con pochi moduli grandi che nascondono complessità dietro interfacce semplici è facile da navigare per un agente AI. Un codebase con tanti moduli piccoli che espongono interfacce complesse è esattamente l'opposto, anche se sulla carta sembra più "modulare".

L'idea viene dal libro di John Ousterhout, A Philosophy of Software Design, che è probabilmente l'analisi più rigorosa pubblicata negli ultimi dieci anni sul concetto di complessità in ingegneria del software. Ousterhout definisce la complessità come tutto ciò che, nella struttura di un sistema, lo rende difficile da capire e da modificare. La definizione sembra ovvia, ma il punto pratico è che la complessità non si misura in righe di codice: si misura in cognitive load richiesto per intervenire correttamente. E la cognitive load di una codebase è una funzione diretta di come i confini fra moduli sono disegnati.

I moduli profondi (deep modules) sono moduli che incapsulano molta funzionalità dietro un'interfaccia semplice. L'esempio canonico che Ousterhout usa sono le system call Unix: cinque chiamate (open, read, write, lseek, close), signature minimali, e dietro c'è tutta la complessità del filesystem, della cache di pagina, degli inode, dei device driver. Chi consuma l'interfaccia non deve sapere niente di tutto questo. Chi la implementa nasconde la complessità in modo deliberato, a beneficio di chiunque la userà.

I moduli superficiali (shallow modules) sono il pattern opposto: tante classi piccole, ciascuna con la propria interfaccia complessa, dipendenze incrociate visibili dall'esterno, gerarchie profonde di abstract class senza contenuto reale. Sono il sintomo classico delle codebase Java enterprise degli anni 2000, e sono esattamente il pattern che l'AI tende a produrre se non viene guidata. La ragione è statistica: il training set è pieno di codice frammentato, e il modello replica il pattern dominante.

Il problema nell'era AI è duplice. Primo: i moduli superficiali sono difficili da navigare per chiunque, umano o agente, perché il numero di confini da attraversare per capire un singolo flow è proporzionale al numero di moduli. Secondo: l'agente AI, quando esplora una codebase superficiale, fatica a costruire un modello mentale del sistema, perché la complessità è distribuita in modo tale che ogni file, preso da solo, è banale, ma il significato emerge solo dalla composizione di decine di file. L'agente naviga in superficie, perde il contesto cumulativo, e finisce per generare modifiche che soddisfano la richiesta locale ma rompono assunzioni che non poteva vedere.

Nelle codebase che audito post-Copilot, il pattern peggiore è quello in cui l'AI ha amplificato la frammentazione preesistente: ha continuato a creare nuove classi piccole invece di consolidare quelle esistenti, ha ramificato gerarchie di tipi invece di approfondire interfacce, ha estratto helper micro-funzioni invece di costruire moduli con responsabilità chiara. La codebase finisce con il triplo dei file e la metà del senso architettonico.

Come si lavora con l'AI come gray box?

Risposta secca: progetta tu l'interfaccia di ogni modulo profondo, lascia che l'AI implementi l'interno, e tratta il modulo come una scatola grigia. La superficie esposta resta sotto il tuo controllo deliberato; l'interno può essere generato, modificato e rigenerato dall'agente con costo cognitivo basso, perché i suoi confini sono protetti dall'interfaccia.

Il pattern funziona perché traduce direttamente l'information hiding di Ousterhout in un workflow di collaborazione human-AI. L'interfaccia è dove vivono le decisioni semantiche del sistema: cosa il modulo promette di fare, cosa consuma in input, cosa emette in output, quali invarianti garantisce. Queste decisioni richiedono intent, che è esattamente ciò che il modello probabilistico non possiede. L'implementazione, invece, è dove vive la traduzione meccanica di una specifica chiara in codice eseguibile. Su quel piano l'AI è effettivamente competente, perché il pattern matching su training set di milioni di implementazioni simili è sufficiente.

Operativamente, nel mio orchestrator personale ho una regola di scoping: ogni nuova feature inizia con la definizione esplicita dell'interfaccia (signature, contract, error model) come artifact di review umano. Solo dopo aver approvato l'interfaccia, l'agente è autorizzato a generare l'implementazione. Le modifiche successive seguono lo stesso pattern: se l'intervento tocca l'interfaccia, è una decisione di design che richiede review umano; se tocca solo l'implementazione interna, l'agente può procedere con feedback loop automatici (test al confine, type check, validazione semantica contro le ADR) come unico gate.

C'è un'eccezione importante che vale la pena esplicitare: il pattern gray-box funziona per moduli non critici, dove il fallimento è recuperabile e l'errore è confinato. Per moduli che gestiscono pagamenti, identità, o invarianti regolamentari (audit trail, retention dei dati personali sotto GDPR, financial reporting), la gray-box non è abbastanza. Lì serve review approfondito anche dell'implementazione, perché ogni linea di codice può rompere requisiti legali. Il pattern di workflow che applico in questi casi è due livelli di gating: gray-box sulla maggior parte del codebase, white-box sui moduli che attraversano i regulatory boundary.

Cosa significa investire nel design ogni giorno con AI?

Kent Beck ha sintetizzato il principio in una formulazione che è difficile migliorare: invest in the design of the system every day. Tradotto operativamente, significa che il design non è un artefatto che si fa una volta e poi si esegue. È un asset vivo che si aggiorna continuamente man mano che il sistema evolve. Il design include le ADR (Architecture Decision Records), il modello di dominio in linguaggio condiviso, le invarianti di sistema, le mappe dei moduli e delle loro responsabilità.

Lo specs-to-code naïve, che ho criticato nell'articolo precedente, è esattamente il pattern opposto: disinveste dal design ad ogni rigenerazione. Il PRD vive da solo, non si propaga al design accumulato, non viene riconciliato con le ADR, non aggiorna il modello di dominio. Iterazione dopo iterazione, il design diventa stale, le rigenerazioni partono da informazioni progressivamente più obsolete, e il sistema diverge.

L'investimento nel design ogni giorno significa, in pratica, tre abitudini operative. Primo: le ADR vengono aggiornate insieme al codice, non separatamente. Quando una decisione architettonica cambia (per qualunque ragione, AI o umana), la ADR viene scritta o aggiornata nello stesso commit del cambio. Secondo: il modello di dominio (il UBIQUITOUS_LANGUAGE.md che ho descritto nel terzo articolo) viene esteso quando emergono nuovi concetti, non lasciato decadere. Terzo: il PRD per ogni nuova feature è module-aware, cioè specifica esplicitamente quali moduli vengono toccati e quali interfacce modificate, integrandosi con la mappa architettonica esistente invece di vivere come documento parallelo.

Nel mio workflow personale, il PRD non è mai un documento che precede tutto. È un asset che evolve insieme al design del sistema, e che incorpora i riferimenti alle decisioni preesistenti. Una nuova feature richiede di leggere e citare le ADR rilevanti, di estendere il glossario di dominio se introduce concetti nuovi, e di dichiarare esplicitamente quali confini di modulo vengono attraversati. È più lento del PRD-libero-da-vincoli, ma è l'unico modo che ho trovato per evitare che il design si sgretoli sotto la velocità di output dell'AI.

Se la tua organizzazione sta integrando AI nei pipeline di sviluppo e vuoi capire come strutturare l'architettura interna per evitare che l'output AI diventi un vettore di disgregazione, nel mio hub dedicato all'AI per aziende sul lato automazione trovi la metodologia di governance architettonica che applico per pipeline production-grade, con artefatti concreti, gate di review e KPI di stabilità.

Come si tratta l'AI come dependency architettonica e non come tool di produzione?

Risposta secca: come tratti qualunque altra dependency che è lenta, non deterministica e potenzialmente inaffidabile. Cioè con queue, circuit breaker, timeout, fallback path, observability completa e Service Level Objective definiti prima della messa in produzione. Se il tuo sistema deve garantire 99,9% di availability e l'integrazione AI nel design non include esplicitamente un fallback funzionale per il fallimento del provider, non hai un sistema 99,9%. Hai una stima ottimistica.

Il pattern è classico nel design dei sistemi distribuiti, ed è stato codificato per la prima volta in modo robusto da Hystrix di Netflix nel 2012. Tre stati del circuito (chiuso, aperto, semi-aperto), thread pool isolation per evitare cascade failure, fallback semantici quando il primary fallisce, sliding window di health check per decidere quando riprovare. Hystrix oggi è deprecato e sostituito da Resilience4j (Java), Polly (.NET) e PyBreaker (Python), ma il modello concettuale è identico. La traduzione per LLM API è naturale: l'agente OpenAI restituisce 429 Too Many Requests, il circuit breaker apre, le richieste successive vengono dirottate verso il provider di fallback (es. Anthropic, o un modello self-hosted come Mistral), e il sistema continua a operare in modalità degraded invece di propagare il timeout fino all'utente finale.

C'è una specificità delle dependency LLM che il modello classico Hystrix non cattura completamente: i fallimenti semantici. Un endpoint LLM può restituire HTTP 200 e un payload di token apparentemente valido, ma il contenuto può essere un'allucinazione di alta qualità, una citazione di una fonte inesistente, o una sequenza di reasoning bloccata in loop. Questo non è un timeout, non è un 5xx, non è una request rejection. È un fallimento qualitativo del contenuto. Il circuit breaker classico non lo cattura. Per gestirlo serve uno stato aggiuntivo, che la letteratura più recente chiama DEGRADED, e un set di metriche di qualità (response coherence, latency-to-quality ratio, hallucination rate misurato per dominio) che alimentano la decisione di apertura del circuito.

Lo stack di resilienza che applico nel mio MCP server di produzione include cinque pattern stratificati. Primo: timeout aggressivo (30 secondi per richieste complete, 8 secondi per first-token su streaming). Secondo: circuit breaker con failure threshold del 50% in finestra mobile di 60 secondi. Terzo: failover chain ordinata (Claude Sonnet → Mistral 3 self-hosted → cache di risposte deterministiche per i casi più frequenti). Quarto: full instrumentation OpenTelemetry, con span dedicati per ogni chiamata LLM (tracer.startActiveSpan('llm.completion', ...)), attributi semantici (modello, route, token consumati) e metriche Prometheus su rate, latency e fallback frequency. Quinto: complexity threshold sul codice generato, dove cyclomatic complexity oltre una soglia predeterminata triggera mandatory human review prima del merge.

Tradotto in termini di SLO per chi gestisce un servizio production-grade che integra AI: il SLO di availability del tuo servizio è il prodotto del SLO del provider AI per il SLO di tutti gli altri componenti, non la somma. Se il provider AI promette 99,5% di uptime e tu non hai fallback, il tuo SLO massimo teorico è 99,5%. Se vuoi 99,9%, hai bisogno di fallback indipendenti, perché il prodotto di due provider in parallelo è quello che alza la disponibilità composita. Questo è ingegneria standard di sistemi distribuiti, ed è esattamente il discorso che si è perso nel marketing dei prodotti AI consumer-grade applicati a contesti aziendali. La scelta di provider con data sovereignty europea come Mistral 3 MoE on-prem diventa rilevante anche da questo punto di vista: avere un fallback che non dipende dallo stesso provider primario è la differenza fra un'architettura resiliente e un single point of failure travestito.

Perché il debito AI non sarà refactorato da nessun modello futuro?

Risposta secca: il refactoring richiede memoria dell'intent originale. L'AI può rifare la sintassi, non può ricostruire un'intenzione che non è mai esistita esplicitamente. Ogni promessa di "il prossimo modello sistemerà tutto" è gravity pointing in the wrong direction: scarica sul futuro un debito che si compone nel presente.

Il punto è strutturale al concetto stesso di refactoring. Refactoring, nella formulazione classica di Martin Fowler, è una trasformazione del codice che ne preserva il comportamento esterno mentre ne migliora la struttura interna. La nozione di "comportamento esterno preservato" presuppone che il comportamento esterno sia stato esplicitato da qualche parte: nei test, nelle ADR, nel modello di dominio, nella documentazione. Senza queste tracce esplicite, il refactoring diventa indistinguibile dalla riscrittura: si producono nuove assunzioni invece di preservarne di esistenti.

Quando il codice è stato generato da un agente AI senza che il developer abbia formalizzato l'intent prima o dopo, il "comportamento esterno" è semplicemente il comportamento implementato. Non c'è nulla da preservare, perché non c'è nulla che precede l'implementazione. Un agente AI futuro che operasse su questa codebase non potrebbe refactorarla nel senso stretto del termine: potrebbe solo riscriverla, generando un nuovo set di assunzioni implicite che si stratifica sopra le precedenti.

I settori che lavorano con software safety-critical hanno capito il problema in modo precoce e formale. La NASA con il suo standard NASA-STD-8739.8 e la procedura NPR 7150.2D §3.7.4 richiede 100% di copertura MC/DC (Modified Condition/Decision Coverage) per ogni componente di software safety-critical. È un livello di rigor che non si raggiunge con codice generato senza intent esplicito, perché MC/DC presuppone che ogni condizione in ogni decisione del codice abbia un effetto verificabile e indipendente sul risultato. Il paper tutorial NASA/TM-2001-210876 lo documenta nel dettaglio, e la lezione del Mars Climate Orbiter del 1999 (perduto in parte per copertura insufficiente del sistema di fault protection) è citata come riferimento canonico. La NASA non distingue formalmente tra codice umano e AI-generated, ma il bar di qualità che impone è un bar che la maggior parte del codice AI-generated non raggiunge senza un layer di review specializzato.

Il caso SQLite è altrettanto istruttivo. SQLite non ha un divieto esplicito sul codice AI-generated, ma il Code of Ethics del progetto, ispirato alla Regola di San Benedetto e centrato su umiltà, stabilità e accountability, è incompatibile con un workflow in cui il contributor non possiede l'intent del codice che sottomette. Il Contributor Agreement richiede affidavit firmati che dedicano il contributo al pubblico dominio, e il copyright di codice generato da un modello probabilistico è una zona grigia legale. Effetto pratico: SQLite di fatto non accetta contributi AI-generated, perché la provenance non è ricostruibile in modo che soddisfi il rigore del progetto. È un'organizzazione che, fra le più diffuse al mondo nel software embedded (presente in miliardi di dispositivi), ha tracciato un confine esplicito che il marketing AI consumer ignora.

Per chi gestisce sistemi che hanno requisiti di compliance, audit trail, o disponibilità garantita contrattualmente, il messaggio è asciutto: il debito AI accumulato oggi non sarà refactorato da un modello futuro. Va gestito ora, con disciplina di intent management, design vivo, architettura resiliente. Non c'è gravity assist verso il futuro che annulli la composizione del debito presente.

Le cinque scelte architettoniche che ho descritto in questo articolo (moduli profondi sopra moduli superficiali, gray-box invece di white-box delegation, investimento continuo nel design, AI come dependency e non come tool, gestione esplicita del debito che non si refactora da solo) compongono lo stack architettonico che separa una pipeline AI di produzione da una pipeline AI di prototipazione. La differenza non è la sofisticazione del modello che usi, è la disciplina con cui lo integri nel sistema. Se la tua organizzazione sta valutando il passaggio dell'integrazione AI da fase pilot a fase produzione e vuoi una review architetturale che identifichi i gap rispetto a queste cinque dimensioni, il modulo di preventivo gratuito ti permette di descrivere lo stato attuale del sistema e ti rispondo direttamente quale punto di leva è il più urgente per il tuo contesto. Nel quinto e ultimo articolo della serie chiudo il cerchio: come riposiziona la propria carriera l'ingegnere senior che non vuole diventare il prossimo prompt operator, e perché il giudizio calibrato è la skill tecnica del decennio.

Ultima modifica: