Menu
Instrumentation ou la mort : Comment j'ai dompté Gemini CLI dans mon projet Android

Instrumentation ou la mort : Comment j'ai dompté Gemini CLI dans mon projet Android

par Nikolay Vlasov

Générer du code est devenu beaucoup trop simple.

J'ai eu une expérience révélatrice : dans un projet personnel, j'ai testé le « vibe-coding » presque sans revue — selon le principe « ce que l'agent écrit, on l'accepte ». Au début, cela semblait rapide et pratique. Cependant, le projet a commencé à se désagréger assez vite : le code est devenu lourd, difficile à maintenir, une partie des fonctionnalités s'est cassée, des ralentissements sont apparus, des fuites de mémoire et, globalement, tout l'éventail de problèmes qui s'accumulent habituellement sur plusieurs mois ; ici, ils sont apparus presque instantanément.

À un certain moment, c'est devenu évident : le problème n'est pas que l'agent écrit du « mauvais » code. Le problème est la vitesse à laquelle ce code s'accumule sans contrôle.


Ce qui casse avec cette approche

Lorsque l'on supprime le contrôle manuel, des schémas typiques commencent à apparaître :

  • contournement des couches architecturales ;
  • duplication de la logique ;
  • frontières de modules floues ;
  • dépendances aléatoires ;
  • complexification inutile du code ;
  • dégradation progressive des performances.

L'important est que tout cela ne se produit pas d'un coup. Chaque étape individuelle semble « normale », mais globalement, le système échappe rapidement à tout contrôle.


Pourquoi un seul prompt ne suffisait pas

Ma première tentative était assez évidente — j'ai rassemblé un grand rules.md où j'ai listé :

  • des contraintes architecturales ;
  • des conventions de nommage (naming conventions) ;
  • des règles de couches ;
  • des accords locaux du projet.

Cela a partiellement aidé, mais n'a pas résolu le problème.

En pratique, il s'est avéré que :

  • un contexte long fonctionne de manière instable ;
  • une partie des règles finit par être ignorée ;
  • le modèle n'applique pas toujours les contraintes de manière cohérente ;
  • le coût et le temps de réponse augmentent avec la taille du prompt.

En fin de compte, j'en suis arrivé à une conclusion plus pragmatique : Les règles importantes doivent être vérifiées, et pas seulement décrites.


1. Konsist : l'architecture comme contrat exécutable

Contraintes architecturales et contrôle de l
Les guardrails architecturaux créent un cadre sécurisé pour le travail de l'agent d'IA

Un agent choisit par défaut le chemin d'implémentation le plus simple. S'il peut contourner une couche, il le fera.

Pour limiter cela, j'ai commencé à décrire l'architecture à l'aide de tests avec Konsist (un outil pour vérifier les règles architecturales du code Kotlin via des tests unitaires).

Exemple :

@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)
    }
}

Deux choses sont ici fondamentales pour moi :

  1. La vérification détecte la violation ;
  2. Le message donne une direction claire sur la manière de la corriger.

L'agent ne travaille plus « dans le vide », mais dans une boucle de rétroaction.


2. Compression des logs : moins de bruit — des itérations plus rapides

L'un des problèmes auxquels j'ai été confronté est le volume des logs.

Si vous donnez à l'agent la sortie complète de Gradle ou de JUnit, il se perd tout simplement dans cette masse. Le contexte se remplit de bruit au lieu de signal.

C'est pourquoi j'ai créé une couche de compression simple :

  • je ne garde que les tests échoués ;
  • j'extrais un court message d'erreur ;
  • je limite la stacktrace ;
  • je supprime tout ce qui est inutile.
Filtrage et compression des logs pour une utilisation efficace de la fenêtre de contexte des LLM
Compression des logs : le filtrage du « bruit » permet au modèle de se concentrer sur les erreurs réelles

Exemple :

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

Après cela, le cycle « ça casse → c'est réparé » est devenu nettement plus rapide et prévisible.


3. Spotless et detekt : une hygiène de base sans intervention humaine

La couche suivante est la vérification automatique de la qualité :

  • Spotless (un outil universel pour le formatage automatique du code) ;
  • detekt (un analyseur statique pour Kotlin pour trouver des problèmes potentiels et des structures complexes).

