Strumentazione o morte: come ho domato Gemini CLI nel mio progetto Android
Generare codice è diventato troppo facile.
Ho avuto un'esperienza significativa: in un progetto personale, ho provato il "vibe-coding" quasi senza revisione — seguendo il principio "ciò che l'agente scrive, accettiamo". All'inizio sembrava veloce e comodo. Ma in breve tempo il progetto ha iniziato a sgretolarsi: il codice è diventato ingombrante, difficile da manutenere, parte della funzionalità si rompeva, sono comparsi ritardi, perdite di memoria e, in generale, l'intero spettro di problemi che solitamente si accumulano in mesi — qui sono emersi quasi subito.
A un certo punto è diventato evidente: il problema non è che l'agente scrive codice "cattivo". Il problema è la velocità con cui questo codice si accumula senza controllo.
Cosa si rompe con questo approccio
Quando si rimuove il controllo manuale, iniziano a manifestarsi schemi tipici:
- aggiramento dei livelli architettonici;
- duplicazione della logica;
- confini dei moduli sfumati;
- dipendenze casuali;
- complicazione del codice senza necessità;
- graduale degrado delle prestazioni.
L'importante è che tutto questo non accada istantaneamente. Ogni singolo passo sembra "normale", ma complessivamente il sistema sfugge rapidamente al controllo.
Perché un solo prompt non è bastato
Il mio primo tentativo è stato scontato — ho raccolto un grande rules.md dove ho inserito:
- restrizioni architettoniche;
- naming conventions;
- regole dei livelli;
- accordi locali del progetto.
Questo ha aiutato in parte, ma non ha risolto il problema.
Nella pratica è emerso che:
- un contesto lungo funziona in modo instabile;
- parte delle regole viene ignorata col tempo;
- il modello non applica sempre le restrizioni in modo coerente;
- il costo e il tempo di risposta crescono con la dimensione del prompt.
Alla fine sono arrivato a una conclusione più pragmatica: Le regole importanti devono essere verificate, non solo descritte.
1. Konsist: l'architettura come contratto eseguibile
Per impostazione predefinita, l'agente sceglie la via di implementazione più semplice. Se può saltare un livello — lo farà.
Per limitare questo, ho iniziato a descrivere l'architettura tramite test con Konsist (uno strumento per verificare le regole architettoniche del codice Kotlin tramite test unitari).
Esempio:
@Test
fun `use cases should have UseCase suffix and reside in domain package`() {
val classes = Konsist.scopeFromProject().classes()
val violations = classes
.filter { classDeclaration ->
classDeclaration.name?.endsWith("UseCase") != true ||
!classDeclaration.resideInPackage("com.core.domain")
}
if (violations.isNotEmpty()) {
val message = buildString {
appendLine("VIOLATION: UseCase naming conventions not followed")
appendLine("FIX: Rename class to end with 'UseCase'")
appendLine("FIX: Move class to com.core.domain package")
}
fail(message)
}
}
Qui sono fondamentali per me due cose:
- La verifica rileva la violazione;
- Il messaggio fornisce una direzione chiara su come risolverla.
L'agente smette di lavorare "nel vuoto" e inizia a lavorare all'interno di un ciclo di feedback.
2. Compressione dei log: meno rumore — iterazioni più veloci
Uno dei problemi che ho riscontrato è il volume dei log.
Se dai all'agente l'intero output di Gradle o JUnit, si perde semplicemente in quel volume. Il contesto si riempie di rumore, non di segnale.
Per questo ho creato un semplice livello di compressione:
- mantengo solo i test falliti;
- prendo un breve messaggio d'errore;
- limito lo stacktrace;
- rimuovo tutto il superfluo.
Esempio:
def parse_xml_reports(root_dir):
summary = []
for testcase in root.findall(".//testcase"):
failure = testcase.find("failure")
if failure is not None:
message = failure.get("message", "No message")
text = failure.text or ""
stacktrace = "\n".join(text.strip().split("\n")[:15])
summary.append(f"FAILED: {testcase.get('name')}")
summary.append(f"Message: {message}")
summary.append(f"Stacktrace:\n{stacktrace}\n")
return summary
Dopo di ciò il ciclo „si rompe → ripariamo“ è diventato notevolmente più veloce e prevedibile.
3. Spotless e detekt: igiene di base senza intervento umano
Il livello successivo — verifica automatica della qualità:
- Spotless (uno strumento universale per la formattazione automatica del codice);
- detekt (analizzatore statico per Kotlin per cercare potenziali problemi e strutture complesse).
Ho smesso di percepirli come „strumenti aggiuntivi“. Sono semplicemente parte del processo.
Se il codice non supera queste verifiche — non è considerato pronto. L'agente torna indietro e corregge autonomamente.
Questo elimina:
- micro-correzioni in fase di revisione;
- dispute sullo stile;
- graduale degrado della leggibilità.
4. Traduzioni: eliminazione di errori meccanici
strings.xml si è rivelato un punto di criticità inaspettato.
Gli LLM sbagliano regolarmente con:
- apostrofi;
- virgolette;
- sequenze di escape.
Non ho provato a „istruire“ il modello su questo tramite testo. È risultato più semplice aggiungere:
- una verifica;
- correzione automatica;
- lavoro tramite parsing XML.
Importante: senza modifiche globali drastiche — solo correzioni mirate.
5. Prestazioni e dimensioni
Un effetto collaterale manifestatosi col tempo è stato il degrado invisibile del prodotto.
L'agente può risolvere il compito, ma nel frattempo:
- aggiungere una dipendenza pesante;
- complicare il codice;
- aumentare la dimensione della build;
- influire sulle prestazioni.
Per monitorare questo, ho aggiunto:
- benchmark di base;
- controllo della dimensione della build.
Non dà una garanzia totale, ma permette almeno di notare per tempo le deviazioni.
6. Pre-commit: filtro rapido (Mercurial/hg o Git)
Parte delle verifiche le ho spostate nel pre-commit:
- auto-formattazione;
- linting puntuale;
- verifiche architettoniche per moduli critici.
L'idea non è „vietare tutto“, ma:
- catturare rapidamente i problemi di base;
- non portarli nel repository;
- non sovraccaricare la revisione con rumore inutile.
Perché funziona
L'essenza dello sviluppo non è cambiata. Test, linter e restrizioni architettoniche sono sempre stati la norma.
È cambiato altro: la velocità con cui appare il codice.
Quando le modifiche vengono generate velocemente e in grandi volumi, il controllo manuale smette di scalare. Ciò che prima si poteva „catturare a occhio“, ora semplicemente non si ha il tempo di processarlo.
In questo contesto, le verifiche automatiche smettono di essere una „buona pratica“ e diventano una necessità di base — semplicemente per mantenere il sistema in uno stato stabile.
Conclusione
Non percepisco questo approccio come universale o obbligatorio per tutti.
Ma nel mio caso ha dato un effetto tangibile — prima di tutto grazie alla riduzione del carico cognitivo.
Non ho più bisogno di:
- tenere a mente tutte le restrizioni;
- leggere manualmente ogni modifica;
- analizzare log enormi;
- ricontrollare costantemente le basi.
Il sistema se ne occupa, e io intervengo dove ha davvero senso.
Tra i vantaggi pratici che ho notato:
- iterazioni più stabili senza regressioni casuali;
- comportamento dell'agente prevedibile grazie al feedback;
- ciclo di correzione più veloce;
- meno rumore in fase di revisione;
- più facile scalare l'uso dell'agente.
Questo non risolve tutti i problemi, ma rende il processo di lavoro con un agente decisamente più gestibile — almeno secondo la mia esperienza.
Link utili
- Konsist — documentazione aggiornata sul lint architettonico per Kotlin.
- Spotless — strumento per la formattazione automatica del codice.
- detekt — analizzatore statico di codice per Kotlin.
- Mercurial (hg) — sistema di controllo versione.
- Maestro — piattaforma per l'automazione dei test UI mobile.
Nel prossimo articolo analizzerò come ho aggiunto a tutto questo l'esecuzione automatica di scenari UI e la verifica dell'interfaccia tramite MCP Mobile e Maestro (framework per test UI mobile semplici e dichiarativi).