Oprzyrządowanie albo śmierć: Jak okiełznałem Gemini CLI w moim projekcie Android
przez Nikolay Vlasov
Generowanie kodu stało się zbyt proste.
Miałem pouczające doświadczenie: w jednym z moich pet-projektów wypróbowałem „vibe-coding” prawie bez żadnego review – zgodnie z zasadą „akceptujemy to, co napisał agent”. Początkowo wydawało się to szybkie i wygodne. Jednak dość szybko projekt zaczął się rozpadać: kod stał się pękaty, trudny w utrzymaniu, części funkcjonalności przestały działać, pojawiły się opóźnienia, wycieki pamięci i ogólnie cały zestaw problemów, które zazwyczaj gromadzą się miesiącami – tutaj wystąpiły niemal natychmiast.
W pewnym momencie stało się oczywiste: problemem nie jest to, że agent pisze „zły” kod. Problemem jest szybkość, z jaką ten kod gromadzi się bez żadnej kontroli.
Co psuje się przy takim podejściu
Kiedy usuwasz ręczną kontrolę, zaczynają pojawiać się typowe wzorce:
- Omijanie warstw architektonicznych;
- Duplikowanie logiki;
- Rozmyte granice modułów;
- Przypadkowe zależności;
- Niepotrzebne komplikowanie kodu;
- Stopniowa degradacja wydajności.
Ważne jest to, że nie dzieje się to od razu. Każdy pojedynczy krok wygląda „normalnie”, ale sumarycznie system szybko wymyka się spod kontroli.
Dlaczego jeden prompt okazał się niewystarczający
Moja pierwsza próba była dość oczywista – zebrałem duży plik rules.md, w którym opisałem:
- Ograniczenia architektoniczne;
- Konwencje nazewnictwa (naming conventions);
- Zasady dotyczące warstw;
- Lokalne ustalenia projektowe.
To częściowo pomogło, ale nie rozwiązało problemu.
W praktyce okazało się, że:
- Długi kontekst działa niestabilnie;
- Część zasad z czasem jest ignorowana;
- Model nie zawsze konsekwentnie stosuje ograniczenia;
- Koszt i czas odpowiedzi rosną wraz z rozmiarem promptu.
W końcu doszedłem do bardziej pragmatycznego wniosku: Ważne zasady muszą być sprawdzane, a nie tylko opisywane.
1. Konsist: Architektura jako wykonywalny kontrakt
Agent domyślnie wybiera najprostszą drogę implementacji. Jeśli można pominąć warstwę – zrobi to.
Aby to ograniczyć, zacząłem opisywać architekturę poprzez testy z wykorzystaniem Konsist (narzędzie do sprawdzania zasad architektonicznych kodu Kotlin za pomocą testów jednostkowych).
Przykład:
@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)
}
}
Tutaj kluczowe są dla mnie dwie rzeczy:
- Sprawdzenie rejestruje naruszenie;
- Komunikat daje jasne wskazówki, jak je naprawić.
Agent przestaje pracować „w próżni” i zaczyna działać w pętli informacji zwrotnej.
2. Kompresja logów: mniej szumu – szybsze iteracje
Jednym z problemów, z którymi się zetknąłem, był ogrom logów.
Jeśli przekażesz agentowi pełny wynik z Gradle lub JUnit, po prostu gubi się on w tej masie. Kontekst wypełnia się szumem zamiast sygnałem.
Dlatego stworzyłem prostą warstwę kompresji:
- Zostawiam tylko testy, które nie przeszły;
- Wyciągam krótki komunikat o błędzie;
- Ograniczam stacktrace;
- Usuwam wszystko, co zbędne.
Przykład:
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
Po tym cykl „zepsuło się → naprawiliśmy” stał się zauważalnie szybszy i bardziej przewidywalny.
3. Spotless i detekt: podstawowa higiena bez udziału człowieka
Kolejna warstwa – automatyczne sprawdzanie jakości:
- Spotless (uniwersalne narzędzie do automatycznego formatowania kodu);
- detekt (statyczny analizator dla Kotlina do wyszukiwania potencjalnych problemów i skomplikowanych konstrukcji).
Przestałem postrzegać to jako „dodatkowe narzędzia”. To po prostu część procesu.
Jeśli kod nie przechodzi tych testów – nie jest uznawany za gotowy. Agent sam wraca i poprawia błędy.
To eliminuje:
- Drobne poprawki na etapie review;
- Spory o styl;
- Stopniową degradację czytelności.
4. Tłumaczenia: eliminacja błędów mechanicznych
Plik strings.xml okazał się niespodziewanym punktem zapalnym.
Modele LLM regularnie mylą się przy:
- Apostrofach;
- Cudzysłowach;
- Sekwencjach ucieczki (escape characters).
Nie próbowałem „uczyć” tego modelu poprzez tekst promptu. Łatwiej okazało się dodać:
- Sprawdzanie;
- Automatyczne poprawianie;
- Pracę poprzez parsowanie XML.
Ważne: bez gwałtownych globalnych zmian – tylko punktowe poprawki.
5. Wydajność i rozmiar
Innym skutkiem, który ujawnił się z czasem, była niezauważalna degradacja produktu.
Agent może rozwiązać zadanie, ale jednocześnie:
- Dodać ciężką zależność;
- Skomplikować kod;
- Zwiększyć rozmiar buildu;
- Wpłynąć na wydajność.
Aby to śledzić, dodałem:
- Podstawowe benchmarki;
- Kontrolę rozmiaru buildu.
Nie daje to pełnej gwarancji, ale pozwala przynajmniej w porę dostrzec odchylenia.
6. Pre-commit: szybki filtr (Mercurial/hg lub Git)
Część testów przeniosłem do pre-commita:
- Autoformatowanie;
- Punktowy linting;
- Sprawdzanie architektury dla krytycznych modułów.
Ideą nie jest to, by „wszystkiego zabronić”, ale po to by:
- Szybko wyłapywać podstawowe problemy;
- Nie wrzucać ich do repozytorium;
- Nie obciążać review zbędnym szumem.
Dlaczego to działa
Sama istota programowania się nie zmieniła. Testy, lintery i ograniczenia architektoniczne zawsze były normą.
Zmieniło się coś innego: szybkość, z jaką pojawia się kod.
Gdy zmiany są generowane szybko i w ogromnych ilościach, ręczna kontrola przestaje być skalowalna. Tego, co kiedyś można było „wyłapać wzrokiem”, teraz po prostu nie ma czasu przetworzyć.
W tym kontekście automatyczne testy przestają być „dobrą praktyką”, a stają się podstawową koniecznością – po prostu po to, by utrzymać system w stabilnym stanie.
Podsumowanie
Nie postrzegam tego podejścia jako uniwersalne czy obowiązkowe dla wszystkich.
Ale w moim przypadku dało ono dość odczuwalny efekt – przede wszystkim dzięki zmniejszeniu obciążenia poznawczego.
Nie muszę już:
- Pamiętać o wszystkich ograniczeniach;
- Ręcznie czytać każdej zmiany;
- Analizować ogromnych logów;
- Ciągle sprawdzać podstawowych spraw.
System przejmuje to na siebie, a ja włączam się tam, gdzie ma to rzeczywisty sens.
Z praktycznych korzyści, które zauważyłem:
- Bardziej stabilne iteracje bez przypadkowych regresji;
- Przewidywalne zachowanie agenta dzięki pętli zwrotnej;
- Szybszy cykl poprawek;
- Mniej szumu na etapie review;
- Łatwiejsze skalowanie wykorzystania agenta.
Nie rozwiązuje to wszystkich problemów, ale sprawia, że proces pracy z agentem staje się znacznie bardziej sterowalny – przynajmniej w moim doświadczeniu.
Przydatne linki
- Konsist — aktualna dokumentacja dotyczącą archi-linta dla Kotlina.
- Spotless — narzędzie do automatycznego formatowania kodu.
- detekt — statyczny analizator kodu dla Kotlina.
- Mercurial (hg) — system kontroli wersji.
- Maestro — platforma do automatyzacji testów UI dla aplikacji mobilnych.
W następnym artykule opiszę, jak dodałem do tego automatyczne przechodzenie scenariuszy UI i sprawdzanie interfejsu za pomocą MCP Mobile oraz Maestro (frameworka do prostego i deklaratywnego testowania UI aplikacji mobilnych).