Możliwości zastosowania CHATA-GPT przy optymalizacji prac developerskich
Prędkość i skuteczność stały się kluczowymi aspektami podczas pracy nad projektami aplikacji. Z uwagi na potrzebę szybkiego wdrażania i dodawania nowych funkcjonalności, narzędzia oparte na sztucznej inteligencji (AI) mogą istotnie przyspieszyć ten proces. Jednym z pierwszych i najpopularniejszych narzędzi tego rodzaju jest ChatGPT.
W ramach niniejszego artykułu będę korzystał z darmowej wersji ChatGPT 3.5 (dostępna jest również płatna wersja 4.0, a w tym roku planowane jest wydanie ChatGPT 5.0).
Czym jest
CHAT-GPTChatGPT jest narzędziem opartym na sztucznej inteligencji, wykorzystującym model GPT do prognozowania języka. Model ten bazuje na sieci neuronowej i analizuje zapytania w języku naturalnym, zwane podpowiedziami, w celu przewidzenia najlepszej możliwej odpowiedzi na podstawie zrozumienia języka. Ponieważ programowanie opiera się na korzystaniu z języków programowania, to podejście jest również stosowane do generowania kodu aplikacji. Model jest trenowany na setkach miliardów parametrów oraz na ogromnych zbiorach danych językowych. Podczas generowania uwzględnia on kontekst wejściowy, co pozwala na prowadzenie "pseudorozmowy", uwzględniając dodatkowe podane parametry. Ogólne zrozumienie zasady działania algorytmu polega na pobraniu n fragmentu danych i na ich podstawie wygenerowaniu kolejnego pojedynczego fragmentu:
W przypadku ChatGPT naszym fragmentem danych jest słowo. Jeśli słowo jest wystarczająco długie, może zostać podzielone na fragmenty ([dwudziesto][groszówka]), a skróty mogą być rozbite na pojedyncze litery lub traktowane jako jedno krótkie słowo, w zależności od częstotliwości występowania danej kombinacji znaków. Aktualne modele językowe mają limity dotyczące maksymalnej ilości fragmentów danych, przez co podział słowa na pojedyncze litery nie przyniósłby rezultatu. Prawdopodobnie w przyszłości ta kwestia ulegnie zmianie.
Obserwujemy ten mechanizm podczas generacji odpowiedzi przez sztuczną inteligencję:
W tym przypadku algorytm wygenerował fragment danych "człowiek" na podstawie wejścia oraz całej swojej poprzednio wygenerowanej odpowiedzi. Kolejnym wynikiem pracy algorytmu będzie wygenerowanie fragmentu "ienia" jako najbardziej prawdopodobnej kontynuacji ciągu danych.
Cały proces będzie się powtarzał, rozszerzając parametry wejściowe aż do spełnienia warunków zakończenia przez algorytm.
Znaczącym elementem algorytmu jest jego niedeterministyczność. To znaczy, że to samo pytanie może wygenerować różne odpowiedzi. Dzieje się tak, ponieważ algorytm zwraca rozkład prawdopodobieństwa wszystkich możliwych fragmentów danych, innymi słowy, zwraca wektor, w którym każdy element wyraża prawdopodobieństwo wyboru określonego tokenu. Następnie "losuje" fragment danych, który zostanie użyty.
Rozkład prawdopodobieństwa jest ustalany na podstawie wcześniej nauczonego modelu. Dobrze wytrenowany model jest w stanie generować sensowne prawdopodobieństwo następnego fragmentu danych. Szczegółowy opis sposobu działania algorytmu to temat na oddzielny artykuł.
W przykładzie użyłem fragmentu danych tekstowych, ale równie dobrze można wykorzystać dowolne dane w dowolnej kombinacji:
tekst -> dźwięk
tekst -> obraz
obraz + tekst -> obraz
Kwestie prawne
W polskim prawie ochrona praw autorskich dotyczy wyłącznie dzieł stworzonych przez ludzi. To umożliwia wykorzystanie kodu generowanego przez modele językowe w projektach komercyjnych.
Jednakże, korzystanie z bezpłatnej wersji CHAT-GPT 3.5 podlega warunkom korzystania, które wykluczają komercyjne zastosowanie narzędzia:
https://openai.com/pl/policies/eu-terms-of-use
W warunkach użytkowania dla klientów biznesowych czytamy:
3.1 Treść Klienta. Ty i Użytkownicy Końcowi możecie dostarczać Dane Wejściowe do Usług ("Wejście") oraz otrzymywać wyniki z Usług na podstawie Wejścia ("Wyjście"). Razem nazywamy to "Treścią Klienta". Między tobą a OpenAI, oraz zgodnie z obowiązującym prawem, (a) zachowujesz wszystkie prawa własności do Wejścia oraz (b) posiadasz wszelkie prawa własności do Wyjścia. Niniejszym przenosimy na ciebie wszelkie nasze prawa, tytuł i interes, jeśli takie istnieją, w Wyjściu.
https://openai.com/policies/business-terms
To daje pełne prawa do wykorzystania generowanego tekstu.
W przypadku korzystania z CHAT-GPT w projektach komercyjnych, zalecamy indywidualne sprawdzenie, czy taka możliwość istnieje.
Generowanie modeli
Generowanie modeli javowych baz danych może być czasochłonnym zadaniem, zwłaszcza gdy mamy do czynienia z dużymi i złożonymi schematami. Często wymaga to ręcznego pisania znacznej ilości kodu, co prowadzi do możliwości popełniania błędów i utrudnień w utrzymaniu. Jednakże, przy wsparciu ChatGPT, ten proces można znacznie usprawnić.
Proces generowania modeli javowych baz danych przy użyciu ChatGPT może zostać podzielony na kilka kroków:
a) Specyfikacja bazy danych: Pierwszym krokiem jest dostarczenie ChatGPT szczegółowej specyfikacji bazy danych. Obejmuje to opis encji, relacje między nimi, typy danych, klucze główne i obce, oraz inne szczegóły dotyczące struktury bazy danych.
Przykład:
Możemy zacząć od poproszenia ChatGPT o wygenerowanie SQL, które posłuży nam jako model.
INPUT:
Napisz zapytanie tworzące tabelę:
User z polami imię, nazwisko, PESEL, identyfikator ubezpieczenia. Ubezpieczenie z polami identyfikator, rodzaj ubezpieczenia, data założenia, gdzie identyfikator ubezpieczenia jest kluczem obcym z tabeli Ubezpieczenie.
OUTPUT:
CREATE TABLE Osoba (
id INT PRIMARY KEY AUTO_INCREMENT,
imie VARCHAR(255) NOT NULL,
nazwisko VARCHAR(255) NOT NULL,
pesel VARCHAR(11) UNIQUE NOT NULL,
id_ubezpieczenia INT,
FOREIGN KEY (id_ubezpieczenia) REFERENCES Ubezpieczenie(id)
);
CREATE TABLE Ubezpieczenie (
id INT PRIMARY KEY AUTO_INCREMENT,
rodzaj_ubezpieczenia VARCHAR(100) NOT NULL,
data_zalozenia DATE NOT NULL
);
b) Generowanie kodu: Na podstawie dostarczonej specyfikacji, ChatGPT może wygenerować kod javowy reprezentujący modele bazy danych. Może to obejmować klasy encji, relacje między nimi, metody dostępu do danych, a nawet metody pomocnicze do obsługi operacji na bazie danych.
Podając tę kwerendę SQL jako wejście do kolejnego zapytania wraz z prośbą o wygenerowanie modelu dla aplikacji Java 11 z wykorzystaniem Spring Data JPA, otrzymamy:
import javax.persistence.*;
@Entity
@Table(name = "Osoba")
public class Osoba {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "imie", nullable = false)
private String imie;
@Column(name = "nazwisko", nullable = false)
private String nazwisko;
@Column(name = "pesel", unique = true, nullable = false)
private String pesel;
@Column(name = "id_ubezpieczenia")
private Long idUbezpieczenia;
// Getters and setters
}
import javax.persistence.*;
import java.util.Date;
@Entity
@Table(name = "Ubezpieczenie")
public class Ubezpieczenie {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "rodzaj_ubezpieczenia", nullable = false)
private String rodzajUbezpieczenia;
@Column(name = "data_zalozenia", nullable = false)
private Date dataZalozenia;
// Getters and setters
}
c) Dostosowywanie i optymalizacja: Wygenerowany kod może zostać później dostosowany i zoptymalizowany przez programistę. Chociaż ChatGPT może wygenerować podstawowy szkielet kodu, może być konieczne jego dostosowanie do konkretnych wymagań projektowych oraz optymalizacja pod kątem wydajności. Podstawowe poprawki mogą być przekazywane słownie, wykorzystując kontynuację sesji, ponieważ poprzednie zapytania są zapamiętane.
INPUT: W tabeli Ubezpieczenie zmień rozdaj ubezpieczenia na tym enum zkładający się z wartości AC i OC
OUTPUT: import javax.persistence.*;
import java.util.Date;
@Entity
@Table(name = "Ubezpieczenie")
public class Ubezpieczenie {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Enumerated(EnumType.STRING)
@Column(name = "rodzaj_ubezpieczenia", nullable = false)
private RodzajUbezpieczenia rodzajUbezpieczenia;
@Column(name = "data_zalozenia", nullable = false)
private Date dataZalozenia;
// Getters and setters
}
public enum RodzajUbezpieczenia {
AC,
OC
}
d) Testowanie i wdrażanie: Po dostosowaniu i optymalizacji, wygenerowany kod może być poddany testom jednostkowym i integracyjnym, a następnie wdrożony do aplikacji. To niezbędny krok w procesie rozwoju oprogramowania. Jednakże, przy bardziej złożonych modelach i relacjach, mogą wystąpić błędy, które programista musi wykryć ręcznie.
Jednakże, korzyści związane z oszczędnością czasu dzięki automatycznej generacji kodu są znaczące. Dzięki posiadaniu analitycznego opisu bazy/modelu, możemy w ciągu sekund zbudować model, co oszczędza nam żmudnego ręcznego pisania kodu.
Generacja testów
ChatGPT może wspomóc programistów w tworzeniu skomplikowanych testów jednostkowych oraz integracyjnych. Ponadto, dzięki swojej elastyczności, może dostarczać sugestii dotyczących testowania różnych przypadków krawędziowych oraz pomagać w automatyzacji procesu tworzenia testów.
Na przykład, mając serwis wykorzystywany przy walidacji formularzy, możemy przekazać cały kod serwisu wraz z prośbą o wygenerowanie testów. Dla ułatwienia, można poprosić o wygenerowanie testów bez szczegółowych warunków, jakie chcielibyśmy, aby te testy obejmowały, a ChatGPT również sobie z tym poradzi.
Błąd wynika z faktu, że Angular nie rozpoznaje właściwości dateRange jako wbudowanego walidatora, ponieważ został on zdefiniowany w niestandardowy sposób w usłudze FormValidationService. Aby ominąć ten błąd, możemy zdefiniować własny walidator dla pola dateRangeField bezpośrednio w teście. Oto jak to zrobić:
INPUT: napisz testy w angularze 11 dla:
import { Injectable } from '@angular/core';
import { FormGroup } from '@angular/forms';
@Injectable({
providedIn: 'root'
})
export class FormValidationService {
constructor() {}
dateRangeValidator(start: string, end: string) {
return (formGroup: FormGroup) => {
const startDate = formGroup.controls[start].value;
const endDate = formGroup.controls[end].value;
if (startDate && endDate && new Date(startDate) > new Date(endDate)) {
formGroup.controls[end].setErrors({ dateRange: true });
formGroup.controls[start].setErrors({ dateRange: true });
} else if (startDate && endDate) {
formGroup.controls[end].setErrors(null);
formGroup.controls[start].setErrors(null);
}
};
}
isFieldValid(formName: string, form: FormGroup): boolean {
const fieldControl = form.get(formName);
if (fieldControl) {
return !(fieldControl.invalid && fieldControl.touched);
} else {
return false;
}
}
isDateInRangeValid(formName: string, form: FormGroup): boolean {
const dataControl = form.get(formName);
if (dataControl) {
return !dataControl.errors?.dateRange;
} else {
return false;
}
}
}
OUTPUT: import { TestBed } from '@angular/core/testing';
import { FormGroup, FormControl } from '@angular/forms';
import { FormValidationService } from './form-validation.service';
describe('FormValidationService', () => {
let service: FormValidationService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(FormValidationService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
it('should set date range errors when start date is after end date', () => {
const formGroup = new FormGroup({
startDate: new FormControl('2024-05-01'),
endDate: new FormControl('2024-04-01')
});
const validator = service.dateRangeValidator('startDate', 'endDate');
validator(formGroup);
expect(formGroup.controls['endDate'].hasError('dateRange')).toBe(true);
expect(formGroup.controls['startDate'].hasError('dateRange')).toBe(true);
});
it('should not set date range errors when dates are in correct order', () => {
const formGroup = new FormGroup({
startDate: new FormControl('2024-04-01'),
endDate: new FormControl('2024-05-01')
});
const validator = service.dateRangeValidator('startDate', 'endDate');
validator(formGroup);
expect(formGroup.controls['endDate'].hasError('dateRange')).toBe(false);
expect(formGroup.controls['startDate'].hasError('dateRange')).toBe(false);
});
it('should return true when field is valid', () => {
const form = new FormGroup({
exampleField: new FormControl('validValue')
});
expect(service.isFieldValid('exampleField', form)).toBe(true);
});
it('should return false when field is invalid', () => {
const form = new FormGroup({
exampleField: new FormControl('', Validators.required)
});
expect(service.isFieldValid('exampleField', form)).toBe(false);
});
it('should return true when date range is valid', () => {
const form = new FormGroup({
dateRangeField: new FormControl('2024-04-01')
});
expect(service.isDateInRangeValid('dateRangeField', form)).toBe(true);
});
it('should return false when date range is invalid', () => {
const form = new FormGroup({
dateRangeField: new FormControl('', { dateRange: true })
});
expect(service.isDateInRangeValid('dateRangeField', form)).toBe(false);
});
});
W powyższej klasie wygenerowanych testów są dwa błędy.
Chat "zapomniał" o dodaniu importu:
import { FormGroup, FormControl, Validators } from '@angular/forms';
Drugim błędem w testach jest:
Object literal may only specify known properties, and 'dateRange' does not exist in type
'ValidatorFn | ValidatorFn[] | AbstractControlOptions'.
W tej sytuacji kontynuujemy poprawę generacji testów, kontynuując "rozmowę" z chatem:
INPUT: Dla:
it('should return false when date range is invalid', () => {
const form = new FormGroup({
dateRangeField: new FormControl('', { dateRange: true })
});
Mam błąd:
9:45 - error TS2345: Argument of type '{ dateRange: boolean; }' is not assignable to parame
ter of type 'ValidatorFn | ValidatorFn[] | AbstractControlOptions | null | undefined'.
Object literal may only specify known properties, and 'dateRange' does not exist in type
'ValidatorFn | ValidatorFn[] | AbstractControlOptions'.
OUTPUT:
Błąd wynika z faktu, że Angular nie rozpoznaje właściwości dateRange jako wbudowanego walidatora, ponieważ został on zdefiniowany w niestandardowy sposób w usłudze FormValidationService. Aby ominąć ten błąd, możemy zdefiniować własny walidator dla pola dateRangeField bezpośrednio w teście. Oto jak to zrobić: import { TestBed } from '@angular/core/testing';
import { FormGroup, FormControl, Validators } from '@angular/forms';
import { FormValidationService } from './form-validation.service';
describe('FormValidationService', () => {
let service: FormValidationService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(FormValidationService);
});
it('should return false when date range is invalid', () => {
const form = new FormGroup({
dateRangeField: new FormControl('', service.dateRangeValidator('startDate', 'endDate'))
});
// Ustawienie daty końcowej przed datą początkową, aby spowodować błąd zakresu dat
form.patchValue({
startDate: '2024-05-01',
endDate: '2024-04-01'
});
expect(form.controls['dateRangeField'].hasError('dateRange')).toBe(true);
});
});
Jednakże ten test nadal nie zadziała, otrzymamy błąd:
Type '(formGroup: FormGroup) => void' is not assignable to type 'ValidatorFn'.
Types of parameters 'formGroup' and 'control' are incompatible.
Możemy kontynuować poprawę błędów z pomocą chatu, dostarczając mu coraz więcej informacji o problemie, lub też samodzielnie poprawić test. Z mojego doświadczenia, na etapie generowania podstawowych testów, łatwiej i szybciej jest samodzielnie wprowadzić poprawki. Niemniej jednak, ilość kodu wygenerowanego przez chat w tak krótkim czasie stanowi znaczącą pomoc.
Refaktoring kodu
W dzisiejszym dynamicznym świecie programowania, refaktoring kodu jest nieodłącznym elementem procesu rozwoju oprogramowania. W tym kontekście, wykorzystanie zaawansowanych narzędzi opartych na sztucznej inteligencji może znacząco ułatwić i przyspieszyć ten proces. Dzięki zdolności do generowania naturalnie brzmiącego tekstu, ChatGPT może pomóc programistom w identyfikowaniu obszarów kodu wymagających optymalizacji, sugerując alternatywne podejścia lub lepsze praktyki programistyczne. Ponadto, poprzez analizę istniejącego kodu i generowanie propozycji refaktoringu. Warto zaznaczyć, że obecnie istnieją lepsze narzędzia do tego celu, takie jak SonarQube, jednak nadal możliwe jest wykorzystanie chatu:
INPUT: zrefaktoryzuj kod:
public static void main(String[] args) {
List<String> fruits = new ArrayList<>();
fruits.add("apple");
fruits.add("banana");
fruits.add("orange");
fruits.add("kiwi");
int count = 0;
for (String fruit : fruits) {
if (fruit.startsWith("a")) {
count++;
}
}
System.out.println("Number of fruits starting with 'a': " + count);
}
OUTPUT:
Oto zrefaktoryzowany kod, który wykorzystuje strumienie do zliczania owoców rozpoczynających się od litery 'a':
public class RefactoringAndOptimization {
public static void main(String[] args) {
List<String> fruits = Arrays.asList("apple", "banana", "orange", "kiwi");
long count = fruits.stream()
.filter(fruit -> fruit.startsWith("a"))
.count();
System.out.println("Number of fruits starting with 'a': " + count);
}
}
W tym zrefaktoryzowanym kodzie używamy strumienia do przetwarzania listy owoców. Metoda filter służy do wyfiltrowania tylko tych owoców, które rozpoczynają się od litery 'a', a następnie wywołujemy metodę count, aby zliczyć ilość pasujących elementów. W rezultacie uzyskujemy tę samą funkcjonalność, ale z bardziej czytelnym i zwięzłym kodem. Użyto metody Arrays.asList() do bezpośredniego tworzenia listy owoców zamiast tworzenia pustej listy i dodawania elementów osobno. Jest to bardziej czytelne i zwięzłe podejście.
W tym prostym przykładzie Chat poprawił metodę i dostarczył słowne wyjaśnienie zmian. Pozwolenie na napisanie kawałka kodu przez programistę może zmniejszyć liczbę błędów wynikających z implementacji. Powyższy przykład jest prosty, ale w trakcie pracy z Chatem jego sugestie były przydatne również przy bardziej złożonych klasach.
Podsumowanie
Przeprowadzając testy wykorzystania sztucznej inteligencji przy pracy programistycznej, nasunęły mi się następujące wnioski:
Chat dobrze radzi sobie z generowaniem kodu na podstawie opisu słownego, schematu bazy danych oraz generacji opisanej logiki. W czasie pracy z nim czułem się jakbym współpracował z młodszym programistą, który radzi sobie z prostymi zadaniami, jednak jego praca wymaga regularnego sprawdzania w celu wykrycia błędów.
Ze względu na zbiór danych wykorzystanych do uczenia chata, najnowsze rozwiązania mogą nie być przez niego dostępne, a optymalizacja i refaktoryzacja kodu są na solidnym, ale nie idealnym poziomie. Podejrzewam, że jest to związane z bazą danych, na której model był szkolony. Optymalne rozwiązania są przykryte przez znaczną ilość rozwiązań solidnych, które przysłaniają pojedyncze, potencjalnie lepsze implementacje. Nie uważam tego za znaczący problem.
Patrząc na aktualne możliwości generacji aplikacji przez chat, podejrzewam, że z czasem rola programisty może sprowadzić się do łączenia gotowych bloków kodu generowanych na podstawie opisu słownego. Pojawiają się już dedykowane rozwiązania, takie jak np. Copilot od GitHuba, szkolony na bazie danych GitHuba, umożliwiający automatyczne uzupełnianie kodu oraz sugerowanie całych funkcji.
Aspekty, na które należy zwrócić uwagę, używając CHAT-GPT:
Ograniczenie zdolności rozwojowych: Nie polecam jego używania dla doświadczonych programistów. Brak umiejętności rozwiązywania problemów: Jest to model językowy, który sam w sobie nie jest w stanie zrozumieć ani wejścia, ani wyjścia. Nie rozwiąże wszystkich problemów, jakie mamy w projekcie. Chat nie jest stworzony do pisania wydajnego kodu. Doświadczony programista będzie w stanie zoptymalizować metody wygenerowane przez chat.
*Artykuł został przepuszczony przez CHAT-GPT 3.5 w celu sprawdzenia i poprawienia.