La mia chat AI locale con Python e Gemma — Parte 14: consigli pratici per il vibe coding

  • Home
  • AI & ChatBot
  • La mia chat AI locale con Python e Gemma — Parte 14: consigli pratici per il vibe coding
Image

La mia chat AI locale con Python e Gemma — Parte 14: consigli pratici per il vibe coding

Scrivere software parlando con un’AI è diventato sorprendentemente normale. Descriviamo una funzione, chiediamo una modifica, incolliamo un errore e qualche secondo dopo ci troviamo davanti a decine o centinaia di righe di codice. La sensazione, soprattutto le prime volte, è quasi pericolosa, sembra che la parte difficile della programmazione sia sparita.

Non è così.

Il vibe coding può accelerare enormemente alcune fasi dello sviluppo, ma non sostituisce l’ingegneria del software. Anzi, più codice lasciamo produrre all’AI, più diventa importante sapere cosa stiamo facendo.

Il termine è stato reso popolare da Andrej Karpathy nel febbraio 2025 per descrivere un modo di programmare nel quale si comunica principalmente attraverso il linguaggio naturale, lasciando al modello gran parte della produzione materiale del codice. Nella versione più estrema si smette quasi di leggere ciò che viene generato, si osserva il risultato, si comunica cosa non funziona e si continua a iterare.

Per un esperimento del sabato pomeriggio può essere anche divertente. Per un software che dovrà vivere qualche anno, essere aggiornato, debuggato, messo in produzione e magari modificato da altre persone, serve un metodo completamente diverso.

Il vibe coding non fa magie

Un modello può scrivere una funzione Python più velocemente di noi. Può creare test, API, classi, query, migrazioni e documentazione. Può leggere una codebase e proporre modifiche distribuite su diversi file.

Ma c’è una differenza importante tra produrre codice e progettare software.

Se chiediamo:

Crea un’applicazione Python per gestire clienti, ordini, pagamenti e notifiche.

l’AI può tranquillamente produrre qualcosa che funziona. Il problema è capire cosa significhi esattamente “funziona”.

Funziona con dieci clienti? Con centomila? Cosa succede quando il gateway di pagamento non risponde? Un ordine viene salvato prima o dopo il pagamento? Come gestiamo un retry? La funzione che invia una mail può bloccare una transazione? Chi può modificare un ordine? Dove vengono validate le autorizzazioni? Cosa succede durante una migrazione del database?

Queste domande non sono dettagli implementativi. Sono il software.

Ed è qui che il ruolo del programmatore cambia. Scriviamo forse meno codice manualmente, ma dobbiamo prendere molte più decisioni.

Per usarlo seriamente serve ancora un senior

Una persona con poca esperienza può usare il vibe coding per creare un prototipo. Questa è probabilmente una delle caratteristiche più interessanti di questi strumenti.

Ma dirigere proficuamente un agente AI su una codebase reale è un altro mestiere.

Serve qualcuno capace di riconoscere una cattiva separazione delle responsabilità, una dipendenza sbagliata, una race condition, una query destinata a diventare lenta, un problema di sicurezza o una scelta architetturale che oggi sembra innocua e tra sei mesi diventerà costosa.

Il senior developer non è semplicemente quello che conosce più sintassi Python. È quello che guarda una soluzione apparentemente funzionante e pensa: “Sì, funziona, ma questa cosa ci esploderà in mano appena aggiungiamo il secondo caso d’uso”.

Con il vibe coding questa competenza diventa ancora più importante.

L’AI è velocissima nel trasformare una decisione in codice. Proprio per questo dobbiamo stare molto attenti alle decisioni che le lasciamo prendere.

Il problema del codice monolitico

Uno dei comportamenti da tenere sotto controllo è la tendenza a concentrare parecchia logica nello stesso punto.

Immaginiamo di chiedere di implementare la registrazione di un ordine. Potremmo ritrovarci con qualcosa del genere:

