Twój prywatny czatbot AI, czyli bliskie spotkanie z RAG
Twój prywatny czatbot AI, czyli bliskie spotkanie z RAG
ChatGPT posiadający wiedzę na nasz temat oraz dostęp do wybranych dokumentów brzmi jak "pamięć doskonała" czy rozwiązanie problemów związanych z przepływem informacji w firmie. Aplikacje typu "porozmawiaj z PDF", "Twój asystent AI" czy "Drugi Mózg" składają niesamowicie duże obietnice, których nie są w stanie dowieźć. Tylko dlaczego?
ChatGPT to interfejs czatu umożliwiający prowadzenie konwersacji z dużymi modelami językowymi, np. GPT-3.5/GPT-4, które domyślnie nie posiadają aktualnej wiedzy czy informacji na nasz temat. Jeżeli jednak w trakcie rozmowy wykorzystamy jakiś fragment, np. opis produktu, to model będzie w stanie użyć go do realizacji wybranego zadania, takiego jak udzielenie odpowiedzi na pytania związane z tym produktem.
Pierwsze problemy zaczynają się gdy chcemy pracować z większym dokumentem (np. dokumentacją) lub wręcz całą bazą wiedzy. Wówczas zderzamy się z limitem kontekstu, który możemy postrzegać jako krótkoterminową pamięć modelu. Ten limit może nie być oczywisty dla osób korzystających z ChatGPT, ponieważ niejawnie adresuje on ten problem umożliwiając nam przeprowadzanie długich konwersacji. Informację o tym ograniczeniu możemy spotkać, gdy w jednej wiadomości spróbujemy przesłać długi tekst. Wówczas otrzymamy następujący błąd:
Nie oznacza to jednak, że nie mamy możliwości połączenia LLM (Large Language Model) z dużymi bazami wiedzy. Taką możliwość sugeruje system RAG (Retrieval-Augmented Generation), którego główna koncepcja polega na połączeniu bazowej wiedzy modelu z zewnętrznymi źródłami danych.
Domyślnie np. GPT-4 odpowiada na pytania bezpośrednio, wykorzystując swoją bazową wiedzę. Oznacza to, że w sytuacji gdy pytamy o coś, co nie jest w niej zawarte, otrzymamy informację o braku danych lub co gorsza błędną odpowiedź wprowadzającą nas w błąd bez jakiegokolwiek ostrzeżenia.
System RAG rozszerza powyższy schemat, dzięki czemu model sięga po zewnętrzne dane, które trafiają do kontekstu zapytania. Dodatkowo instrukcja systemowa uwzględnia konieczność ich wykorzystania podczas generowania odpowiedzi. Dane o których mowa mogą pochodzić np. z wyników wyszukiwania w Internecie czy treści dokumentów PDF.
Samo połączenie ze źródłami danych nie jest wystarczające, ponieważ nadal obowiązuje nas limit kontekstu dla pojedynczego zapytania, który obejmuje zarówno treść bieżącej konwersacji, jak i dostarczonych danych. Z tego powodu większe ilości danych muszą zostać podzielone na fragmenty, a następnie dynamicznie dołączane do konwersacji, podobnie jak to miało miejsce w przypadku wspomnianego "wklejania opisu produktu", ale tym razem odbywa się to automatycznie z pomocą silników wyszukiwania.
Zatem RAG to w uproszczeniu:
- instrukcja systemowa mówiąca o udzielaniu odpowiedzi na podstawie dostarczonego kontekstu oraz opis zachowania w przypadku niewystarczających danych
- połączenie z zewnętrznym źródłem danych, którego fragmenty zostają dynamicznie wczytywane do kontekstu w trakcie konwersacji
- narzędzia umożliwiające wyszukiwanie istotnych informacji (np. hybrydowe wyszukiwanie łączące silniki wyszukiwania takie jak Algolia Search z zapytaniami do bazy wektorowej)
- narzędzia przygotowujące treści (np. dzielące je na mniejsze fragmenty, wzbogacające, podsumowujące) do formy nadającej się do wykorzystania jako dynamiczny fragment, ponieważ nie zawsze treści czytelne dla człowieka są zrozumiałe dla modeli językowych.
To wszystko brzmi banalnie prosto, bo wystarczy zmodyfikować prompt, podzielić długie treści na mniejsze i wyszukać istotne fragmenty na podstawie zapytania użytkownika. W praktyce, Świat jest znacznie bardziej złożony i taki schemat może wystarczyć jedynie na potrzeby wczesnego prototypu i już wyjaśniam, dlaczego tak jest.
Pierwsze problemy związane z budową systemów RAG
Zbudowanie prostego systemu RAG nie jest szczególnie wymagające. Pomijając element wyszukiwania, nawiązanie kontaktu z krótkim plikiem .txt możliwe jest poprzez poniższy kod wykorzystujący framework LangChain.
Jeśli nie programujesz w JavaScript, zwróć uwagę na komentarze. Dokładnie jak napisałem wcześniej, mówimy tutaj o:
- wczytaniu pliku
- podziale jego treści na mniejsze fragmenty
- wczytanie fragmentów do kontekstu
- wygenerowanie odpowiedzi na podstawie dostarczonych fragmentów
W związku z tym, że plik zawiera kilka informacji na mój temat, wygenerowana odpowiedź poprawnie mnie opisuje. Oznacza to, że nasz podstawowy system działa, ale jego możliwości szybko kończą się w momencie, gdy wzrośnie objętość pliku memory.md na którym pracujemy.
Nawet w przypadku stosunkowo małych plików, wczytywanie ich całej treści do kontekstu ma negatywny wpływ na jakość odpowiedzi, czas jej generowania oraz koszty związane z przetwarzaniem tzw. tokenów. Natomiast i tak nadrzędnym problemem pozostaje limit kontekstu.
Tutaj ponownie we wprost banalny sposób możemy skorzystać z tzw. embeddingu oraz działający w pamięci store dla wektorów. Jeśli te pojęcia nic Ci nie mówią, to mówimy tutaj o wyszukiwaniu istotnych fragmentów dokumentu, które trafią do kontekstu konwersacji. Poniższy przykład pokazuje, że wystarczą zaledwie dwie dodatkowe funkcje, aby nasz mechanizm zaczął działać na większych dokumentach.
UWAGA: poniższy kod generuje embedding za każdym razem przy uruchomieniu aplikacji. Jeśli plik na którym pracujesz będzie duży, szybko wygenerujesz ogromne koszty na swoim koncie OpenAI. Jest to jedynie przykład nienadający się do produkcyjnego zastosowania.
Wyszukiwanie dokumentów powiązanych z zapytaniem użytkownika pozwoliło na precyzyjne wybranie informacji istotnej do udzielenia poprawnej odpowiedzi. No, i tutaj zaczyna się prawdziwy rollercoaster wyzwań, z którymi zderzysz się rozwijając taki system.
Wyzwania systemów RAG
Chciałbym teraz przedstawić Ci problemy związane z systemami RAG, jednak szczerze mówiąc, nie wiem od którego zacząć, na których się skupić i które pominąć. Dlatego przejdziemy sobie przez kilka obszarów, jednak miej z tyłu głowy fakt, że jest jeszcze mnóstwo wątków, które celowo na tym etapie pomijam.
Wyobraźmy sobie przykład mojego ostatniego e-booka "Codzienność z GPT-4", który moglibyśmy podłączyć do LLM umożliwiając rozmowę z jego treścią. Jeżeli zadamy pytanie "Czym jest GPT-4", to prawdopodobnie udzielona odpowiedź będzie bardzo precyzyjna, ponieważ w jego treści znajduje się akapit, który o tym mówi. Jeśli jednak nasze pytanie będzie dotyczyło "wszystkich makr omówionych w e-booku", to wygenerowana odpowiedź może nie być kompletna w przypadku gdy kontekst budowany jest jedynie poprzez wybranie "topX najbardziej istotnych dokumentów".
Obrazując ten scenariusz, wyobraźmy sobie treść e-booka:
- Wprowadzenie
- Definicja GPT-4
- Makro 1
- Makro 2
- Makro 3
- Makro 4
- ...
- Makro 10
W przypadku pierwszego pytania wystarczy pobrać dokument #2 i dołączyć jego treść do kontekstu. W przypadku pytania o makra, musielibyśmy dołączyć dokumenty #3-#8 i wiązałoby się to z ryzykiem przekroczenia limitu kontekstu. Dlatego musielibyśmy zastosować inne podejście związane z ogólną organizacją tej treści (np. przygotowaniem krótkich wersji dokumentów) lub generowania odpowiedzi w pętli przechodzącej przez każdy dokument.
Nieoczywiste jest jednak to, że znajdziemy dokumenty pasujące do zapytania użytkownika. Chociażby w sytuacji gdy zapytałby on o to "Czym są duże modele językowe", to szansa na to, że nasz system znalazłby dokument #2 zawierający definicję GPT-4 spada. Co więcej sytuacja znacznie by się pogorszyła, gdyby do kontekstu zostały dobrane nieodpowiednie dokumenty, które co prawda dotyczyłyby dużych modeli językowych ale nie zawierały wystarczającej ilości informacji potrzebnych do zbudowania sensownej odpowiedzi.
Wspomniane dopasowanie "dużych modeli językowych" do "definicji GPT-4" moglibyśmy zaadresować z pomocą "Similarity Search" realizowanego przez bazy wektorowe. Obrazuje to poniższy przykład w którym poprawnie został odszukany dokument powiązany z zapytaniem, nawet pomimo faktu, że nie występują w nim konkretne słowa kluczowe nawiązujące do zapytania.
Wydaje się, że taki rodzaj wyszukiwania adresuje wszystkie możliwe problemy. I chociaż bazy wektorowe stanowią istotny element systemów RAG, to w praktyce potrzebujemy czegoś więcej, co potrafię zobrazować banalnym przykładem.
- Wprowadzenie
- Definicja GPT-4 cz.1
- Definicja GPT-4 cz.2
- Definicja GPT-4 cz.3
- ...
- Makro 10
Wiesz już, że praca z dużymi zestawami danych wymaga podzielenia ich na mniejsze części. Jeżeli wspomniana "Definicja GPT-4" byłaby długim fragmentem, który musiałby zostać podzielony, to potrzebowalibyśmy mechanizmu pozwalającego na poprawne odnalezienie wszystkich części i wykorzystanie ich przy generowaniu odpowiedzi. Oczywiście można to zrobić, np. zapisując źródło pochodzenia wybranych fragmentów oraz ich kolejność, co przydaje się także na potrzeby aktualizacji ich zawartości.
Zakładając, że wdrożyliśmy już działający system potrafiący odzyskiwać informacje dokładnie w taki sposób, to wówczas mierzymy się z kolejnym problemem wynikającym z faktu, że generowanie odpowiedzi na podstawie wielu dokumentów zajmuje czas oraz generuje dodatkowe koszty. Jeśli pytanie wymagające takiej operacji padałoby odpowiednio często, nasz rachunek OpenAI szybko osiągnąłby spore wartości.
Pomimo tego, że przeszliśmy przez kilka wyzwań związanych z budowaniem RAG, to ich lista jest jeszcze bardzo długa. Aby nie rozciągać treści tego wpisu, wymienię od podpunktów te na które warto zwrócić szczególną uwagę. Niewykluczone, że poniższe informacje oszczędzą Ci sporo czasu i być może także pieniędzy.
- Kontekst dobierany do udzielenia odpowiedzi nie może obejmować wyłącznie ostatniej wiadomości użytkownika. Zwykle znajdujemy się w kontekście czatu i użytkownik może najpierw zapytać o jakąś definicję a potem poprosić o rozszerzenie jednego z punktów wymienionych w odpowiedzi. Wówczas jego pytanie brzmi "powiedz mi więcej na temat #3" i samo w sobie nie zawiera podstawowej informacji o tym, co dokładnie ma na myśli przez "#3"
- Udostępnienie aplikacji użytkownikom, szczególnie w formie okna czatu sprawia, że możesz spodziewać się praktycznie dowolnych zapytań na które Twój system musi odpowiednio reagować. Poza nieprecyzyjnymi wiadomościami z ich strony musisz brać pod uwagę także potencjalnie niebezpieczne próby naginania zasad działania Twojej aplikacji
- Możliwość rozmowy z bazą danych wymaga w pierwszej kolejności zgromadzenia wszystkich treści w jedno miejsce lub przynajmniej zbudowanie systemu zapewniającego dostęp do najbardziej aktualnych wersji. Pomyśl o tym już na początku planowania RAG, ponieważ brak możliwości aktualizowania danych bardzo ogranicza jego praktyczne zastosowanie
- Niekiedy gromadzone treści będą zawierały dodatkowe informacje, które są istotne z punktu widzenia człowieka, jednak nie mają znaczenia z punktu widzenia modelu. Dlatego należy zadbać o to aby oczyścić dane z niepotrzebnych elementów
- Nie każdy format danych umożliwia łatwy dostęp do zawartości dokumentu. W przypadku plików docx czy PDF samo odczytanie treści niekiedy jest wprost niemożliwe lub bardzo skomplikowane. Znacznie lepiej sprawdzają się otwarte formaty, takie jak .csv, .txt czy .md. Podobnie też całkiem dobrze wypadają narzędzia takie jak Notion oferujące stosunkowo łatwy dostęp do zapisanych w nich treści poprzez API
- Poza jakością odpowiedzi obecnie istotne są także koszty związane z generowaniem odpowiedzi oraz wszystkie wątki związane z ogólnymi doświadczeniami użytkownika (UX). Praktycznie zawsze RAG wymaga zrealizowania złożonej logiki budującej kontekst, co sprawia, że optymalizacja jest istotna praktycznie od samego początku. Greg Brockman z OpenAI moim zdaniem słusznie twierdzi, że przy pracy z LLM perfekcjonizm jest wprost oczekiwany
- W produkcyjnej aplikacji musisz brać pod uwagę nie tylko dane wprowadzane przez użytkownika, ale również jakość odpowiedzi generowanych przez system. Dobrym pomysłem jest dołączanie źródeł na podstawie których została udzielona odpowiedź oraz załączenie dodatkowych materiałów (np. linków czy zdjęć)
- W przypadku zaawansowanych systemów RAG pracujących na różnych zestawach danych, które częściowo mogą się pokrywać, konieczne jest także uwzględnienie wzbogacania oraz identyfikowania zapytania użytkownika. Obie operacje mogą być wykonane przez dodatkowe zapytanie do modelu, a ich rezultat pomaga w zawężeniu obszaru przeszukiwanej bazy i tym samym zwiększeniu szansy na znalezienie istotnych fragmentów
- Niektóre fragmenty dokumentów wczytywanych do dynamicznego kontekstu mogą wprowadzać model w błąd, np. gdy ich treść zawiera instrukcję przypominającą polecenie, które ma zostać zrealizowane. Dlatego szczególnie istotnym elementem działania systemu RAG są odpowiednio zaprojektowane prompty, sterujące zachowaniem modelu także w takich sytuacjach
- Działający system RAG nawet przy sporej optymalizacji będzie wymagać przetwarzania dużych zestawów danych. Dlatego warto (tam gdzie to możliwe) skorzystać z tańszych wersji modeli (np. GPT-3.5-Turbo lub alternatywy OpenSource). Dobrą praktyką jest także domyślne stosowanie języka angielskiego, aczkolwiek z oczywistych względów nie zawsze jest to możliwe. Warto jednak mieć na uwadze fakt, że odpowiedzi anglojęzyczne generowane są szybciej i nawet większa ilość tekstu generuje mniejsze koszty niż w przypadku innych języków
- Poza doprowadzeniem do działania systemu, należy także uwzględnić jego rozwój i modyfikacje. Po stronie kodu mamy do dyspozycji wiele narzędzi, które nam to umożliwiają. Natomiast po stronie LLM i np. optymalizacji promptów, obecnie brakuje nam sensownych narzędzi oraz technik, które zminimalizują ryzyko uszkodzenia działających elementów systemu przez nowo wprowadzone zmiany.
To tyle (na teraz). Każdy z powyższych punktów zasługuje na rozwinięcie. Jednocześnie każdy z nich można zaadresować, aczkolwiek nie zawsze będzie to konieczne. Niekiedy też wyda się wprost niemożliwe zaadresowanie niektórych aspektów, aczkolwiek w wielu przypadkach skuteczne mogą okazać się najprostsze rozwiązania.
Pomimo tego, że obecnie jesteśmy jeszcze na wczesnym etapie rozwoju narzędzi ułatwiających pracę z LLM (np. GPT-4) oraz techniki projektowania promptów są jeszcze na dość wczesnym etapie rozwoju, to już teraz z powodzeniem możemy projektować systemy działające na nasze potrzeby lub jako wsparcie wybranych elementów procesów biznesowych.
Z całą pewnością niejednokrotnie będziemy jeszcze wracać do tematu RAG oraz technik rozszerzania bazowych możliwości dużych modeli językowych. Tymczasem pozostaje mi mieć nadzieję, że powyższe informacje były dla Ciebie wartościowe i że przynajmniej część z nich wykorzystasz w praktyce.
- Rozmowa z własną bazą przez LLM
- Czym jest RAG?
- Największe wyzwania i sugestie
- Narzędzia LangChain.js