Instrumentación o muerte: Cómo domé el Gemini CLI en mi proyecto Android
por Nikolay Vlasov
Generar código se ha vuelto demasiado fácil.
Tuve una experiencia reveladora: en un proyecto personal, probé el "vibe-coding" casi sin revisión, bajo el principio de "lo que escriba el agente, lo aceptamos". Al principio me pareció rápido y cómodo. Sin embargo, pronto el proyecto empezó a desmoronarse: el código se volvió pesado, difícil de mantener, parte de la funcionalidad se rompía, aparecieron retrasos, fugas de memoria y, en general, todo el conjunto de problemas que suelen acumularse durante meses; aquí surgieron casi de inmediato.
En algún momento se hizo evidente: el problema no es que el agente escriba un código "malo". El problema es la velocidad a la que ese código se acumula sin control.
Qué falla con este enfoque
Cuando eliminas el control manual, empiezan a aparecer patrones típicos:
- Evasión de las capas arquitectónicas;
- Duplicación de lógica;
- Límites de módulos difusos;
- Dependencias aleatorias;
- Complejidad innecesaria del código;
- Degradación gradual del rendimiento.
Lo importante es que todo esto no sucede de golpe. Cada paso individual parece "normal", pero en conjunto, el sistema se sale de control rápidamente.
Por qué un solo prompt no fue suficiente
Mi primer intento fue bastante obvio: reuní un gran rules.md donde incluí:
- Restricciones arquitectónicas;
- Convenciones de nomenclatura (naming conventions);
- Reglas de capas;
- Acuerdos locales del proyecto.
Esto ayudó en parte, pero no resolvió el problema.
En la práctica, resultó que:
- El contexto largo funciona de manera inestable;
- Con el tiempo, se ignoran algunas reglas;
- El modelo no siempre aplica las restricciones de manera consistente;
- El coste y el tiempo de respuesta crecen junto con el tamaño del prompt.
Al final, llegué a una conclusión más pragmática: Las reglas importantes deben verificarse, no solo describirse.
1. Konsist: la arquitectura como un contrato ejecutable
Por defecto, el agente elige el camino más sencillo para la implementación. Si puede saltarse una capa, lo hará.
Para limitar esto, empecé a describir la arquitectura mediante pruebas con Konsist (una herramienta para verificar las reglas arquitectónicas del código Kotlin mediante tests unitarios).
Ejemplo:
@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)
}
}
Para mí, aquí hay dos cosas fundamentales:
- La verificación detecta la infracción;
- El mensaje ofrece una dirección clara sobre cómo solucionarla.
El agente deja de trabajar "en el vacío" y pasa a trabajar dentro de un bucle de retroalimentación.
2. Compresión de logs: menos ruido, iteraciones más rápidas
Uno de los problemas que encontré fue el volumen de los logs.
Si le entregas al agente la salida completa de Gradle o JUnit, simplemente se pierde en ese volumen. El contexto se llena de ruido en lugar de señales.
Por eso, creé una capa sencilla de compresión:
- Mantengo solo los tests fallidos;
- Extraigo un mensaje de error corto;
- Limito el stacktrace;
- Elimino todo lo innecesario.
Ejemplo:
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
Después de esto, el ciclo "se rompe → se arregla" se volvió notablemente más rápido y predecible.
3. Spotless y detekt: higiene básica sin intervención humana
La siguiente capa es la verificación automática de calidad:
- Spotless (una herramienta universal para el formateo automático de código);
- detekt (un analizador estático para Kotlin que busca problemas potenciales y estructuras complejas).
He dejado de ver estas herramientas como "adicionales". Son simplemente parte del proceso.
Si el código no supera estas verificaciones, no se considera terminado. El propio agente vuelve atrás y lo corrige.
Esto elimina:
- Pequeñas correcciones en las revisiones;
- Discusiones sobre el estilo;
- La degradación gradual de la legibilidad.
4. Traducciones: eliminación de errores mecánicos
El archivo strings.xml resultó ser un punto de problemas inesperado.
Los LLM cometen errores regularmente con:
- Apóstrofes;
- Comillas;
- Secuencias de escape.
No intenté "enseñar" esto al modelo mediante texto. Resultó más sencillo añadir:
- Una verificación;
- Corrección automática;
- Trabajo mediante el análisis de XML.
Importante: sin cambios globales bruscos, solo correcciones puntuales.
5. Rendimiento y tamaño
Un efecto aparte que se manifestó con el tiempo fue la degradación invisible del producto.
El agente puede resolver la tarea, pero al mismo tiempo:
- Añadir una dependencia pesada;
- Complicar el código;
- Aumentar el tamaño de la compilación;
- Afectar al rendimiento.
Para rastrear esto, añadí:
- Benchmarks básicos;
- Control del tamaño de la compilación.
Esto no ofrece una garantía total, pero permite al menos detectar desviaciones a tiempo.
6. Pre-commit: filtro rápido (Mercurial/hg o Git)
Moví parte de las verificaciones al pre-commit:
- Formateo automático;
- Linting puntual;
- Verificaciones arquitectónicas para módulos críticos.
La idea no es "prohibirlo todo", sino:
- Capturar rápidamente problemas básicos;
- No introducirlos en el repositorio;
- No sobrecargar la revisión con ruido innecesario.
Por qué funciona
La esencia del desarrollo no ha cambiado. Los tests, linters y restricciones arquitectónicas siempre han sido la norma.
Lo que ha cambiado es otra cosa: la velocidad a la que aparece el código.
Cuando los cambios se generan rápido y en gran volumen, el control manual deja de ser escalable. Lo que antes se podía "ver a simple vista", ahora simplemente no da tiempo a procesarlo.
En este contexto, las verificaciones automáticas dejan de ser una "buena práctica" y se convierten en una necesidad básica, simplemente para mantener el sistema en un estado estable.
Conclusión
No considero este enfoque como universal u obligatorio para todos.
Sin embargo, en mi caso, ha tenido un efecto bastante notable, principalmente al reducir la carga cognitiva.
Ya no necesito:
- Tener presentes todas las restricciones;
- Revisar manualmente cada cambio;
- Analizar logs enormes;
- Verificar constantemente cosas básicas.
El sistema se encarga de ello y yo intervengo donde realmente tiene sentido.
Entre los beneficios prácticos que he notado:
- Iteraciones más estables sin regresiones aleatorias;
- Comportamiento predecible del agente gracias a la retroalimentación;
- Un ciclo de corrección más rápido;
- Menos ruido en las revisiones;
- Es más fácil escalar el uso del agente.
Esto no resuelve todos los problemas, pero hace que el proceso de trabajar con un agente sea notablemente más gestionable, al menos según mi experiencia.
Enlaces útiles
- Konsist: documentación actualizada sobre el lint arquitectónico para Kotlin.
- Spotless: herramienta para el formateo automático de código.
- detekt: analizador de código estático para Kotlin.
- Mercurial (hg): sistema de control de versiones.
- Maestro: plataforma para la automatización de pruebas de interfaz de usuario (UI) en aplicaciones móviles.
En el próximo artículo analizaré cómo añadí a esto la ejecución automática de escenarios de interfaz de usuario y la verificación de la interfaz mediante MCP Mobile y Maestro (un framework para pruebas de UI de aplicaciones móviles sencillo y declarativo).