Python
def create_order(user, products, database, payment_client, email_client):
    if not user:
        raise ValueError("Utente non valido")

    total = 0

    for product in products:
        if product.stock <= 0:
            raise ValueError("Prodotto non disponibile")

        total += product.price

    payment = payment_client.charge(
        user.credit_card,
        total,
    )

    if not payment.success:
        raise RuntimeError("Pagamento fallito")

    order = database.orders.create(
        user_id=user.id,
        products=products,
        total=total,
        payment_id=payment.id,
    )

    for product in products:
        product.stock -= 1
        database.products.save(product)

    email_client.send(
        user.email,
        "Ordine confermato",
        f"Il tuo ordine {order.id} è stato confermato",
    )

    return order

È leggibile. È persino plausibile. E probabilmente, opportunamente completata, la funzione funziona.

Ma dentro abbiamo validazione, disponibilità prodotti, pricing, pagamento, persistenza, aggiornamento inventario e notifiche.

Il problema emerge al secondo giro di modifiche.

“Adesso aggiungi i coupon.”

“Adesso alcuni clienti hanno prezzi differenti.”

“Adesso supportiamo PayPal.”

“Adesso il pagamento può essere asincrono.”

“Adesso le mail devono essere inviate attraverso una coda.”

Se continuiamo a chiedere modifiche senza imporre un disegno, l’agente tenderà spesso ad aggiungere condizioni alla struttura esistente, perché modificare qualcosa che già funziona è la strada localmente più semplice.

Non è soltanto una sensazione di chi utilizza questi strumenti. Una ricerca del 2026 sull’architettura del codice prodotto da LLM e coding agent ha osservato, nei sistemi analizzati, una preferenza per metodi lunghi e strategie ad alto accoppiamento, arrivando a descrivere una sorta di “modularità apparente”: file separati che continuano però a dipendere troppo gli uni dagli altri. Gli autori invitano comunque alla cautela nel generalizzare i risultati a qualsiasi agente o workflow.

Un altro benchmark dedicato allo sviluppo iterativo ha osservato un progressivo aumento di verbosità e concentrazione della complessità durante successive modifiche effettuate dagli agenti. È particolarmente interessante perché riproduce proprio quello che facciamo nel vibe coding, aggiungiamo una feature, poi un’altra, poi correggiamo qualcosa, poi estendiamo nuovamente il programma. Quindi non dobbiamo limitarci a dire all’AI cosa deve fare il software. Dobbiamo dirle anche dove devono vivere le responsabilità.

Prima le cartelle, poi il codice

Quando parto da un progetto nuovo preferisco non chiedere immediatamente di implementare funzionalità. Prima definisco lo scheletro. Per esempio, una piccola applicazione Python potrebbe partire così:

Python
project/

├── src/
│   └── app/
│       ├── domain/
│       ├── services/
│       ├── repositories/
│       ├── integrations/
│       ├── api/
│       └── config/

├── tests/
│   ├── unit/
│   ├── integration/
│   └── fixtures/

├── docs/
│   ├── ARCHITECTURE.md
│   ├── DECISIONS.md
│   ├── WISHLIST.md
│   └── CONTEXT.md

├── CLAUDE.md
├── pyproject.toml
└── README.md

Non considero questa struttura universale. Cambierà in base al progetto.

Quello che conta è il principio: l’architettura non dovrebbe essere un sottoprodotto accidentale delle conversazioni con l’AI.

Se abbiamo deciso che il dominio non deve conoscere il database, quella regola deve esistere prima che vengano generate cinquanta classi.

Diamo una memoria al progetto

Un altro errore comune consiste nell’affidare tutte le regole alla cronologia della chat.

  • “Ricordati che usiamo repository astratti.”
  • “Ricordati che non voglio logica di dominio negli endpoint.”
  • “Ricordati che ogni nuova feature deve avere test.”
  • “Ricordati che non devi aggiungere dipendenze senza chiedere.”

Dopo qualche sessione la situazione diventa ingestibile. Le direttive importanti devono invece diventare parte del repository.

Con Claude Code possiamo utilizzare CLAUDE.md. La documentazione ufficiale lo definisce precisamente come un file destinato a fornire istruzioni persistenti relative al progetto, comprese convenzioni, architettura, workflow e comandi di test. È importante però ricordare un dettaglio, queste direttive vengono trattate come contesto, non come vincoli tecnicamente impossibili da violare. Anthropic raccomanda inoltre istruzioni concise, specifiche e ben strutturate.