J'ai cessé de percevoir cela comme des « outils supplémentaires ». Cela fait simplement partie du processus.

Si le code ne passe pas ces vérifications, il n'est pas considéré comme prêt. L'agent revient de lui-même et corrige.

Cela élimine :

  • les petites retouches lors de la revue ;
  • les débats sur le style ;
  • la dégradation progressive de la lisibilité.

4. Traductions : élimination des erreurs mécaniques

Le fichier strings.xml s'est avéré être un point de friction inattendu.

Les LLM font régulièrement des erreurs avec :

  • les apostrophes ;
  • les guillemets ;
  • les séquences d'échappement.

Je n'ai pas essayé d'« éduquer » le modèle à ce sujet via du texte. Il s'est avéré plus simple d'ajouter :

  • une vérification ;
  • une correction automatique ;
  • un traitement par analyse XML.

Important : sans changements globaux brutaux — uniquement des corrections ponctuelles.


5. Performance et taille

Un autre effet qui s'est manifesté avec le temps est la dégradation invisible du produit.

L'agent peut résoudre une tâche, tout en :

  • ajoutant une dépendance lourde ;
  • complexifiant le code ;
  • augmentant la taille de la compilation ;
  • impactant les performances.

Pour suivre cela, j'ai ajouté :

  • des benchmarks de base ;
  • un contrôle de la taille de la compilation.
Suivi des performances et de la taille d
Le suivi constant des indicateurs prévient la dégradation « silencieuse » du produit

Cela ne donne pas une garantie totale, mais permet au moins de repérer les dérives à temps.


6. Pre-commit : un filtre rapide (Mercurial/hg ou Git)

J'ai déporté une partie des vérifications dans le pre-commit :

  • formatage automatique ;
  • linting ponctuel ;
  • vérifications architecturales pour les modules critiques.

L'idée n'est pas de « tout interdire », mais :

  • d'attraper rapidement les problèmes de base ;
  • de ne pas les introduire dans le dépôt ;
  • de ne pas surcharger la revue avec du bruit inutile.

Pourquoi cela fonctionne

L'essence même du développement n'a pas changé. Les tests, les linters et les contraintes architecturales ont toujours été la norme.

Ce qui a changé est autre : la vitesse à laquelle le code apparaît.

Lorsque les modifications sont générées rapidement et en grand volume, le contrôle manuel n'est plus à l'échelle. Ce que l'on pouvait autrefois « attraper à l'œil » n'est plus traitable par manque de temps.

Dans ce contexte, les vérifications automatiques cessent d'être une « bonne pratique » pour devenir une nécessité absolue — simplement pour maintenir le système dans un état stable.


Conclusion

Je ne vois pas cette approche comme universelle ou obligatoire pour tous.

Mais dans mon cas, elle a eu un effet assez tangible — principalement par la réduction de la charge cognitive.

Je n'ai plus besoin de :

  • garder en tête toutes les contraintes ;
  • relire manuellement chaque changement ;
  • analyser d'énormes fichiers de logs ;
  • vérifier constamment les bases.

Le système s'en charge, et j'interviens là où cela a réellement du sens.

Parmi les bénéfices pratiques que j'ai notés :

  • des itérations plus stables sans régressions aléatoires ;
  • un comportement de l'agent prévisible grâce au retour d'information ;
  • un cycle de correction plus rapide ;
  • moins de bruit lors de la revue ;
  • une plus grande facilité à passer à l'échelle dans l'utilisation de l'agent.

Cela ne résout pas tous les problèmes, mais rend le processus de travail avec un agent nettement plus gérable — du moins selon mon expérience.


Liens utiles

  • Konsist — documentation jour sur le lint architectural pour Kotlin.
  • Spotless — outil pour le formatage automatique du code.
  • detekt — analyseur statique de code pour Kotlin.
  • Mercurial (hg) — système de gestion de versions.
  • Maestro — plateforme pour l'automatisation des tests d'interface (UI) mobile.

Dans le prochain article, j'expliquerai comment j'ai ajouté à tout cela l'exécution automatique de scénarios d'interface et la vérification de l'interface via MCP Mobile et Maestro (un framework simple et déclaratif pour les tests d'interface mobile).