Zgłosił się do nas klient z aplikacją mobilną, która działała już produkcyjnie, miała aktywnych użytkowników i płacących klientów. Znaczna część kodu powstała przy użyciu AI, a logika czatu AI opierała się na jednym prompcie liczącym około 500 wierszy.

Produkt spełniał swoje zadania, jednak jego dalszy rozwój stawał się coraz trudniejszy. W bazie kodu pozostawały nieużywane fragmenty, podobna logika pojawiała się w kilku miejscach, a nowa instrukcja dodana do promptu mogła nieoczekiwanie wpłynąć na scenariusz, który wcześniej działał poprawnie.

Szczególnie dobrze pokazało to jedno, pozornie niewielkie zadanie. Zespół miał zmienić pojedynczą regułę w aplikacji. Zanim jednak programista mógł wprowadzić poprawkę, musiał ustalić, która z kilku podobnych implementacji jest faktycznie używana, dlaczego obok nadal znajduje się inna wersja tej samej logiki i co się stanie po usunięciu starych metod.

Sama zmiana zajęła mniej czasu niż przygotowanie się do jej wprowadzenia.

Podczas dalszej analizy zespół zidentyfikował dwa powiązane problemy: skutki kolejnych iteracji wykonywanych z pomocą AI w bazie kodu oraz monolityczną strukturę logiki AI, która utrudniała przewidywalne zmienianie zachowania czatu.

W artykule
  • kiedy lokalne zmiany wprowadzane przez AI zaczynają ze sobą kolidować;
  • prompt, w którym wszystko zaczęło zależeć od wszystkiego;
  • jak podzieliliśmy logikę AI;
  • co zmieniło się po refaktoryzacji;
  • co zrobiliśmy z samą aplikacją.

Kiedy lokalne zmiany wprowadzane przez AI zaczynają ze sobą kolidować

Vibe coding dobrze sprawdza się w ramach konkretnego zadania. Model otrzymuje polecenie, modyfikuje odpowiedni fragment kodu i zwraca wynik.

Przy kolejnej iteracji kontekst może być już jednak inny. AI tworzy nową implementację, ale nie zawsze rozpoznaje, że poprzednia wersja nie jest już potrzebna.

O zanonimizowanych przykładach.
Nazwy encji i scenariusze użytkowników zostały zmienione, aby nie ujawniać wewnętrznej logiki produktu. Opisane typy problemów odpowiadają jednak temu, z czym zespół zetknął się podczas projektu.

Podczas jednej z iteracji model wygenerował nowy sposób obsługi danych. Aplikacja zaczęła z niego korzystać, ale modele i metody pomocnicze związane z poprzednią wersją nadal pozostawały w projekcie. Została tam również nieaktualna logika cache’owania.

Nic się nie psuło. Użytkownicy nie mieli możliwości zauważyć problemu.

Przy kolejnej modyfikacji stary i nowy kod wyglądały jednak jak dwie prawidłowe implementacje tego samego procesu. Obie pojawiały się w wynikach wyszukiwania, odwoływały się do sąsiednich komponentów i utrudniały ustalenie, który fragment systemu można bezpiecznie zmienić.

Podobna sytuacja wystąpiła w logice walidacji. Zamiast ponownie wykorzystać istniejącą funkcję, AI rozwiązało to samo zadanie osobno dla kilku ekranów. Reguła walidacji została zduplikowana, a poszczególne wersje nieznacznie się od siebie różniły.

Jedna zmiana wymagała teraz pracy w kilku miejscach. Zespół musiał najpierw znaleźć wszystkie wersje logiki i ustalić, które z nich są faktycznie wywoływane. Dopiero później można było sprawdzić, jak modyfikacja wpłynie na stan interfejsu.

W ten sposób dług techniczny narastał bez natychmiastowych awarii. Każda iteracja AI rozwiązywała swoje lokalne zadanie, a ogólna struktura aplikacji stopniowo stawała się coraz mniej czytelna.

Prompt, w którym wszystko zależało od wszystkiego

Podobny problem pojawił się wewnątrz czatu AI.

Około 500 wierszy promptu zawierało reguły przetwarzania zapytań, ograniczenia, wymagania dotyczące formatu oraz instrukcje dla poszczególnych scenariuszy. Dopóki logika pozostawała stosunkowo prosta, takie podejście pozwalało szybko dodawać nowe zachowania.