Io separerei però le informazioni.

CLAUDE.md dovrebbe contenere le regole operative che vogliamo vengano considerate continuamente, struttura del progetto, vincoli architetturali, standard Python, strategia dei test, operazioni vietate e comportamento da seguire prima di modificare il codice.

ARCHITECTURE.md descriverà invece come è costruito il sistema e quali responsabilità appartengono ai diversi layer.

DECISIONS.md conserverà le decisioni già prese. Per esempio: “abbiamo scelto SQLAlchemy invece di query SQL sparse nel progetto”, oppure “gli eventi applicativi vengono pubblicati dopo il commit della transazione”.

WISHLIST.md è volutamente diverso. Qui possiamo raccogliere idee e funzionalità desiderate che non sono ancora requisiti da implementare. È una distinzione preziosa perché un agente molto zelante potrebbe interpretare un’idea letta nella documentazione come qualcosa da realizzare immediatamente.

CONTEXT.md può contenere terminologia del dominio, vincoli aziendali, riferimenti tecnici e informazioni che aiutano il modello a capire il perché delle nostre scelte.

Se lavoriamo con più coding agent possiamo anche mantenere un file comune come AGENTS.md. Claude Code, per esempio, documenta la possibilità di importarlo da CLAUDE.md, evitando di duplicare tutte le istruzioni per strumenti differenti.

La memoria dell’agente, quindi, non dovrebbe essere una gigantesca discarica di prompt precedenti. Dovrebbe assomigliare alla documentazione che avremmo voluto trovare entrando per la prima volta in un buon progetto.

Prima ragiona. Poi eventualmente scrivi

Questa è probabilmente la regola che ha migliorato maggiormente il mio modo di usare gli agenti di coding.

Per una modifica non banale, non chiederei: “Implementa il sistema di caching”.

Chiederei qualcosa di più vicino a: “Analizza il progetto e individua dove sarebbe opportuno introdurre il caching. Non modificare nessun file. Descrivi le alternative possibili, i vantaggi, gli svantaggi, i rischi di invalidazione della cache e quali componenti verrebbero coinvolti. Proponi una strategia e attendi la mia approvazione.”

A quel punto leggiamo.

  • Possiamo contestare la soluzione.
  • Possiamo chiedere perché Redis serva davvero.
  • Possiamo decidere che per il nostro carico basti una cache in memoria.
  • Possiamo accorgerci che stiamo cercando di mettere una cache sopra una query progettata male.

Solo dopo approviamo la strategia.

A quel punto possiamo chiedere all’agente di trasformare quella decisione in codice.

Gli strumenti stessi si stanno muovendo in questa direzione. Claude Code, per esempio, consiglia attualmente di separare esplorazione, pianificazione e implementazione attraverso il Plan Mode, proprio per evitare di iniziare a modificare il repository prima di aver compreso il problema. Questa distinzione cambia completamente il rapporto con l’AI. Non stiamo più dicendo: “Fammi il software”. Stiamo dicendo: “Studia il problema, proponimi delle alternative. Io scelgo. Poi implementa ciò che abbiamo deciso”.

Una cosa per volta

Il prompt peggiore è spesso quello più ambizioso. Aggiungi autenticazione OAuth, sistema di ruoli, reset password, logging, rate limiting e modifica gli endpoint esistenti. Potrebbe funzionare. Potrebbe anche modificare trenta file, aggiungere tre dipendenze, cambiare il modello dati e introdurre un bug che scopriremo due ore più tardi senza sapere esattamente quando è comparso.

Meglio lavorare per operazioni atomiche. Prima definiamo il modello degli utenti. Controlliamo il diff. Eseguiamo i test. Commit. Poi implementiamo l’hashing delle password. Controlliamo. Test. Commit. Poi login. Poi autorizzazioni. Poi eventualmente OAuth.

Può sembrare più lento, ma rende il vibe coding molto più veloce sul medio periodo perché riduce drasticamente il costo del momento più doloroso di tutto il processo, capire quale modifica abbia rotto qualcosa.

