Diciamocelo: nella maggior parte delle aziende il “proof of concept” era una parola usata per indicare qualcos’altro… cioè qualcosa che nasceva per validare un’idea e finiva per diventare un software in produzione interna se andava bene, a tutti i clienti altrimenti. Questo avveniva e tuttora avviene perché realizzare un POC ha un costo. Tre giorni-uomo, cinque, dieci, e una cosa che costa dieci giorni-uomo non la butta via nessuno, nemmeno quando dovrebbe.
Il risultato è un vizio che ho visto ovunque e che ho avuto anch’io: si parte già preoccupati per la qualità. Si sceglie l’architettura giusta prima di sapere se il problema è quello che si crede. Si discute della libreria di validazione prima di capire se la feature ha senso. È un’ottimizzazione prematura applicata a una fase di discovery, in cui ha poco senso ottimizzare un percorso di cui non si sa ancora dove finirà.

Quello che è cambiato è banale ed enorme insieme: il costo marginale di scrivere codice esplorativo è crollato. Non parlo di scrivere codice buono, quello costa ancora, e ci arrivo. Parlo di arrivare a qualcosa che gira, che si può toccare, che risponde alla domanda “ma questa idea sta in piedi?“.
E quando una cosa costa ore, si può buttare. È tutta lì la differenza.
Fase uno: libero arbitrio sulle dipendenze
Il mio flusso per i POC comincia con una regola che a diversi colleghi fa storcere il naso: in fase di esplorazione non mi pongo alcun limite alle dipendenze. Serve un SDK? Lo installo. Serve una libreria di validazione? La installo. Serve una cosa che si trascina dietro 94 pacchetti? Vabbè, creo quel buco nero attira byte chiamato “node_modules“.
Il motivo è semplice: in questa fase l’obiettivo non è il codice, ma la risposta. Ogni minuto speso a valutare se una libreria sia la scelta giusta è un minuto sottratto alla domanda vera: se ciò che sto costruendo serva a qualcuno. E nell’esplorazione le dipendenze sono un acceleratore enorme: mi danno gratis tutti i pezzi a cui non voglio ancora pensare.
Il caso concreto: un server MCP (Model Context Protocol, il protocollo che permette a un assistente AI di parlare con sistemi esterni) che espone, come strumenti, le funzioni di un apparato di rete che ho in casa. Volevo capire se l’idea reggesse: chiedergli, in linguaggio naturale, lo stato dei dispositivi connessi, invece di navigare un pannello di amministrazione disegnato nel 2014 e mai più guardato in faccia da nessuno.
Ho preso l’SDK ufficiale, ho preso zod per la validazione, ho scritto gli handler, ha funzionato. 28 strumenti esposti, chiamate vere all’apparato, risposte corrette. Tre ore, forse quattro. La domanda aveva la sua risposta: sì, l’idea regge (se siete curiosi, trovate il lavoro nel mio repository su GitHub).
E qui, nel flusso tradizionale, sarebbe finita. Funziona, quindi va bene. Invece è qui che comincia la parte interessante.
Il bivio: butto o poto?
Un POC che funziona non è automaticamente un POC da mantenere. Scritto così sembra ovvio e, nella pratica, è il passaggio che salto più spesso anch’io.
Il criterio che uso comprende due domande, in quest’ordine.
La prima: ho raggiunto l’obiettivo? Il codice si scrive perché c’è un obiettivo, non per scrivere codice. Se il POC non ci arriva, si butta, e si è imparato qualcosa, che poi è esattamente il valore che ci si aspetta da un proof of concept. Non è un fallimento, è il risultato.
La seconda, quella scomoda: con che qualità ci sono arrivato? Perché si può raggiungere l’obiettivo in un modo che non vale la pena di mantenere. Magari funziona, ma è ingestibile. Magari funziona, ma il valore aggiunto rispetto a come si faceva prima è così marginale da non giustificare il costo di mantenerlo in vita. In quel caso, il POC si butta anche se ha avuto successo e si lascia tutto com’era.
Mi è capitato di recente, in una direzione esattamente opposta a quella della storia che sto raccontando. Avevo scritto un paio di tool da zero e funzionavano; il POC serviva a capire se fosse conveniente farli ex novo o costruire un plugin su qualcosa già esistente. La risposta è stata che erano ridondanti rispetto agli strumenti open source già in circolazione e mantenuti da altri. Buttati. Non perché non funzionassero, ma perché ciò che aggiungevano non giustificava il costo di portarseli dietro.
È lo stesso criterio del server MCP applicato al contrario: lì ho sostituito le librerie generaliste con il mio codice, qui ho sostituito il mio codice con le librerie di qualcun altro. Il criterio non è “meno dipendenze”. È il codice giusto per quel problema e per la manutenzione che avrà.
Questa è la parte del metodo che regge tutto il resto e anche quella che costa di più da esercitare, perché sembra buttare via lavoro. Ma se non si è disposti a buttare via un POC riuscito, non si sta esplorando: si sta costruendo e si è tornati al punto di partenza, in cui il costo già speso decide al posto nostro.
Nel caso del server MCP entrambe le risposte sono state sì: il salto rispetto al pannello di amministrazione era reale. Quindi si potava.
E qui arriva la terza domanda, quella che decide il come e non il se: quanta manutenzione avrà questa cosa?
Passare da POC a progetto significa prendere decisioni su come quel codice verrà mantenuto ed è lì, non prima, che si decide quanto specializzarlo. Se il progetto avrà poca manutenzione, un tool interno, un server che faccia una sola cosa e non evolverà granché, allora conviene specializzarlo: si riduce la superficie di attacco e ci si tiene solo ciò che serve. Se invece quel codice finisce in mano a un team che ci lavorerà sopra per anni, l’SDK generalista compra una cosa che il codice specializzato non ha: documentazione che non avete scritto voi, e gente che sa già come funziona. In quel caso trecento casi d’uso contro quattro non sono uno spreco: è onboarding pagato in anticipo.
Il POC serve a imparare e a valutare la fattibilità. Non serve a produrre codice da tenere; quello è un secondo lavoro che comincia dopo e parte da questa domanda.
La gabbia: i test non sono igiene, sono la precondizione
Qui c’è il pezzo che rende possibile tutto il resto.
Prima di toccare una sola dipendenza, ho costruito una gabbia di test: unitari sui singoli componenti, funzionali end-to-end sul comportamento del server visto dall’esterno. 99 test, verdi. E, soprattutto, ho verificato l’interoperabilità con il client SDK reale, con tutti e 28 gli strumenti, prima di rimuovere la dipendenza dall’SDK. Il che significa che avevo una descrizione esecutiva di ciò che il sistema doveva fare, redatta mentre l’SDK era ancora disponibile.
È anche il punto in cui la Definition of Done fa il lavoro per cui esiste: dice quando il comportamento è descritto abbastanza bene da poter cambiare quello che sta sotto. È la differenza tra potare e sperare: senza la gabbia, sostituire un SDK con codice proprio è un atto di fede: togli, provi a mano due o tre casi, sembra che vada, spedisci. Con la gabbia, ogni sostituzione produce una risposta binaria entro 30 secondi.
Detto in altri termini: la libertà della fase uno è pagata con la disciplina della fase due. Le due cose vengono in coppia e, nel mio caso, l’ordine conta: i test scritti prima sono l’unica ragione per cui potevo permettermi di essere spericolato all’inizio.
Il cherry picking: cosa tieni, cosa sostituisci
Sì, ma come fare il cherry-picking? Cosa tenere? Cosa sostituisco e cosa rimuovo?
Cosa tengo. Le librerie indispensabili e con una community forte alle spalle. Indispensabile vuol dire che risolve un problema che non voglio risolvere io, con una complessità reale. Community forte vuol dire che c’è gente che la usa in produzione, che i bug li trova qualcun altro prima di me, che gli aggiornamenti di sicurezza arrivano.
Cosa sostituisco. Le librerie il cui rapporto tra casi d’uso offerti e casi d’uso necessari è sbilanciato. Un SDK che gestisce trecento scenari quando a me ne servono quattro non è un acceleratore: è un peso che mi porto dietro per sempre. E le librerie che aggiungono complessità per contesti che non sono i miei, il caso da manuale è left-pad quelle undici righe che nel 2016 ruppero mezzo npm quando l’autore le tolse dal registry. Se devo allineare una stringa e non mi servono le trentasei varianti di encoding che qualcun altro ha previsto, quel rischio di supply chain è accettato per risparmiare tre righe.
L’audit sul mio caso è stato imbarazzante per la sua chiarezza. L’SDK MCP si trascinava dietro 94 pacchetti — express, hono, ajv, jose, cors, eventsource — per un server che usa esclusivamente il transport stdio. Tutta quella roba serve a gestire i transport HTTP, l’autenticazione OAuth, il CORS: casi d’uso reali e legittimi, semplicemente non i miei. Mi stavo portando in giro un web server completo per far parlare due processi sulla stessa macchina attraverso standard input e standard output: l’equivalente di spedire una lettera al vicino di casa facendola passare dal centro di smistamento.
Ho scritto due file:
src/mcp/server.ts— il server MCP su stdio, JSON-RPC 2.0, che implementa esattamente ciò che serve:initialize,tools/list,tools/call,ping.src/mcp/schema.ts— uno shim di validazione compatibile con l’uso che facevo dizodemette un JSON Schema e valida gli argomenti.
Il risultato:
| Prima | Dopo | |
|---|---|---|
| Dipendenze runtime | 94 pacchetti | 0 |
| Bundle | 1,1 MB | 84 KB, solo builtin node |
node_modules | 94 | 4 (solo toolchain di build) |
Zero dipendenze di runtime significa che la superficie della supply chain a runtime è costituita esclusivamente dal runtime di Node. Non c’è alcuna libreria di terze parti da compromettere, perché non ce ne sono.
Vale per entrambi i criteri quanto dicevo sopra: la soglia si sposta con la manutenzione prevista. Su un progetto che non evolverà sostituisco volentieri; su uno che passerà per molte mani, la stessa identica libreria me la tengo.
Un’eccezione che vale la pena dichiarare, perché il criterio del rapporto casi d’uso non la copre: un SDK crittografico con trecento casi d’uso, di cui me ne servono quattro, non lo riscrivo comunque. Lì la genericità non è il problema; il valore di quel codice sta proprio nel fatto che è stato attaccato per anni da gente più brava di me. Il criterio vale quando la complessità della libreria è accidentale, non quando è la sostanza.
Perché proprio qui serve la verifica avversariale
C’è un’obiezione ovvia a tutto questo, ed è giusta: ho tolto un SDK collaudato da migliaia di utenti e ci ho messo del mio codice. Non ho eliminato il rischio, l’ho spostato dalla supply chain al codice proprietario.
La risposta breve è che non ho riscritto l’SDK: ne ho riscritto solo la parte che mi serviva. Codice specializzato sul problema al posto di codice generalista. Ma la risposta breve non basta, perché quella frazione è comunque il pezzo più critico del sistema, e nasceva senza un solo test.
Quindi ho fatto girare la verifica avversariale proprio lì, con il mandato di correggere e aggiungere test. L’agente ha tracciato riga per riga il nuovo codice su tre fronti: framing JSON-RPC (chunk parziali, CRLF, id=0, JSON malformato), validazione dello schema, accuratezza della documentazione dopo tutti i cambiamenti.
Il risultato più interessante è un non-risultato. Il sospetto più grosso era che i campi extra non validati potessero finire nei params verso l’apparato — una falla silenziosa e brutta. L’agente ha ispezionato tutti e 36 gli handler e, per il tracciamento, ha confermato che nessuno propaga: l’invariante reggeva. Solo che io quella cosa non l’avevo verificata, l’avevo assunta. Ed è esattamente il tipo di assunzione che, in un team di agenti che si compiacciono a vicenda, passa liscia.
32 test aggiunti dove non ce n’erano: 131 in totale. Bundle byte-identico, l’agente ha aggiunto solo test. Più una piccola imprecisione nella documentazione, corretta.
Un’obiezione onesta, a questo punto: quei test li ha aggiunti un agente, e la gabbia che mi autorizza a potare è costruita dallo stesso tipo di sistema che ha scritto il codice da potare. La circolarità è reale, ed è precisamente il problema per cui quel metodo è nato, l’ho raccontato per esteso in Adversarial verification come metodo e in Agenti disfunzionali, software funzionante, e non lo rifaccio qui: la verifica avversariale serve a ottenere una suite di test reale e senza falsi, non a farsi dire che va tutto bene.
Aggiungo solo il dettaglio che, nel mio caso, ha fatto da ancoraggio esterno: i 99 test iniziali erano stati scritti mentre l’SDK ufficiale era ancora installato, e l’interoperabilità con il client SDK reale su tutti e 28 gli strumenti l’ho verificata prima di rimuovere la dipendenza.
La regola operativa che me ne porto dietro è questa: il livello di scrutinio va mantenuto proporzionale a quanto codice collaudato avete appena rimosso. Se togliete un SDK maturo, la revisione standard non basta. Non perché il vostro codice sia peggiore per forza, ma perché il suo era già stato sbagliato da migliaia di persone prima di voi, mentre il vostro no.
Benefit collaterali
Il footprint. Qui vado più cauto di quanto vorrei, e lo dichiaro prima che me lo facciate notare voi. Pruning e tree shaking sono pratiche di green coding riconosciute, e quello che faccio va nella stessa direzione, solo applicato a mano a un livello più in alto: alle dipendenze invece che ai simboli. Ma non vi vendo chili di CO2 risparmiata. Un server che gira in locale qualche decina di volte al giorno ha un consumo operativo che è un rumore statistico. Il risparmio vero, se c’è, sta nelle 94 dipendenze che non vengono scaricate, aggiornate e ricostruite in CI a ogni push, moltiplicate per chiunque cloni il repository. Il Software Carbon Intensity della Green Software Foundation somma il consumo operativo, E, all’impronta incorporata di hardware e distribuzione, M: questa roba impatta soprattutto sull’M. È meno spettacolare e molto più difendibile.
Il codice specializzato al posto del codice generalista. È quella che mi interessa di più, e paradossalmente è la meno tecnica. Il codice generalista risolve il vostro problema, insieme ad altri duecento problemi che non avete. Funziona, ma vi obbliga a ragionare su astrazioni che non vi servono, a leggere documentazione su casi che non incontrerete, a portarvi dietro configurazioni che esistono per qualcun altro. A sei mesi di distanza, 84 KB che fanno esattamente quello che serve si leggono molto meglio di 1,1 MB che fanno tutto.
La morale
Il codice si scrive perché c’è un obiettivo, non per scrivere codice.
L’AI non ha reso il codice migliore: ha reso più economico esplorare. Ed è un vantaggio che si spreca in due modi opposti: buttando in produzione il primo POC che funziona perché tanto ormai è lì, oppure continuando a fare POC come si facevano prima, con la stessa cautela e lo stesso terrore di sprecare.
Il metodo sta in mezzo, e in tre mosse:
- In esplorazione, libero arbitrio. Nessun vincolo sulle dipendenze. L’obiettivo è la risposta, non il codice.
- Prima di potare, la gabbia. Test unitari e funzionali end-to-end, scritti mentre le dipendenze sono ancora presenti. Senza, non si può fare pruning.
- Poi il cherry picking, tarato sulla manutenzione attesa. Tenete l’indispensabile con una community forte. Sostituite quello il cui rapporto tra casi d’uso offerti e casi d’uso necessari è sbilanciato, ma solo se quel codice non dovrà passare per molte mani, perché lì la libreria generalista sta comprando documentazione e onboarding. E se avete tolto qualcosa di collaudato, alzate il livello di scrutinio in proporzione.
Con un’opzione sempre sul tavolo, quella che tiene tutto il resto onesto: buttare via il POC anche quando ha funzionato.
Quei 94 pacchetti, alla fine, hanno fatto il loro mestiere. Mi hanno portato dritto alla risposta in tre ore. Che poi il loro mestiere finisse lì, e che a tenerli sarei stato io, a lavorare per loro invece del contrario, è la parte che si capisce solo dopo, ed è per quello che il POC va fatto, e non solo pensato.
Se questo modo di lavorare con gli agenti vi interessa, la postura che c’è sotto è quella che ho provato a mettere nero su bianco in Pensare con gli LLM, the Right Way, e che porto in aula nel corso su come costruire agenti AI e imparare a interrogarli. Per portarlo in azienda, scrivetemi.