Liczba zastosowań modeli generatywnych jest coraz większa – dużą ich część skomercjalizowano, inne nie są jeszcze na to gotowe. W niniejszym artykule chcielibyśmy skupić się na jednym z bardziej interesujących obszarów – wytwarzaniu oprogramowania.
Nie na całym tym obszarze oczywiście, ale na specyficznej części związanej z modernizacją aplikacji. Jest on na tyle ciekawy, że istnieje już wiele narzędzi go wspierających, ale każde z nich ma spore ograniczenia. Chcielibyśmy więc przybliżyć wyzwania, na które napotykamy przy typowych projektach modernizacyjnych, i opowiedzieć o tym, gdzie GenAI może nam pomóc.
Siedem liter R
Modernizacja to bardzo szeroki temat i często użycie tego słowa zależy od celu biznesowego klienta. Regularnie w dyskusjach o modernizacji przywołuje się tzw. model 7R. Litery R oznaczają po prostu siedem różnych strategii migracyjnych do chmury: retire, retain, repurchase, rehost, relocate, replatform i refactor. Należy tutaj zaznaczyć, że choć koncepty te odnoszą się do przenoszenia aplikacji do chmury, a to – co do zasady – nie musi być modernizacją, jednak w większości opracowań obszary te się przenikają, a słowa „modernizacja” i „migracja” zaczynają być używane wymiennie – niestety. Spróbujmy jednak zdefiniować pokrótce wszystkie powyższe strategie w kontekście modernizacji, aby potem móc dobrze opisać ten jeden konkretny przypadek użycia, o którym chcielibyśmy opowiedzieć w niniejszym artykule:
- Retire – to nie jest strategia migracyjna, tylko decyzja, której skutkiem jest usunięcie oprogramowania z portfolio danej organizacji. Wynikać to może z wielu przyczyn, np. aplikacja jest zbyt stara i nie da się jej zmodernizować ani sensownie utrzymywać. Może też zdarzyć się tak, że program jest używany przez zbyt małą liczbę użytkowników, by opłacało się go dalej wspierać.
- Retain – to w zasadzie nie jest strategia migracyjna, ale decyzja o utrzymaniu aplikacji w miejscu, w którym jest obecnie. Może istnieć cała masa argumentów za taką decyzją – najczęściej jednak jest to ogólnie pojęta trudność przeniesienia lub modyfikacji. Na przykład oprogramowanie uruchomione jest w środowisku, z którego migracja byłaby utrudniona (choćby mainframe), albo wykonane zostało w niszowej technologii, co utrudnia modernizację, a sama aplikacja jest krytyczna dla działalności organizacji.
- Rehost – inaczej zwana lift & shift, czyli po prostu przeniesienie aplikacji do chmury bez zmian. Często wymaga to stworzenia specyficznej konfiguracji, bo aplikacja np. może działać w określonych warunkach sieciowych czy w konkretnej izolacji od innych zasobów. Oczywiście same zasoby chmurowe muszą zostać we właściwy sposób powołane do życia i skonfigurowane. W tej strategii GenAI może być bardzo przydatne, np. poprzez stworzenie kodu opisującego infrastrukturę (Infrastructure as Code, IaC) lub skryptów automatyzujących na podstawie wymagań i opisu procesu rehostingu.
- Repurchase – to podejście jest jednym z rozsądniejszych w niektórych przypadkach: skoro bowiem trudno jest utrzymać i rozwijać daną aplikację, to może warto skorzystać z produktu ekwiwalentnego od zewnętrznego dostawcy? To wyjątkowo szybka strategia i – jeśli tylko produkt nie jest krytyczny dla organizacji – bywa bardzo skuteczna. W przypadku aplikacji ważnych dla funkcjonowania organizacji migracja ze starej platformy do nowej zwykle jest jednak drogą przez mękę, więc w tych sytuacjach ta strategia nie powinna być wybierana jako domyślna.
- Relocate – to szczególna postać rehostingu, w ramach której przenosimy zasoby bez zmiany konfiguracji. Przykładowo, kiedy nasza serwerownia obsługiwana jest przez jakąś platformę wirtualizacyjną typu VMWare, istnieje możliwość przeniesienia zasobów bez niemal żadnej dodatkowej konfiguracji po stronie chmury lub innego centrum danych.
- Replatform – to strategia będąca rozwinięciem prostego rehostingu (lift & shift). Tutaj nie tylko przenosimy aplikację do chmury, ale także zmieniamy jej konfigurację, wykorzystujemy nowe usługi, zmieniamy infrastrukturę. Przykładowo aplikację uruchomioną na maszynach wirtualnych konteneryzujemy i uruchamiamy na Kubernetesie w chmurze. W ten sposób uzyskujemy nie tylko nowoczesną platformę usług cloudowych, ale także modernizujemy sam sposób działania aplikacji, zyskując np. skalowalność czy optymalizację kosztów. GenAI właśnie w migracjach z jednego środowiska na drugie potrafi pomóc najbardziej.
- Refactor – to najbardziej złożone podejście. Oznacza ono, że w zasadzie zmieniamy całą aplikację, czasem jej technologię, framework, architekturę, infrastrukturę czy proces release’owy. W praktyce wiąże się to czasami z przepisaniem oprogramowania na nowo. Tym podejściem będziemy się zajmować w niniejszym artykule, bo ono najlepiej ukazuje zalety zastosowania GenAI w procesie modernizacyjnym.
PODEJŚCIE DO MODERNIZACJI
Powyższa lista strategii modernizacyjnych (czy też migracyjnych, jeśli mowa o chmurze) jest w zasadzie fundamentem każdej decyzji biznesowej w odniesieniu do zmian w aplikacjach, znajdujących się w portfolio dowolnej organizacji. Na potrzeby tego tekstu przyjmiemy, że wstępna analiza aplikacji została już wykonana i organizacja wie, że potrzebny jest pełny refaktor (czy też tzw. rearchitecture).
Powodów takiej decyzji może być mnóstwo, ale przedstawmy te, z którymi najczęściej można się spotkać we współpracy z klientami:
- Oprogramowanie jest istotnym, jeśli nie najważniejszym, elementem działalności firmy. Mamy z tym do czynienia np. w sytuacji, w której firma oferuje usługę typu Software as a Service (SaaS) i modernizowana aplikacja jest właśnie tą usługą.
- Technologia, w której wykonano oprogramowanie, jest na tyle stara, że trudno znaleźć programistów, którzy byliby w stanie efektywnie je utrzymywać lub w ogóle chcieliby to robić. Przykładem może być stara aplikacja napisana w PHP, .NET Windows Forms czy nawet COBOL-u. Mowa tu też o starych technologiach bazodanowych typu dBase czy FoxPro.
- Technologia, w której aplikacja jest napisana, nie odpowiada potrzebom współczesnych użytkowników. Jeśli np. firma oferuje usługę SaaS za pomocą aplikacji okienkowej, udostępnianej przez zdalny pulpit, to prawdopodobnie trzeba taką aplikację przepisać bez zbędnej zwłoki na technologie webowe.
- Użytkownicy domagają się rozwoju oprogramowania, wprowadzania nowych możliwości, ale ten rozwój jest w zasadzie niemożliwy lub bardzo trudny przy użyciu obecnie wykorzystanej technologii. Może to mieć miejsce zarówno ze względu na jej ograniczenia, jak i brak możliwości pozyskania oraz utrzymania kompetentnych deweloperów.
- Aplikacja zmaga się z wieloma niefunkcjonalnymi wyzwaniami, np. związanymi z wydajnością (brak skalowania) czy zgodnością (brak możliwości przejścia audytów bezpieczeństwa ze względu na nieutrzymywaną technologię).
Jeśli wszystkie lub większość powyższych scenariuszy jest spełniona, to niemal na pewno mamy do czynienia z dobrym kandydatem na refaktoring. Niestety wstępna analiza to dopiero początek wyzwań. To, czego potrzebujemy, to konkrety:
- Do jakiej technologii migrujemy naszą aplikację?
- Jakie kryteria determinują docelowy stos technologiczny?
- Co jest ważne? Koszty, wydajność, skalowalność, nowoczesność, całkowity koszt wytworzenia i utrzymania (Total Cost of Ownership, TCO)?
- Przepisujemy całość czy tylko część aplikacji?
- Jak zdefiniujemy „sukces” refaktoringu?
- Czy w ogóle wiemy, jak nasza aplikacja powinna działać?
- Gdzie jest (i czy w ogóle istnieje) dokumentacja procesów związanych z oprogramowaniem? Czy istnieje dokumentacja samej aplikacji?
Na te i na wiele innych pytań należy sobie odpowiedzieć, zanim proces modernizacji zostanie na dobre uruchomiony. Na szczęście możemy sobie pomóc generatywną sztuczną inteligencją.
Dokumentacja
Jednym z ważniejszych wyzwań do rozwiązania na wstępnym etapie modernizacji kodu jest pozyskanie dokumentacji. Niestety w dużej części przypadków, z którymi można się spotkać, dokumentacja jest szczątkowa lub nie ma jej w ogóle. Aplikacje, które modernizujemy, mają niejednokrotnie 20–30 lat, nie ma więc już często dostępnych ludzi, którzy je tworzyli i mogliby pomóc zrozumieć w pełni ich architekturę czy przyjęte rozwiązania.
Duże modele językowe pomagają jednak poradzić sobie z tym problemem. Oczywiście nie jest to rozwiązanie doskonałe, gdyż sam kod może być napisany hermetycznie i nie zawierać wprost wskazówek dotyczących biznesowego tła konkretnej implementacji. Co więcej, technologia wykorzystana do napisania tego konkretnego kodu może to tło przykrywać przez złożoność implementacji – świetnym przykładem jest tu oprogramowanie napisane w COBOL-u.
Niemniej – przy użyciu generatywnej sztucznej inteligencji – jesteśmy w stanie uzyskać następujące rzeczy:
- Podsumowanie celu i sensu aplikacji.
- Detekcję technologii, frameworku czy statusu legacy (tzn. określenia, jak bardzo przestarzały jest dany kod).
- Listę ważnych informacji dotyczących kodu – np. istotnych problemów czy skomplikowanych zależności.
- Odnalezienie zależności z innymi aplikacjami lub systemami zewnętrznymi.
- Opis działania poszczególnych modułów oraz fragmentów kodu (np. pojedynczych klas).
Tak pozyskana dokumentacja nie jest oczywiście pełna i będzie w niej brakowało wielu istotnych elementów, takich jak powiązanie wymagań biznesowych z konkretną implementacją czy powody wykorzystania konkretnego rozwiązania technologicznego (choć w tym ostatnim przypadku można również za pomocą LLM-ów próbować rozwikłać dany problem).
Ważniejsze jest jednak co innego – każdy, kto kiedykolwiek programował, wie, jak trudno jest zrozumieć cudzy czy stary kod. Podejście wykorzystujące generatywną sztuczną inteligencję pomaga szybciej rozpocząć efektywną pracę z istniejącymi aplikacjami.
Dług technologiczny
Jednym z ważniejszych aspektów modernizacji kodu jest próba spłacenia długu technologicznego. Jego detekcja w istniejących aplikacjach jest bardzo trudna, ponieważ wymaga dogłębnego zrozumienia sposobu działania danego kodu i jaka jest architektura rozwiązania. Oczywiście modernizacja może oznaczać kompletne przepisanie aplikacji lub nawet zmianę technologii, ale można spotkać się z przypadkami, w których klient chce skupić się wyłącznie na aspekcie długu. Zresztą nawet w przypadku całościowej modernizacji detekcja może pomóc zrozumieć, jak działa dana aplikacja, a nawet jej biznesowe tło.
Dzięki LLM-om jesteśmy w stanie próbować odkryć dług w istniejącym kodzie. Problem, z którym często można się spotkać w tym przypadku, jest związany z rozmiarem danej aplikacji. Im więcej kodu, tym trudniej syntetycznie zebrać informacje o długu i tym bardziej karkołomne staje się wygenerowanie rekomendacji, jak sobie z nim poradzić. W praktyce wykorzystujemy tutaj często podejście typu map-reduce. W uproszczeniu – detekcja odbywa się na fragmentach kodu, a potem te informacje są składane w jeden, spójny zestaw danych o długu technologicznym całej aplikacji. Tak samo postępujemy z rekomendacjami, jak sobie z długiem poradzić, choć tutaj można je generować na podstawie fragmentów informacji lub finalnych podsumowań.
Podczas warsztatów modernizacyjnych informacje o długu technologicznym wielokrotnie okazują się kluczowe do zrozumienia krytycznych szczegółów implementacyjnych i dlatego wykorzystanie tej metody staje się niezbędne w procesie refaktoringu.
AUDYT
Interesującym rozwinięciem powyższych koncepcji jest tworzenie automatycznych narzędzi wspomagających audyt konkretnych rozwiązań technologicznych, np. pipeline’ów dbt (dbt to narzędzie umożliwiające implementację transformacji danych) czy infrastruktury chmurowej.
Istnieje oczywiście wiele klasycznych narzędzi do wykonywania tego rodzaju audytów, ale brakuje im zwykle kreatywności, która może wspomóc proces wyciągania wniosków czy też nawet implementacji wymaganych zmian.
Wyobraźmy sobie infrastrukturę chmurową, do której nie mamy żadnego kodu ani opisu. Przy użyciu połączenia klasycznych narzędzi i takich opartych na LLM-ach można wykonać reverse engineering infrastruktury (automatyczna implementacja istniejącej infrastruktury w kodzie) oraz analizę istniejącego już kodu z uwzględnieniem konkretnych, bardzo dobrze zdefiniowanych reguł. Reguły te mogą obejmować skalowalność, bezpieczeństwo i wiele innych aspektów dobrych praktyk.
Dzięki procesom z wykorzystaniem LLM-ów możemy nawet wygenerować automatyczny raport z takiego audytu, który nie będzie tylko listą problemów, ale także – a może przede wszystkim – listą rekomendacji oraz propozycją ich implementacji.
REVERSE ENGINEERING
Jednym z ciekawszych i bardziej efektownych aspektów procesu modernizacji jest próba wykonania reverse engineeringu na istniejącej aplikacji. W tym przypadku oznacza to generowanie wymagań biznesowych z kodu – przy użyciu LLM-ów oczywiście. W ten sposób można byłoby np. tworzyć automatycznie testy integracyjne czy systemowe lub nawet generować nową implementację bezpośrednio z wymagań, a nie ze starego kodu. Innymi słowy, modernizacja mogłaby wyglądać następująco:
- Z istniejącego kodu generujemy wymagania biznesowe.
- Z wymagań biznesowych tworzymy nowy kod, w innej technologii.
Pozwoliłoby to, w teorii, uniknąć nieświadomego kopiowania błędów implementacyjnych przy modernizacji typu kod do kodu. Niestety nie jest to takie proste. Wymagania wygenerowane z kodu nie zawsze opisują we właściwy sposób biznesowe tło aplikacji. Są bowiem tylko sumą informacji zawartych bezpośrednio w kodzie oraz pewnego poziomu halucynacji dużego modelu językowego. Niemniej taki reverse engineering jest pewną formą dodatkowej dokumentacji istniejącej aplikacji i jako taki może pomóc zrozumieć jej złożoność.
Eksperymenty modernizacyjne
W trakcie warsztatów modernizacyjnych często dochodzi do sytuacji, w której rozumiemy już z grubsza kod, a także znamy cel modernizacji i wiemy, jaka jest technologia docelowa. Czas zatem zabrać się za proof of concept, które na potrzeby tego artykułu nazwiemy eksperymentem modernizacyjnym. Duże modele językowe mogą pomóc nam w dwóch aspektach:
- Tworzenie automatycznego planu modernizacyjnego – możemy użyć takich planów, wykorzystując podejście quasi-agentowe. Wygląda to z grubsza w ten sposób, że pierwszym etapem jest wygenerowanie kroków, na podstawie których należy zmodyfikować dany kod. Taki plan może uwzględniać docelową technologię i najczęściej będzie się składał z elementów, które po prostu implementują dobre praktyki, np. „wykorzystaj strukturę obiektową”, „użyj warstwy abstrakcji dostępu do danych”, „utwórz testy jednostkowe” itd. Te kroki są zwykle dosyć typowe, ale LLM potrafi dobrać je do użytej technologii. Co oczywiste jednak, plan ten będzie zwykle generyczny, więc w procesie tworzenia samej implementacji należy umożliwić jego modyfikację.
- Modernizacja przy użyciu wygenerowanego planu – kolejnym krokiem jest iteracyjne generowanie docelowych plików. Dlaczego iteracyjne? Zwykle modele językowe dopuszczają spory kontekst wejściowy – innymi słowy, w zapytaniu do modelu można przekazać całkiem sporo kodu (z reguły tego starego). Modele te mają jednak ograniczoną długość wygenerowanej treści – w tym przypadku kodu (zwykle jest to na poziomie 4000 lub 8000 tokenów). Oznacza to, że nie możemy potraktować LLM-u jako magicznego konwertera jednego dużego kodu w inny. Stąd często wykorzystywaną strategią jest generowanie najpierw struktury docelowych plików (bez zawartości), a dopiero potem iteracyjne tworzenie ich zawartości (czyli każde zapytanie do modelu skutkuje powstaniem tylko jednego pliku).
Takie eksperymenty doprowadzają zwykle do wypracowania właściwej taktyki modernizacji całego kodu – jeśli bowiem jakieś podejście zadziała na relatywnie niewielkim fragmencie, to w przypadku większości aplikacji można je ekstrapolować na całą resztę. Rozwiązania – nawet legacy – budowane są zwykle według pewnego wzorca projektowego czy paradygmatu i w ogromnej większości przypadków mamy do czynienia z – dającą się wykorzystać – powtarzalnością w strukturze kodu.
ITERACYJNE POPRAWIANIE PROCESU
Za sprawą rezultatów eksperymentów jesteśmy w stanie wypracować z klientem inicjalne podejście do modernizacji. Istnieje nawet możliwość, że można je zastosować masowo do całej aplikacji i wygenerować po prostu nowy kod, który wystarczy poprawić, dopracować i wydać.
Jak pokazuje praktyka, to założenie jest zwykle zbyt optymistyczne – tym bardziej nierealistyczne, z im większym objętościowo kodem mamy do czynienia. Zakładamy jednak, że w modernizacji lepiej przyjąć taktykę iteracji:
- Przy użyciu rezultatów eksperymentów modernizacyjnych tworzymy pierwszą, niewielką partię nowego kodu.
- Programiści i architekci weryfikują jego poprawność (kryteria tej poprawności należy oczywiście zawczasu przygotować – z reguły to szeroki zakres normalnych zasad obowiązujących przy „zwykłym” tworzeniu kodu – od code review aż po testy automatyczne).
- Na tym etapie zwykle pojawiają się uwagi dotyczące wygenerowanego kodu – często chodzi o detale implementacyjne, wykorzystanie konkretnych bibliotek czy po prostu strukturę. Te uwagi można włączyć w automatyczny proces generowania jako zestawu reguł.
- W kolejnej iteracji wygenerowany kod powinien być lepszy. Jeśli tak jest, powtarzamy dwa poprzednie kroki, aż dopracujemy proces automatycznego tworzenia kodu tak, by spełniał nasze kryteria jakościowe.
Nie należy jednak liczyć na to, że rezultat takiej automatycznej modernizacji będzie idealny, kompilowalny i w zasadzie będzie go można od razu wydać w przynajmniej testowej wersji. Ze względu na niedeterministyczność działania LLM-ów oraz trudne do uchwycenia zasady logiki biznesowej prawie na pewno trzeba będzie taki kod poprawiać ręcznie. Kluczem jest jednak ogromna oszczędność czasu oraz uniknięcie tzw. klątwy białej kartki – w praktyce nie istnieje w tym procesie moment, w którym nie mamy kompletnie nic.
Rola człowieka
Na tym oczywiście możliwości dużych modeli językowych się nie wyczerpują. Istnieje cała gama zastosowań, które mogą być ważne w konkretnych typach procesów modernizacyjnych. Z łatwością można sobie wyobrazić proces, w którym każdy krok będzie tak czy inaczej zautomatyzowany, a rolą architekta, DevOpsa, programisty czy testera okaże się jedynie walidacja poszczególnych elementów. Czy to oznacza, że programiści nie będą potrzebni w procesach związanych z unowocześnianiem aplikacji? Odpowiedź na to pytanie to w dużej mierze wróżenie z fusów, ponieważ nie wiemy tak naprawdę, w którą stronę pójdzie ewolucja LLM-ów (o ile nie zostaną one zastąpione czymś lepszym). Na razie programiści w takim procesie są potrzebni, choć na pewno w dużo mniejszym zakresie niż kiedyś. Istotne jest jednak to, że oszczędność czasu w modernizacji bierze się głównie z unikania powtarzalnych czynności, a to, co najbardziej interesujące – logika biznesowa – nadal jest domeną człowieka.
Autor
Krzysztof Kąkol
Autor jest architektem odpowiedzialnym za wdrażanie rozwiązań GenAI w aplikacjach i procesie wytwarzania oprogramowania. Specjalizuje się w chmurze – w szczególności AWS-u. Jego obszary zainteresowań obejmują najlepsze praktyki DevOps, designing for data privacy, przetwarzanie danych i uczenie maszynowe. Zajmuje się także badaniami naukowymi dotyczącymi przetwarzania i poprawy jakości sygnału mowy w obecności zakłóceń.