La velocità non è quante righe riusciamo a generare in un minuto. È quanto tempo passa tra un requisito e una modifica affidabile che possiamo lasciare serenamente nel repository.

Quando la documentazione dobbiamo portarla noi

Gli LLM non possiedono una conoscenza perfetta e continuamente aggiornata di ogni libreria. Questo problema diventa evidente con framework giovani, versioni appena pubblicate, SDK modificati recentemente oppure repository che evolvono molto velocemente. Se stiamo usando una versione recente di una libreria Python e l’agente continua a proporre API vecchie, insistere con altri cinque prompt raramente è la soluzione migliore.

Dobbiamo fornirgli la fonte corretta. Possiamo indicargli la documentazione ufficiale pertinente, il repository Git corretto, la release che stiamo utilizzando, esempi ufficiali e, quando serve particolare precisione, persino il commit o il tag al quale deve fare riferimento.

Poi possiamo chiedergli: Studia esclusivamente questa documentazione e questi sorgenti per capire l’API. Non implementare ancora nulla. Dimmi quale approccio useresti nel nostro progetto e segnala eventuali incompatibilità con il codice esistente.

Prima conoscenza. Poi strategia. Poi approvazione. Solo alla fine codice. Il modello non dovrebbe essere costretto a indovinare qualcosa che possiamo fornirgli in forma deterministica.

I test diventano ancora più importanti

Quando scriviamo manualmente una funzione, mentre programmiamo costruiamo anche un modello mentale di come funziona. Quando quella funzione viene generata in pochi secondi dall’AI, rischiamo di saltare proprio quella fase. I test diventano quindi una delle migliori interfacce tra sviluppatore e agente. I test sono un contratto/comportamento a cui l’agente deve sottostare.

Può cambiare internamente l’implementazione, ma deve continuare a rispettare quel comportamento.Il passaggio successivo è ancora più interessante: prima di una feature possiamo chiedere all’AI di proporre i casi di test, approvarli noi e solo successivamente autorizzarla a implementare il codice necessario per farli passare. In questo modo utilizziamo l’AI prima come analista del problema e soltanto dopo come generatore.

Autocritica

Un agente può produrre codice molto convincente. Nomi corretti. Commenti eleganti. Pattern riconoscibili. Tipizzazione. Docstring. Test verdi.Niente di tutto questo garantisce che la modifica sia giusta. Un metodo utile consiste nel chiedere, dopo l’implementazione, un secondo passaggio nel quale l’agente non deve più scrivere codice ma deve criticare ciò che ha appena fatto.

Per esempio: “Non modificare nulla. Analizza il diff appena prodotto come se stessi effettuando una code review su codice scritto da un altro sviluppatore. Cerca bug, regressioni, problemi di sicurezza, duplicazioni, accoppiamento eccessivo, casi limite non coperti e violazioni di ARCHITECTURE.md. Segnala soltanto i problemi. Non correggerli.

Poi controlliamo il risultato. E soltanto dopo decidiamo quali correzioni effettuare, sempre una per volta. Far generare e revisionare nello stesso impulso porta facilmente il modello a difendere implicitamente la propria soluzione. Separare le due attività produce un contesto mentale differente e, soprattutto, permette ancora una volta a noi di rimanere nel circuito decisionale.

Le precauzioni

