GPT-4 z ~25 milionów znaków Twojej wiedzy
GPT-4 z ~25 milionów znaków Twojej wiedzy
Asystent AI umożliwiający rozmowę z naszą bazą wiedzy, zwykle przychodzi na myśl jako pierwszy w kontekście praktycznego zastosowania GPT-4. Rozwijając projekt Alice sam także eksploruję takie możliwości. Pomyślałem więc, że podzielę się swoim doświadczeniem w pracy z dość dużą bazą treści, skupiając się przy tym zarówno na możliwościach, problemach, ograniczeniach i pytaniach bez jednoznacznej odpowiedzi. Tym razem jednak pominę głębokie aspekty techniczne, ponieważ na nie przyjdzie czas przy innej okazji.
Asystent AI umożliwiający rozmowę z naszą bazą wiedzy, zwykle przychodzi na myśl jako pierwszy w kontekście praktycznego zastosowania GPT-4.
Rozwijając projekt Alice, sam także eksploruję takie możliwości. Pomyślałem więc, że podzielę się swoim doświadczeniem w pracy z dość dużą bazą treści, skupiając się przy tym zarówno na możliwościach, problemach, ograniczeniach i pytaniach bez jednoznacznej odpowiedzi. Tym razem jednak pominę głębokie aspekty techniczne, ponieważ na nie przyjdzie czas przy innej okazji.
Wizja i cel projektu "Alice"
Projekt "Alice" prawdopodobnie już znasz, jednak powiem o nim teraz trochę więcej. Rozwijam go w celu połączenia mojej codzienności z AI zarówno w wymiarze zawodowym, jak i prywatnym. Uwzględnia to zarówno dostęp do aktualnej i szerokiej bazy wiedzy, jak i możliwości integracji z usługami, a nawet urządzeniami. Ze względów złożoności projektu, jego najbardziej zaawansowana forma dostępna jest wyłącznie dla mnie. Natomiast wersja w postaci aplikacji desktopowej, działa niezależnie na komputerze klienta i komunikuje się bezpośrednio z OpenAI.
Alice stopniowo poszerza bazę wiedzy i umiejętności. Posiada także mechanizmy refleksji nad bieżącą konwersacją, łącząc je (gdy jest taka potrzeba) ze swoimi wcześniejszymi wspomnieniami. Jest to istotne z punktu widzenia przechowywania informacji oraz strategii ich odzyskiwania, co wpływa na ogólną skuteczność systemu. Nietrudno się domyślić, że zaprojektowanie tych mechanizmów, nie jest oczywiste. Istnieje też ogromna różnica pomiędzy projektowaniem takich mechanizmów na własne potrzeby oraz robieniem tego, z myślą o innych użytkownikach. Z tego powodu wersja aplikacji desktopowej, posiada tylko wybrane funkcjonalności, które pomijają np. długoterminową pamięć asystenta (choć być może uda się to zmienić).
Podsumowując, o Alice możesz myśleć w następujących wymiarach:
- prywatnym (hiperpersonalizacja, integracje, nieograniczony dostęp)
- publicznym (wybrane bazy wiedzy, ograniczone umiejętności, dodatkowe weryfikacje oraz moderacja)
- aplikacji desktopowej (obecnie dane przechowywane lokalnie na komputerze użytkownika, bezpośrednie połączenie z OpenAI)
W tym wpisie skupimy się przede wszystkim na drugim wymiarze, uwzględniającym pracę z bazą wiedzy, której wielkość to obecnie ~25 milionów znaków. Wersja ta dostępna jest w ramach platformy eduweb.pl i jest jeszcze na bardzo wczesnym etapie rozwoju.
Struktura bazy wiedzy i rodzaje informacji
W publicznej wersji, baza wiedzy Alice obejmuje nasze publikacje z eduweb. Niebawem uzyska także dostęp do ahoy oraz bazy naszych newsletterów. Mówimy więc tutaj o sporej bazie tekstu, która domyślnie jest podzielona na różnego rodzaju kategorie oraz tagi, co odegra istotną rolę w kontekście pracy z LLM.
Jak zapewne wiesz, obecnie duże modele językowe posiadają ograniczony kontekst pojedynczej interakcji (8 tys. tokenów dla modelu GPT-4). Limit dotyczy zarówno tekstu, który przekazujemy w zapytaniu, jak i ten, który zostaje wygenerowany przez model. Musimy więc pamiętać o tym, aby zostawić przestrzeń na informacje dotyczące samej rozmowy oraz generowaną odpowiedź.
Zatem konieczne jest podzielenie naszej bazy treści na mniejsze części (tzw. chunki lub dokumenty) oraz opracowanie sensownej strategii ich odzyskiwania oraz osadzenia w kontekście. Mówimy tutaj o przygotowaniu indeksu treści, która będzie mogła być skutecznie przeszukiwana, na podstawie zapytania użytkownika. W rezultacie do kontekstu musi trafić wszystko, co jest potrzebne do aktualnie realizowanego zadania.
Aby to zobrazować, załóżmy, że użytkownik zadaje pytanie: "Na jakiej bazie pracuje Alice"? Wówczas na jego podstawie, do kontekstu zostanie dodany następujący fragment aktualnego wpisu:
Na jego podstawie GPT-4 będzie w stanie bez problemu udzielić poprawnej odpowiedzi. Po prostu wykorzysta dostarczone informacje, wybierając tylko te, które faktycznie mają znaczenie w danej chwili.
Sytuacja zaczyna się komplikować, gdy udzielenie odpowiedzi wymaga więcej niż jednego dokumentu. Wówczas musimy upewnić się, że on także zostanie dołączony do kontekstu rozmowy. Jeżeli tak się nie stanie, GPT-4 nadal będzie próbował udzielić odpowiedzi na podstawie posiadanych informacji.
Powyższa odpowiedź jest na pierwszy rzut oka poprawna, ale w świetle całego wpisu, bardzo ograniczona. Z tego powodu strategia odzyskiwania informacji i budowania kontekstu, powinna sprawić, by przy generowaniu odpowiedzi, został uwzględniony także dodatkowy, poniższy fragment.
Dzięki niemu odpowiedź na dokładnie to samo pytanie zostanie wzbogacona o dodatkowe szczegóły, które rysują nieco szerszy, bardziej pomocny obraz. Naturalnie w tym przykładzie fragmenty wybrałem samodzielnie do zadawanego pytania. Zwykle jednak dzieje się to automatycznie i w przypadku, gdy odpowiadający za to mechanizm zostanie zbudowany niepoprawnie, to do kontekstu mogą trafić informacje obniżające skuteczność odpowiedzi lub nawet bezpośrednie wprowadzające w błąd.
Doprowadzenie do sytuacji, w której fragmenty kontekstu działają na niekorzyść generowanej odpowiedzi, jest banalnie proste. Np. dla systemu działającego na treści wpisu, który właśnie czytasz, konieczne jest podzielenie tekstu na mniejsze części. Zwykle podział opiera się na liczbie tokenów, a długość fragmentu zależy od rodzaju treści z którą pracujemy. Ogólna zasada długości mówi o tym, aby fragment był na tyle długi, aby nie zawierał informacji wyrwanych z kontekstu, oraz na tyle krótki aby nie zawierał szumu. Tutaj istotną informacją jest fakt, że token to fragment słowa lub całe słowo. Co więcej tekst o tej samej treści w języku angielskim składa się ze znacznie mniejszej liczby tokenów niż dla języka polskiego. Poniżej jeden z naszych fragmentów według tokenizera składa się z 337 tokenów, podczas gdy jego angielska wersja to zaledwie 132 tokeny.
Oznacza to, że nieporównywalnie lepiej jest pracować z kontekstem zapisanym w języku angielskim, ponieważ mniejsza liczba tokenów pozwoli nam uwzględnić większą ilość informacji. Z tego powodu w przypadku Alice, angielski jest jej podstawowym językiem ale naturalnie może komunikować się także w języku polskim. Jednak w chwili gdy zadaję jej pytanie w języku polskim, tłumaczy go na angielski z pomocą usługi Deepl.
Poza tym, przy wyszukiwaniu fragmentów dla aktualnego zapytania, nie powinniśmy opierać się wyłącznie na aktualnym zapytaniu użytkownika ale na nieco szerszym kontekście rozmowy. Wyobraź sobie scenariusz w którym użytkownik zadaje pytanie "Kim jest Alice?". Wówczas słowo kluczowe "Alice" sprawia, że do kontekstu zostają dobrane fragmenty zawierające informacje na jej temat. Jeżeli jednak użytkownik w kolejnej wiadomości dopyta "powiesz coś więcej?", to samo pytanie nie zawiera żadnych wskazówek pozwalających na dobranie odpowiednich fragmentów. W takich sytuacjach kluczowe staje się wprowadzenie dodatkowego mechanizmu refleksji pozwalającego określać temat aktualnej rozmowy. Ten przypadek obrazuje następująca konwersacja:
- Alice, przedstawisz się?
- (dobiera informacje o Alice i udziela odpowiedzi)
- Powiesz coś więcej?
- (gubi kontekst rozmowy i zadaje pytanie "o co konkretnie pytasz?")
Warto o tym pamiętać.
Podsumowując:
- Treść na potrzeby LLM musi być podzielona na mniejsze fragmenty
- Fragmenty powinny być dzielone według długości lub liczby tokenów
- Długość fragmentów nie musi być stała, ale powinna uwzględniać fakt pojawienia się w limitowanym kontekście modelu
- Liczba tokenów dla tekstu w języku angielskim jest niższa niż w języku polskim
- Informacje muszą być dobierane w sposób, który zminimalizuje ryzyko halucynacji modelu i generowania nieprzydatnych odpowiedzi
- Do przygotowania treści wykorzystałem własne skrypty przy których projektowaniu pomagała mi Alice oraz narzędzia biblioteki LangChain.
Strategia podziału i odzyskiwania treści dla kontekstu LLM
Przeglądając Internet, odnoszę wrażenie, że przygotowanie treści na potrzeby systemów pracujących z LLM, jest bardzo mocno niedoprecyzowane i nikt jeszcze nie wie, jak robić to skutecznie.
Powstają jednak pierwsze narzędzia oraz techniki, na które warto zwrócić uwagę. Dotychczasowe doświadczenie sugeruje mi jednak, że najlepiej jest opracować własny plan na zorganizowanie oraz późniejsze przeszukiwanie treści. Domyślam się, że z czasem pojawią się frameworki i wzorce na których będzie można się opierać. Do tego momentu działamy raczej mało znanym obszarze i na wiele pytań będziemy zmuszeni odpowiedzieć sobie sami.
W przypadku Alice zastosowałem strategię, która świetnie sprawdza mi się przy rozwijaniu mojego Cyfrowego Ogrodu, polegającej na tym aby ustalić bardzo prostą strukturę i pozostawać w niej tak długo, jak to możliwe. Rozdzieliłem więc pamięć Alice na jej prywatne wspomnienia (na jej temat), moje notatki, źródła wiedzy, historię wiadomości oraz umiejętności. Każda z tych kategorii może posiadać także swoje podkategorie oraz tagi pozwalające na późniejsze łączenie treści. Poza tym wspomnienia posiadają także dodatkowe metadane, pozwalające na odzyskanie np. treści związanych tylko z konkretnym wydaniem newslettera eduweb.
Powyższa taksonomia wykorzystywana jest praktycznie przy każdej interakcji z Alice. Podczas zapamiętywania pozwala na odpowiednie zapisanie oraz opisanie informacji, a podczas wykorzystywania, ułatwia ich odnalezienie.
Zatem przykładowo, jeżeli Alice czyta newsletter, zapisuje go w sekcji "Notes" wraz z tytułem newslettera, kategorią oraz tagami. Przypisanie tych informacji odbywa się częściowo za pośrednictwem kodu (np. sztywne wstawienie tytułu newslettera) a częściowo za pośrednictwem promptów (np. generowanie tagów na podstawie zapisywanej treści oraz aktualnej listy tagów pozwalającej na ewentualne przypisanie do już istniejącego).
W praktyce więc gdy do Alice trafi prośba o podsumowanie aktualnej lekcji eduweb, przeszukiwanie będzie odbywać się wyłącznie we fragmentach, których metadane pasują do metadanych aktualnie otwartej lekcji. Nie jest to jednak jedyne źródło informacji, ponieważ cały prompt uwzględnia także inne sekcje źródeł danych (np. informacje o kursach czy wyniki wyszukiwania umożliwiające poprowadzenie do zewnętrznych źródeł wiedzy). W rezultacie ogólny prompt na którym pracuje Alice, składa się z części wymienionych poniżej.
Poza zbudowaniem kontekstu istotne jest także określenie zasad jego wykorzystania. Przykładowo Alice zdaje sobie sprawę, że transkrypcje mogą zawierać błędy oraz posiada wiedzę na temat sposobu budowania linków do lekcji, które w celu oszczędzania tokenów nie są dołączane do promptu.
Kompletny, aktualny schemat generowania odpowiedzi, składa się z kilku kroków, które są realizowane w zależności od spełnionych warunków.
Uproszczona lista kroków wygląda następująco:
Zatem w pierwszej kolejności weryfikuję zapytanie pod kątem polityki OpenAI oraz naszych własnych zasad. Następnie tłumaczę je na język angielski, wzbogacam poprzez dodanie np. istotnych słów kluczowych. W ten sposób uzyskuję zestaw informacji, które mogę wykorzystać do klasyfikacji (np. określenia tematyki) na podstawie której przeszukuję sieć oraz nasze bazy wiedzy, które trafiają do kontekstu.
Podsumowując:
- Baza wspomnień Alice oparta jest o możliwie prostą strukturę oraz serię metadanych i tagów, które pozwalają łączyć ze sobą dokumenty (np. odbudować transkrypt lekcji, łącząc jego pojedyncze fragmenty)
- Pojedynczy dokument zostaje zapisany w języku angielskim (aby zredukować liczbę tokenów) i jest nie dłuższy niż ~500 tokenów, czyli około 350 słów
- Dane przed zapisaniem zostały oczyszczone z niepotrzebnych informacji. W niektórych przypadkach informacje te trafiły do metadanych dokumentu
- Aktualizacja lub usunięcie raz zaindeksowanych informacji odbywa się na podstawie ich metadanych (np. mogę z łatwością znaleźć opisy lekcji X i je zaktualizować lub usunąć)
- Wyszukiwanie jest wieloetapowe i uwzględnia filtrowanie zarówno na poziomie słów kluczowych, promptów klasyfikujących zapytanie oraz similarity search
Debugowanie, monitorowanie i bezpieczeństwo
Na etapie przygotowania aplikacji, która ma zostać oddana w ręce użytkowników, trudno przewidzieć w jaki sposób faktycznie będzie wykorzystywana. Co więcej, niedeterministyczna natura dużych modeli językowych sprawia, że przewidzenie ich zachowania jest praktycznie niemożliwe. Musimy jednak być w stanie monitorować zachowanie systemu oraz zadbać o kwestie bezpieczeństwa czy utrzymanie kosztów na akceptowalnym poziomie. Do tego dochodzi także wątek związany z wczesnym etapem rozwoju tej technologii i często spotykanych problemach z dostępem do usług.
Bezpieczeństwo
Podejrzenie promptu naszego systemu lub nawet zmiana jego zachowania, stanowią bardzo poważne problemy bezpieczeństwa. Co więcej, obecnie wydają się być elementem fundamentalnej natury LLM a ich całkowite rozwiązanie jest niemożliwe. Nie oznacza to jednak, że nie możemy podjąć działań mających na celu zminimalizowanie ryzyka blokady naszego konta lub narażenia się inne problemy związane z nietypowym zachowaniem użytkowników.
Niezbędnym minimum jest korzystanie z Moderation API, które weryfikuje zapytanie pod kątem zgodności z polityką OpenAI. Następnie konieczne jest zadbanie o zaprojektowanie promptu w sposób blokujący zmianę jego zasad. W moim przypadku stosuję także elementy Constitutional AI w celu zweryfikowania promptu pod kątem zdefiniowanych przeze mnie reguł. W ten sposób doprowadziłem do wykrywania wszystkich znanych mi technik adversarial prompting, aczkolwiek mam świadomość, że złamanie tego promptu nadal jest możliwe. Z tego powodu stosuję dodatkowe mechanizmy zabezpieczające, które wykrywają naruszenie bezpieczeństwa "po fakcie", informując mnie o zaistniałej sytuacji. Krytycznie ważne staje się także stosowanie KYC (Know-Your-Customer) w celu identyfikacji użytkowników.
W zasadzie mówimy tutaj o pełnym zastosowaniu rekomendacji OpenAI: https://platform.openai.com/docs/guides/safety-best-practices
Monitorowanie
Poza kwestiami bezpieczeństwa, konieczne jest także szczegółowe monitorowanie aktualnego stanu systemu w momencie wysyłania zapytania. Każdy z wymienionych przeze mnie kroków jest logowany, przez co mogę z łatwością prześledzić co wydarzyło się przy realizowaniu zapytania oraz co wpłynęło na ewentualne błędne zachowanie. W bazie danych mam przewidziane dodatkowe pola, które zapisują nie tylko zapytania użytkowników oraz generowane odpowiedzi ale także historię błędów oraz strukturę promptów na podstawie których została wygenerowana odpowiedź.
W praktyce, wykorzystuję logger NestJS, dodatkowe kolumny w bazie danych w których zapisuję prompty oraz metadane zapytania. Posiadam także system notyfikacji (make.com & slack) typu "heartbeat" a także dodatkowe powiadomienia dla scenariuszy awaryjnych. Inaczej mówiąc — system nieustannie monitoruje wszystkie niepożądane zachowania.
Debugowanie
Debugowanie staje się znacznie prostsze w sytuacji, gdy posiadamy już monitoring systemu. Poza monitorowaniem istotna staje się także obsługa błędów uwzględniająca szczegółowe komunikaty wysyłane zarówno do mnie, jak i użytkownika (aczkolwiek tutaj zwykle sprowadza się to do jasnego poinformowania o niedostępności systemu lub zgłoszeniu wiadomości do moderacji).
Zakładając więc, że zachowanie Alice jest niezgodne z oczekiwaniami, mogę wczytać stan dla dowolnego etapu interakcji z systemem i następnie wykryć potencjalną przyczynę problemu. Większym wyzwaniem staje się wprowadzenie zmian w sposób, który nie doprowadzi do regresji. Na to jednak nie mam odpowiedzi a Alice nie posiada sensownego mechanizmu testującego automatycznie jej zachowania.
Aktualizacje, wydajność i koszty
Ze względu na znaczne tempo rozwoju LLM, ważnym elementem rozwoju Alice jest dla mnie zachowanie elastyczności oraz możliwości aktualizacji dowolnego fragmentu jej wiedzy. Poza tym, ważną rolę odgrywają także koszty oraz wydajność i skuteczność całego systemu. Na każde z tych zagadnień można poświęcić cały wpis, jednak w uproszczeniu wygląda to następująco:
- Aktualizacje — ogólna struktura aplikacji i przechowywanych danych jest tak prosta, że jestem w stanie odbudować całość w kilka godzin. Unikam złożoności i szukam abstrakcji umożliwiających reużywanie tych samych komponentów. Np. identyfikacja zapytania zbudowana jest w sposób umożliwiający wykorzystywanie jej także na potrzeby zapisywania i aktualizacji informacji
- Baza danych — przechowuję informacje równolegle w bazie wektorowej (pinecone) oraz w PostgreSQL. Każdy wpis posiada swoje unikatowe identyfikatory i metadane umożliwiające łatwe odnalezienie powiązanych informacji. W ten sposób mogę np. zaktualizować opisy kursów lub transkrypcje, z pomocą jednego zapytania
- Wydajność — w sytuacji gdy realizowane zadanie jest prostsze, korzystam z modelu GPT-3.5-Turbo. Zadania, które mogą być realizowane niezależnie, wykonuję równolegle. Do tłumaczeń wykorzystuję skuteczne i bardzo szybkie Deepl. Planuję wprowadzić także cache'owanie bardzo podobnych zapytań dla wybranego kontekstu, podobnie jak realizuje to https://www.perplexity.ai. Ostatecznie zadania, które mogą być wykonane w tle, realizowane są poza bezpośrednią interakcją z użytkownikiem
- Koszty — w każdym istotnym punkcie systemu, wykorzystuję model GPT-4. Dbam jednak o to, aby zapytanie pracowało na minimalnej liczbie tokenów oraz generowało możliwie zwięzłe odpowiedzi.
- Koszty embeddingu — embedding to proces zamiany tekstu na listę wektorów, które można zapisać w bazie wektorowej w celu późniejszego przeszukiwania treści. Embedding przeprowadzam z pomocą modelu text-embedding-ada-002, którego koszt dla ~30 milionów znaków wyniósł kilkadziesiąt dolarów. Niestety nie jestem w stanie podać konkretnych wyników, bo OpenAI nie podaje tak szczegółowych raportów. Nadal alternatywnie możemy skorzystać z modeli offline (polecam ten ranking: https://huggingface.co/spaces/mteb/leaderboard) i zredukować koszty embeddingu praktycznie do zera.
Podsumowanie
Zbudowanie systemu wykorzystującego OpenAI oraz naszą własną bazę wiedzy, jest dość wymagające, ponieważ stabilność platformy pozostawia wiele do życzenia, a obecne narzędzia są jeszcze na wczesnym etapie rozwoju. Przykładowo LangChain całkiem dobrze sprawdził się do pracy z dokumentami, jednak nie sprawdza się do faktycznego prowadzenia interakcji z modelem.
Dodatkową komplikacją jest także znaczny spadek skuteczności modeli OpenAI, który jest bardzo zauważalny nawet w przypadku prostych zadań oraz generowaniem odpowiedzi w języku polskim. Z tego powodu uważam, że obecna technologia nie jest jeszcze gotowa do produkcyjnych zastosowań w obszarach krytycznych. Może jednak sprawdzać się jako dodatek lub dobrze radzić sobie jako system wspierający, działający pod kontrolą developerów oraz osób znających mechaniki LLM. Bardzo pomocne okazuje się utrzymywanie systemu możliwie prostym, aby wprowadzanie zmian było maksymalnie łatwe i szybkie.
Ostatecznie jednak można doprowadzić do sytuacji w której pracujący dla nas system zaczyna wnosić realną wartość. Na ten moment wyraźnie obserwuję to w izolowanych przypadkach, gdy Alice ma do zrealizowania zadania osadzone w ściśle określonym kontekście. Wersję dostępną na eduweb czekają jeszcze zmiany, które stopniowo wprowadzamy poprzez praktycznie codzienne aktualizacje. Z mojego punktu widzenia bardzo wyraźny jest także rozwój modeli, który faktycznie zalicza różne wpadki, ale całościowo technologia którą widzimy dzisiaj, oferuje zupełnie inne możliwości niż ~7 miesięcy temu.
Zbierając to wszystko w całość, oto lista wniosków, które mogą okazać się przydatne przy tworzeniu integracji OpenAI z własną bazą danych:
- zaplanowanie struktury treści oraz schematu metadanych odgrywa najważniejszą rolę w budowie całego systemu. Błędy popełnione na tym etapie trudno jakkolwiek naprawić
- zapewnienie elastyczności zarówno aplikacji, jak i samej bazy treści, jest przydatne do zachowania dynamiki w obliczu zmieniającej się technologii
- zadbanie o kwestie bezpieczeństwa jest na tyle istotne, że bez nich można całkowicie darować sobie rozwój aplikacji
- KYC, monitorowanie, obsługa błędów i system powiadomień, potrzebne są przy rozwoju aplikacji oraz adresowania nierozwiązanych problemów LLM dotyczących bezpieczeństwa oraz ich niedeterministycznej natury
- zdecydowanie warto zapoznać się z możliwościami offline'owych modeli w kontekście redukcji kosztów oraz zachowania prywatności
To wszystko! Mam nadzieję, że w powyższym wpisie znajdziesz coś, co okaże się przydatne przy integracji LLM z Twoją bazą wiedzy.
- Własna baza wiedzy
- Organizacja i wyszukiwanie
- KYC, bezpieczeństwo i monitoring
- Aktualizacje i problemy produkcyjne