Z czasem prompt stał się podatny na błędy.

Jak powstawał konflikt
Instrukcja bazowa

Zwrócić wynik w ściśle określonej strukturze JSON.

Reguła dla konkretnego scenariusza

Prowadzić naturalną rozmowę i szczegółowo wyjaśniać użytkownikowi kolejne działania.

Rezultat:
model mógł dodać zwykły tekst poza strukturą JSON albo zwrócić sztywną strukturę danych tam, gdzie oczekiwano pełnej odpowiedzi konwersacyjnej.

Każda z tych instrukcji osobno wyglądała poprawnie. Razem prowadziły jednak do niespójnych rezultatów.

Zmiana jednego sformułowania nie gwarantowała rozwiązania problemu. Odpowiedź zależała od kombinacji reguł rozmieszczonych w różnych częściach promptu. Nowa instrukcja mogła poprawić jeden scenariusz, jednocześnie niepostrzeżenie zmieniając inny.

W rezultacie każda kolejna modyfikacja czatu wymagała prześledzenia zależności między dziesiątkami warunków, które znajdowały się w jednym dokumencie, ale sterowały różnymi rodzajami zachowania.

Jak podzieliliśmy logikę AI

Zespół odszedł od założenia, że jeden prompt powinien jednocześnie rozumieć zapytanie, wybierać scenariusz, pobierać potrzebne dane i generować ostateczną odpowiedź.

Te odpowiedzialności rozdzielono pomiędzy kilku agentów.

ETAP 01

Router

Określa typ zapytania i wybiera dalszą ścieżkę przetwarzania.

ETAP 02

Retriever

Pobiera kontekst i reguły potrzebne dla wybranego scenariusza.

ETAP 03

Generator

Tworzy końcową odpowiedź na podstawie danych przygotowanych na wcześniejszych etapach.

Każdy komponent pracuje teraz z ograniczonym kontekstem i odpowiada za jeden konkretny etap, zamiast przetwarzać całą logikę produktu jednocześnie.

Sam podział promptu nie rozwiązał jednak całego problemu. Agenci musieli wymieniać między sobą informacje, a te połączenia również wymagały kontroli.

Dlatego komponenty przekazują wyniki w ustrukturyzowanym formacie.

Przykład kontraktu między komponentami
{
  "intent": "...",
  "confidence": 0.91,
  "entities": [...]
}

Zanim dane trafią do kolejnego etapu, system sprawdza, czy wszystkie wymagane pola są obecne i czy wynik odpowiada oczekiwanemu formatowi.

Jeśli jeden z agentów zwróci nieprawidłową strukturę, błąd zostaje zatrzymany na tym etapie, zamiast przechodzić dalej przez cały pipeline.

Co zmieniło się po refaktoryzacji

Taka architektura uprościła również testowanie. Zespół QA mógł osobno sprawdzać klasyfikację zapytania, pobieranie kontekstu i generowanie odpowiedzi. Niewielka zmiana nie wymagała już ponownego analizowania całego promptu liczącego 500 wierszy.

Przed

Czat mógł naruszyć wymagany format, pominąć ograniczenie albo zmienić swoje zachowanie w jednym scenariuszu po aktualizacji innego.

Po

Takie odchylenia pojawiały się rzadziej, a czat zachowywał się bardziej przewidywalnie w powtarzanych scenariuszach testowych.

Zespół QA może teraz zmieniać i testować pojedynczy fragment logiki AI w ramach konkretnego zadania, bez analizowania całego promptu.

Co zrobiliśmy z samą aplikacją

Pełne przepisanie aplikacji nie było konieczne.

Zespół najpierw odtworzył faktyczną strukturę projektu. Zidentyfikował implementacje, które nadal były używane, oddzielił je od nieaktualnego kodu i usunął duplikaty tam, gdzie utrudniały dalsze zmiany.

Działające części aplikacji zostały zachowane. Problematyczne obszary refaktoryzowano stopniowo, bez wstrzymywania rozwoju produktu w celu przebudowy całego systemu.

Rezultat

Aplikacja pozostała w środowisku produkcyjnym, a zespół otrzymał bazę kodu i architekturę AI, które można dalej rozwijać. Nowe zadania nie zaczynały się już od ustalania, który z kilku podobnych fragmentów jest aktualny ani która reguła ukryta w ogromnym prompcie może wpłynąć na odpowiedź.