Quando un agente può modificare direttamente il repository, preferisco adottare alcune regole abbastanza rigide:

  • Git sempre, e commit piccoli. Ogni modifica significativa deve poter essere annullata senza chirurgia manuale.
  • Branch dedicati alle modifiche AI. Una sperimentazione non dovrebbe contaminare immediatamente il ramo stabile.
  • Backup frequenti anche fuori da Git. Database di sviluppo, configurazioni, file caricati dagli utenti e altri dati non versionati devono avere una strategia di ripristino.
  • Test prima e dopo ogni intervento. Se i test erano già rossi prima della modifica dobbiamo saperlo.
  • Diff piccoli e leggibili. Se una richiesta produce centinaia di righe modificate in aree diverse, probabilmente abbiamo chiesto troppe cose contemporaneamente.
  • Niente dipendenze nuove senza approvazione. Una libreria aggiunta per risparmiare venti righe diventa una dipendenza da mantenere per anni.
  • Migrazioni del database trattate separatamente. Schema, migrazione dei dati e modifica applicativa non dovrebbero diventare un unico gigantesco intervento.
  • Segreti fuori dal contesto quando non sono necessari. Token, password, chiavi private e credenziali di produzione non devono finire casualmente in prompt, log o file di memoria.
  • Linting, type checking e analisi statica automatizzati. In Python strumenti come Ruff, mypy o Pyright possono fornire un controllo deterministico che non dipende dall’opinione dell’LLM.
  • Test di integrazione per i confini importanti. Database, code, filesystem, servizi esterni e API meritano controlli che vadano oltre il singolo unit test.
  • Niente refactoring nascosti dentro una feature. Se l’agente pensa che una parte del progetto debba essere rifattorizzata, prima deve proporre il refactoring e ottenere l’approvazione.
  • Misurare prima di ottimizzare. Se propone cache, parallelismo o query più complesse, chiediamo benchmark e motivazioni.
  • Controllare sempre autenticazione e autorizzazione. Sono aree nelle quali un codice apparentemente elegante può produrre conseguenze molto serie.
  • Chiedere esplicitamente cosa non ha verificato. È una domanda sorprendentemente utile: costringe l’agente a rendere visibili i punti nei quali sta facendo supposizioni.
  • Registrare le decisioni importanti. Se abbiamo rifiutato una strategia per un motivo preciso, inseriamolo in DECISIONS.md affinché la stessa discussione non ricominci fra tre settimane.

Il vero cambio di mentalità

Il vibe coding diventa pericoloso quando trasformiamo l’AI in un programmatore al quale diciamo soltanto cosa vogliamo vedere sullo schermo. Diventa molto più interessante quando la trattiamo come un esecutore estremamente veloce inserito dentro un processo ingegneristico controllato.

Paradossalmente, più gli strumenti diventano capaci di scrivere codice, meno il vantaggio competitivo del programmatore risiede nella velocità con cui digita codice.

Contano architettura, capacità di scomporre problemi, conoscenza del dominio, debugging, sicurezza, valutazione dei compromessi e capacità di riconoscere una soluzione fragile prima che diventi un problema.

Il programmatore senior diventa quindi una specie di direttore tecnico dell’agente. Decide la struttura, costruisce il contesto, stabilisce i confini, approva la strategia, osserva il diff e mantiene il diritto di dire no.

Il vibe coding funziona bene quando smettiamo di chiedere all’AI di pensare e decidere tutto al posto nostro e iniziamo a usarla per trasformare molto rapidamente decisioni già comprese in software verificabile.

A quel punto il codice può arrivare davvero molto più velocemente.

La responsabilità, però, rimane ancora tutta nostra.

Se l’articolo ti è piaciuto restiamo in contato su linkedin a https://www.linkedin.com/in/andreatonin/

Releated Posts

Private Vision: i LoRA e la matematica della personalizzazione — Parte 4

La cartella loras/ di Private Vision era piena di roba fatta da altri. Funzionava, per carità: Private Vision…

DiByAndrea Tonin Set 9, 2026

Dalla Skill all’Agente AI: automatizzare le note spese con ChatGPT e Google Workspace

Prima abbiamo distinto Progetti, GPT e Skill. Un Progetto è uno spazio di lavoro, un GPT è un…

DiByAndrea Tonin Ago 23, 2026

Anatomia di una Skill di ChatGPT: quando il prompt diventa una procedura

Negli ultimi mesi abbiamo imparato a scrivere prompt sempre più strutturati. Non più il classico “fammi un testo…

DiByAndrea Tonin Ago 23, 2026

Progetti, GPT e Skill: la differenza spiegata bene

Negli ultimi anni ChatGPT è passato dall’essere “una chat intelligente” a diventare un piccolo ecosistema di strumenti: Progetti,…

DiByAndrea Tonin Ago 23, 2026