Projektowanie pipeline’u opartego na RAG: dzielenie tekstu, filtrowanie i przesyłanie danych w strumieniu.
Przewodnik po zasadach budowy opartej na RAG platformy: dzielenie tekstu z uwzględnieniem struktury, bezpieczne dzielenie na tokeny, trójetapowe filtrowanie wyników wyszukiwania, ponowne łączenie fragmentów, projektowanie zapytań i przesyłanie danych w strumieniu.
Modele językowe uniwersalne potrafią doskonale rozumować, pisać i tłumaczyć, dopóki nie zadasz pytania o system, którego nigdy wcześniej nie widziały: twoją wewnętrzną dokumentację, procesy pracy w Twojej dziedzinie, dokładne zachowanie aplikacji korporacyjnej, którą Twoja zespół konfiguruje codziennie. Takie informacje nie znajdują się w danych treningowych, a żaden trik związany z promptem nie może ich tam dodać. Co gorsza, model rzadko przyznaje się do braku wiedzy; zamiast tego przedstawia płynną i pewną siebie odpowiedź. Poniżej przedstawiono podstawy systemu generacji wzbogaconej o wyszukiwanie danych (RAG): jak przygotowuje się dokumenty, jak tworzy się fragmenty tekstu, jak filtrowane są kandydatki na odpowiedzi, jak kształtuje się prompt oraz w jaki sposób odpowiedzi trafiają do użytkownika, wraz z uzasadnieniem każdej decyzji, abyś mógł zastosować to rozwiązanie we własnym projekcie.
Dlaczego dodatkowe dane zmieniają zadanie modelu
Zasada działania RAG jest prosta do wyjaśnienia. Zamiast polegać na modelu, który ma zapamiętać odpowiedź, podaje mu się tę odpowiedź do przeczytania. Prawdziwa dokumentacja jest podzielona na fragmenty nadające się do wyszukiwania; gdy pojawia się pytanie, system znajduje fragmenty, które najprawdopodobniej na nie odpowiadają, i umieszcza je w kontekście modelu. Zadanie zmienia się z „przypomnij sobie ten fakt” na „przeczytaj ten fragment i dobrze go wyjaśnij”, co jest dokładnie tym rodzajem pracy, który modele językowe potrafią wykonywać niezawodnie.
Koncepcja jest prosta, ale jej prawidłowe funkcjonowanie nie jest. Pobieranie odpowiednich elementów, utrzymywanie ich spójności oraz szybkie odpowiadanie w taki sposób, by nikt nie wpatrywał się w ikonę ładowania, wymaga decyzji, które łatwo jest niedocenić. Opisany tutaj system stanowi celowo etap podstawowy: najpierw pobranie informacji, a dopiero potem odpowiedź. Bardziej ambitne rozszerzenia, takie jak grafy wiedzy czy agent używający narzędzi, opierają się dokładnie na tych elementach i mogą działać tylko wtedy, gdy podstawa jest solidna.
Markdown jako źródło prawdy
Cała wiedza, na którą odpowiada system, znajduje się w plikach Markdown. Ten wybór jest praktyczny, a nie estetyczny. Markdown zawiera wystarczającą strukturę, by zachować znaczenie, w tym nagłówki, hierarchię, listy i tabele, a jednocześnie pozostaje zwykłym tekstem, który można dzielić na fragmenty i włączać bez konieczności korzystania z bardziej skomplikowanych formatów oznaczeń.
Pliki są tworzone w sposób, w jaki ludzie naturalnie piszą dokumentację techniczną: nagłówek dla każdego tematu, podnagłówki dla szczegółów poniżej, tabele tam, gdzie ważna jest struktura, oraz zrzuty ekranu tam, gdzie obraz wyjaśnia sprawę szybciej niż tekst. Żaden element stylu pisania nie jest dostosowywany pod wymogi procesu przetwarzania. To ważne, ponieważ dokumentacja, która musi być tworzona specjalnie dla systemów wyszukiwania, często w ogóle nie powstaje.
Zrzuty ekranu wymagają odrębnej decyzji projektowej. W dokumentacji aplikacji nie służą jedynie jako dekoracja – duża część pytań typu „jak to zrobić” dotyczy właśnie tego, który przycisk lub menu należy użyć, a sam tekst słabo sobie z tym radzi.
Zamiast wstawiać obrazy bezpośrednio do plików dokumentacji, obrazy są przechowywane oddzielnie, a każdy plik Markdown odwołuje się do nich za pomocą adresu URL. Link jest zwykłym tekstem, więc przechodzi przez procesy dzielenia na fragmenty, wstawiania i pobierania tak jak każdy inny tekst. Gdy fragment zawierający taki link trafia do odpowiedzi, interfejs użytkownika odczytuje adres URL i ładuje obraz w momencie renderowania. Użytkownik widzi dokumentację taką, jak została napisana, włączając obrazy, podczas gdy cały proces przetwarzania i pobierania w ogóle nie obejmuje danych obrazowych.
Tę zasadę warto nazwać: utrzymuj treść w formie tekstu na każdym etapie, który wymaga tylko tekstu, a bardziej złożone media przetwarzaj w ostatnim możliwym momencie, gdy mają zostać pokazane osobie.
Strumieniowanie odpowiedzi w momencie jej generowania
To samo podejście „ostatniego kroku” odnosi się do dostarczania odpowiedzi. Gdy tylko rozpoczyna się generowanie, nie ma powodu, by zmuszać użytkownika do czekania na pełną odpowiedź. Tokeny są przesyłane z powrotem przez trwałe połączenie w miarę ich tworzenia przez model, więc odpowiedź powstaje krok po kroku, podobnie jak gdy ktoś pisze wyjaśnienie. W połączeniu z obrazami, które ładowane są asynchronicznie w miarę pojawiania się ich adresów URL, doświadczenie przypomina rozmowę, mimo że w rzeczywistości jest to głównie zapytanie do bazy danych, po którym następuje generowanie.
Hierarchiczne dzielenie na fragmenty: szanowanie struktury dokumentu
Zanim cokolwiek będzie mogło zostać wyszukane, dokument musi zostać podzielony na jednostki, które można niezależnie włączyć i pobrać. To właśnie na tym etapie wiele systemów RAG po cichu omija pewne zasady, a konsekwencje tego są łatwe do przeoczenia.
Dlaczego dzielenie na fragmenty o stałej wielkości działa nieskutecznie
Podejście naiwne jest mechaniczne: wybiera się określoną liczbę tokenów, dzieli dokument na fragmenty o mniej więcej takiej wielkości i kontynuuje. Jest szybkie w implementacji, ale błędne w taki sposób, że nie powoduje żadnego błędu. Okno o stałej wielkości nie rozróżnia granic zdań, a tym bardziej granic koncepcyjnych. Bez problemu dzieli numerowaną procedurę na pół, oddziela tabelę od nagłówka, który nadaje jej sens, lub pozostawia definicję w jednym fragmencie, podczas gdy przykład trafia do innego.
Takie fragmenty mają właściwą długość, ale niewłaściwy kształt, a jakość wydobywania informacji zależy od tego kształtu. Jeśli wydobyty tekst nie zawiera kompletnego myślenia, żadne staranne instrukcje późniejszego etapu nie mogą tego naprawić. Model rozumuje na podstawie fragmentu, a odpowiedź odzwierciedla to.
Zamiast tego – czytanie drzewa nagłówków
Hierarchiczne dzielenie na fragmenty opiera się na założeniu, że dokumentacja już posiada strukturę, a ta struktura stanowi atut. Dobrze napisane dokumenty techniczne tworzą drzewo nagłówków: główne sekcje, podsekcje umieszczone poniżej nich oraz tekst główny dołączony na każdym poziomie. Zamiast ignorować to drzewo, narzędzie do dzielenia na fragmenty przemierza je:
- Każda główna sekcja staje się oddzielnym fragmentem.
- Każda podsekcja również staje się oddzielnym fragmentem, ale z informacją o swojej sekcji nadrzędnej.
- Jeśli podsekcja jest na tyle krótka, że lepiej czyta się ją razem z sekcją nadrzędną, obie łączą się w jeden wspólny fragment zamiast tworzyć mały, pozbawiony kontekstu fragment.
- Treść, która nie należy do żadnego nagłówka, tak jak wprowadzenia, pojedyncze akapity lub notatki, jest nadal rejestrowana jako odrębny typ fragmentu, zamiast być pominięta lub przymusowo włączona do sąsiedniego fragmentu.
Metryki umożliwiające późniejsze etapy
Każdy fragment zawiera coś więcej niż tylko tekst:
- Szlak nawigacyjny: łańcuch nagłówków przodków. Fragment dotyczący pojedynczej opcji konfiguracji nadal wie, że należy do szerszego procesu pracy, nawet gdy jest pobierany samodzielnie.
- Etykieta typu: odróżniająca sekcje najwyższego poziomu, zagłębione szczegóły, połączone fragmenty rodzica i dziecka oraz osobne notatki.
- Stabilny identyfikator: łączący fragment z jego dokładnym źródłem w pliku źródłowym.
Ta metryka nie jest tylko dekoracją. To ona umożliwia późniejsze przegrupowanie, ponowne połączenie rozdzielonych sekcji oraz spójne, źródłowo udokumentowane odpowiedzi. Prosta lista równie dużych fragmentów tekstu nie może tego zapewnić; fragment, który zna swoje miejsce w strukturze drzewa, może.
Podstawowym kompromisem jest przestrzeganie struktury już istniejącej w dokumencie zamiast nakładania na nią dowolnej innej. Przynosi to szybkie korzyści: fragmenty te wyglądają jak kompletnymi myślami, ponieważ rzeczywiście nimi są, ustrukturyzowane w sposób zgodny z tym, jak to zrobił autor dokumentacji.
Adaptacyjna druga faza obliczeń dotycząca limitu tokenów
Hierarchiczne dzielenie na fragmenty zapewnia odpowiedni kształt, ale nie bierze pod uwagę istniejącego twardego ograniczenia: model embedding przyjmuje tylko ograniczoną liczbę tokenów na dane wejściowe. Struktura dokumentu nie uwzględnia tego limitu. Długa lista pytań i odpowiedzi, rozległa tabela referencyjna lub podsekcja, która po prostu jest zbyt długa, może stanowić doskonale spójny fragment, a mimo to przekroczyć limit przyjęty przez model.
To awarie jest niebezpiecznie cicha. W zależności od klienta, zbyt duży fragment jest albo odrzucany, albo w tajemnicy skracany przed włączeniem, a w obu przypadkach treść znika bez żadnej reakcji podczas przetwarzania. Skracanie jest bardziej podstępnym skutkiem, ponieważ fragment nadal istnieje i jest pobierany; jego wektor po prostu nie odzwierciedla już tekstu, za który go uważamy.
Rozwiązaniem jest druga faza przetwarzania po hierarchicznej. Każdy fragment jest sprawdzany pod kątem progu bezpieczeństwa ustawionego znacznie poniżej rzeczywistej maksymalnej wartości modelu, dzięki czemu istnieje bufor zamiast ostrej granicy. Liczba tokenów różni się w zależności od narzędzi do ich dzielenia, a margines pokrywa tę niepewność. Fragmenty poniżej progu przechodzą bez zmian. Te powyżej są dalej dzielone, a każdy powstały kawałek jest:
- wyraźnie oznaczony jako część większej całości, aby późniejsze etapy mogły znaleźć jego „braci”
To, gdzie dokładnie przeprowadzić cięcie i dlaczego proste dzielenie nie jest wystarczające nawet w tym przypadku, stanowi odrębny, ważny temat projektowy. Podstawowym założeniem jest to, że hierarchiczna faza gwarantuje odpowiedni kształt, a faza adaptacyjna – dopasowanie, dzięki czemu dobry kształt nigdy nie kosztuje nas treści.
Zachowywanie wektorów wraz z ich metadanymi
Zawarte fragmenty wymagają struktury zaprojektowanej wokół jednego pytania: które z tych licznych wektorów są najbliższe temu nowemu, przy czym odpowiedź musi być szybka i możliwa do przeprowadzenia na dużą skalę. Bazy danych relacyjnych nie są stworzone wokół takiego zapytania, dlatego do tego zadania nadaje się specjalnie zaprojektowany magazyn wektorów.
Baza danych działa w kontenerze, zamiast być instalowana bezpośrednio na serwerze host. Powody są praktyczne: instancja w kontenerze łatwo się uruchamia, można ją usunąć lub przenieść na inny komputer, a także nie wnosi żadnych zależności do systemu hosta.
Oprócz każdego wektora, magazyn przechowuje kompletny zestaw metadanych zawierający tekst fragmentu, ścieżkę nawigacyjną, typ oraz identyfikator źródła. Dzięki temu każdy wynik podobieństwa przychodzi wraz ze wszystkimi niezbędnymi informacjami, aby można go było natychmiast wykorzystać, zamiast jako anonimowy wektor. Połączenie szybkiego wyszukiwania podobieństw z bogatymi metadanami dołączonymi do każdego wyniku umożliwia filtrowanie, ponowne sortowanie oraz łączenie wyników bez konieczności dodatkowych poszukiwań w innych źródłach danych.
Życiowy cykl żądania: od pytania do odpowiedzi w formie strumienia
Tutaj system wykonywa swoją prawdziwą pracę, dlatego warto ją opisać na tyle dokładnie, aby logikę można było ponownie wykorzystać.
Rozdzielenie przesyłania danych od transmisji strumieniowej
API udostępnia dwa punkty końcowe pełniące różne funkcje. Pierwszy otrzymuje pytanie i natychmiast zwraca identyfikator rozmowy. Drugi to punkt końcowy do transmisji strumieniowej, do którego frontend łączy się za pomocą tego identyfikatora. Przyjmowanie pytania i dostarczanie odpowiedzi to odrębne zadania, a ich rozdzielenie umożliwia stopniowe przesyłanie odpowiedzi, zamiast utrzymywania otwartej pojedynczej prośby aż do przygotowania monolitycznej odpowiedzi. Dzięki temu klient ma również prosty sposób na ponowne połączenie lub powiązanie logów konkretnej rozmowy.
Pomiędzy tymi dwoma punktami końcowymi kolejno wykonywane są następujące kroki.
Krok 1: włączenie pytania za pomocą modelu przetwarzania danych
Tekst użytkownika jest embedowany za pomocą dokładnie tego samego modelu, który był używany podczas jego przetwarzania. To nie jest szczegół, którego można zmienić. Porównania podobieństwa mają sens tylko wtedy, gdy wektor zapytania i przechowywane wektory znajdują się w tym samym przestrzeni wektorowej. Jeśli model embedowania kiedykolwiek się zmieni, każdy przechowywany fragment musi zostać ponownie embedowany, ponieważ wektory z dwóch różnych modeli nie są porównywalne, nawet jeśli opisują identyczny tekst.
Krok 2: szerokie poszukiwania
Wektor zapytania jest porównywany z wektorami przechowywanych fragmentów, a powraca około dwunastu najbliższych wyników. Miarą jest podobieństwo kosinowe, które w istocie mierzy kąt pomiędzy dwoma wektorami – im mniejszy kąt, tym większe podobieństwo znaczeń. Ten etap jest celowo szeroki; jego celem jest zapewnienie pełnego przywrócenia wszystkich istotnych informacji, zanim rozpocznie się faktyczna selekcja.
Krok 3: filtrowanie kandydatów poprzez trzy etapy
Następnie działają sekwencyjnie trzy filtry, aby zredukować tę dziesiątkę kandydatów do tych nielicznych, które są istotne:
- Progiem podobieństwa. Usuwane są kandydaci o wyniku poniżej minimalnego poziomu. Są to fragmenty, które znalazły się na liście tylko dlatego, że nie istniało nic lepszego, a ich zachowanie doprowadziłoby do dodania szumu.
- Etap ponownego rankowania. Przetrwali otrzymują nowe oceny od modelu, który działa inaczej niż podczas pierwszego wyszukiwania. Zamiast osobno embedować pytanie i każdy fragment oraz porównywać wektory, model bierze pytanie wraz z jednym fragmentem jako wspólny wpis i ocenia, czy dany fragment rzeczywiście odpowiada na pytanie. Dzięki temu eliminowane są fałszywie pozytywne wyniki: fragmenty, które w przestrzeni wektorowej znajdują się blisko zapytania, ale po przeczytaniu obok niego okazują się nie zawierać odpowiedzi.
Każda z tych mechanizmów ma jedną funkcję: pierwsza chroni wyniki przed szumem, druga zapewnia precyzję, a trzecia narzuca sztywny limit na ilość kontekstu docierającego do modelu. To, co pozostaje, to najlepszy dostępny materiał, a nie tylko elementy z góry listy.
Powodem takiego uporządkowania jest koszt. Mechanizm ponownego rankowania oparty na czytaniu par jest znacznie dokładniejszy od porównania wektorowego, ale też o wiele droższy, ponieważ musi przetwarzać każdą parę pytań i fragmentów tekstowych łącznie. Uruchomienie go na tuzinie wcześniej przefiltrowanych kandydatów zamiast na całym korpusie pozwala uzyskać odpowiednią precyzję bez ponoszenia pełnych kosztów.
Krok 4: przywrócenie podzielonych sekcji
Część zachowanych fragmentów to jedynie części sekcji, które proces adaptacyjny musiał podzielić. Jeśli model otrzyma na przykład drugą z trzech części bez żadnej wskazówki o istnieniu pozostałych, jego odpowiedź opiera się na niepełnych informacjach. Dlatego przed skomponowaniem zapytania sprawdza się każdy zachowany fragment pod kątem istnienia jego „braci” – pozostałych części tej samej oryginalnej sekcji. Gdy takie istnieją, są pobierane i ułożone w odpowiedniej kolejności, z oznaczeniem ich pozycji. Model czyta wtedy całą sekcję, a nie tylko jej fragment. Właśnie tutaj przydają się etykiety części oraz stabilne identyfikatory dodane podczas przetwarzania danych.
Krok 5: skomponowanie zapytania
Wszystkie pobraane fragmenty wraz z przywróconymi elementami tworzą spójny kontekst. Pytanie użytkownika jest przesyłane razem z nimi, a instrukcja systemowa, opisana w następnej sekcji, mówi modelowi, jaką rolę ma pełnić i jak korzystać z tych danych.
Krok 6: przesyłanie odpowiedzi strumieniowo
Wygenerowany tekst jest przesyłany z powrotem przez endpoint strumieniowy, do którego połączył się klient, w małych partiach zamiast jednej długiej odpowiedzi. Użytkownik może obserwować powstawanie odpowiedzi w czasie rzeczywistym.
Podsumowując: wektoruj zapytanie, przeszukaj szeroko, przefiltruj przez trzy etapy, uzupełnij brakujące części, podaj instrukcje modelowi i przesyłaj jego wyniki strumieniowo. Żaden z tych etapów nie jest wyjątkowy – jakość wynika ze sprawnego przekazywania danych pomiędzy etapami oraz od przyznania każdemu z nich dokładnie jednej responsywności.
Projektowanie instrukcji: format, model i ton
Gdy prompt zostanie przygotowany, trudne problemy z odzyskiwaniem informacji są już rozwiązane: znaleziono odpowiednie fragmenty, przefiltrowano je i ponownie połączono. To, co pozostaje, jest równie podatne na błędy: trzeba przedstawić materiał w taki sposób, aby model dostarczył dobre odpowiedzi, a nie tylko takie, które są technicznie poprawne.
Pisanie promptu w Markdown
Sam prompt jest napisany w formacie Markdown. Powód jest ponownie praktyczny – modele widziały ogromne ilości tekstu w formacie Markdown w dokumentacji, plikach README oraz tekstach technicznych, więc instrukcje w znajomym formacie są interpretowane bardziej niezawodnie niż te o specjalnej strukturze, którą model musi rozszyfrować. Nagłówki dzielą sekcje promptu, uzyskany kontekst jest wyraźnie oddzielony od otaczających go instrukcji, a każdy treść złożona z połączonych fragmentów ma widoczną oznaczkę, dzięki czemu model może odróżnić „jedną spójną ideę złożoną z elementów” od zwykłego uzyskanego tekstu.
Wybór mniejszego, szybszego modelu do syntezy
Model, który tworzy ostateczną odpowiedź, jest mniejszy i szybszy, a nie największy dostępny. Może to wydawać się sprzeczne, dopóki nie przyjrzymy się zadaniu, które faktycznie wykonuje. Nie ma on za zadanie niczego wytwarzać od zera, pamiętać rzadkich szczegółów ani uzupełniać brakującej wiedzy; ciężka praca została już wykonana wcześniej, podczas wyszukiwania i filtrowania. Jego pozostałym zadaniem jest przyjęcie starannie wybranego, uporządkowanego kontekstu i przekształcenie go w jasną, szybką odpowiedź w odpowiednim tonie.
To jest zadanie syntezy. Gdy kontekst jest już dostępny, zwięzły model daje odpowiedzi o podobnej jakości do znacznie większego modelu, przy czym reaguje szybciej i kosztuje o wiele mniej. Zdolność rozumowania większego modelu jest warta dopłaty, gdy musi on coś wyjaśnić lub rozwiązać. Ma znacznie mniejszą wartość, gdy odpowiedź jest już dostępna, a zadaniem jest jej właściwe wyjaśnienie. Ten kompromis zależy od jakości pozyskiwania informacji: im słabszy jest kontekst, tym ważniejsza staje się zdolność większego modelu do radzenia sobie z niejednoznacznościami, co jest kolejnym powodem do inwestycji w etapy filtrowania.
Anatomia celowego promptu
Prompt systemowy ma ustaloną strukturę, a nie jest improwizowany:
- Rola. Zaczyna się od określenia, jaki rodzaj asystenta jest modelem i w jakiej dziedzinie działa, dzięki czemu ton i założenia są ustalane już od pierwszego wiersza.
Nic nie jest pozostawione do uznania modelu co do tego, jak powinien brzmieć pomocny asystent. Każde zachowanie jest dokładnie opisane, podobnie jak przy wprowadzaniu nowego członka zespołu poprzez wyjaśnienie norm komunikacyjnych zespołu przed przydzieleniem jakichkolwiek zadań.
Korzyścią jest asystent, który odpowiada z pewnością siebie, gdy ma na to podstawy, szczerze przyznaje, gdy ich nie ma, oraz zachowuje spójny ton we wszystkich rozmowach. Ta spójność nie pochodzi od modelu – pochodzi od zadań, które mu są podawane.
Kierowanie według intencji: regularne fragmenty i fragmenty ekranowe
Zapytania, które wyglądają podobnie, mogą dotyczyć zupełnie różnych rzeczy. Pytanie takie jak „Jak obliczane są ceny w tym procesie?” ma charakter koncepcyjny i wymaga wyjaśnienia. Innego typu jest pytanie „Które pole na tym ekranie służy do wprowadzania ceny?”, które ma charakter nawigacyjny i wymaga wskazania konkretnego pola, przycisku lub obszaru interfejsu. Obie kategorie zapytań mogą odnosić się do tych samych fragmentów dokumentacji, jednak idealne odpowiedzi na nie prawie w niczym się nie pokrywają. Traktowanie ich jednakowo skutkuje przedstawieniem długiego wykładu koncepcyjnego osobie, która potrzebowała jedynie informacji o lokalizacji, albo, co gorsza, opisem abstrakcyjnego elementu interfejsu, podczas gdy użytkownik potrzebował dokładnej lokalizacji tego elementu.
Rozdzielanie wiedzy przy przetwarzaniu informacji
Różnica ta jest wprowadzana na poziomie fragmentów, a nie tylko w momencie odpowiadania. Obok zwykłych fragmentów dokumentacji prozatorskiej istnieje druga kategoria zawierająca treści opisujące ekranы i ich pola: jak nazywa się każde pole, co robi oraz gdzie znajduje się w stosunku do innych pól na tym samym ekranie. Fragmenty te nie są oznaczane później – klasyfikacja następuje już podczas przetwarzania treści, dzięki czemu w momencie pojawienia się pytania istnieją dwa wyraźnie oddzielone zbiory wiedzy, a nie jeden nierozróżniony zbiór.
Wybór sposobu odpowiadania w momencie zapytania
Gdy pojawia się pytanie, ocenia się, o jaki rodzaj fragmentu faktycznie chodzi: dokumentację koncepcyjną czy szczegóły konkretnego ekranu i jego pól. Ta klasyfikacja wpływa na to, które fragmenty są priorytetowe podczas wyszukiwania oraz jakie pytanie o odpowiedź otrzymuje model:
- Pytanie skierowane na ekran otrzymuje instrukcję dostosowaną do precyzyjnego opisania elementów interfejsu: dosłowną, konkretną i skupioną dokładnie na tym, gdzie i co znajduje się.
- Pytanie konceptualne otrzymuje instrukcję dostosowaną do wyjaśnienia: szerszą i bardziej skłonną do łączenia pomysłów.
Proces wydobywania informacji oraz model generujący są takie same w obu przypadkach; zmienia się jedynie sposób odpowiadania, wybierany zgodnie z potrzebami pytania, zamiast przymuszania każdej odpowiedzi do pasowania do jednego ogólnego szablonu.
To ma większe znaczenie, niż się wydaje. Asystent o jednym stylu odpowiadania brzmi ogólnikowo, bez względu na jakość jego wyszukiwania informacji. Rozpoznawanie intencji, a nie tylko tematu, sprawia, że asystent wydaje się zwracać uwagę na to, o co faktycznie zostało zapytane. Jeśli przyjmiesz tę metodę, miej plan awaryjny na pytania niejednoznaczne – na przykład używaj podejścia koncepcyjnego i uwzględniaj wszystkie mocno pasujące fragmenty tekstu, aby błędna klasyfikacja skutkowała łagodnym pogorszeniem jakości odpowiedzi, a nie błędnym stylem odpowiadania.
Kontrola pamięci
Ostrożne pobieranie i generowanie danych są bezużyteczne, jeśli usługa stopniowo wyczerpuje swoją pamięć. Proces, który wstawia tekst, utrzymuje fragmenty danych w pamięci, komponuje zapytania i przekazuje wyniki, powtarzając to wielokrotnie, a czasem jednocześnie, cicho gromadzi obciążenie pamięci, chyba że coś aktywnie nim zarządza. Obiekty potrzebne tylko do jednego zapytania mają tendencję do pozostawania w pamięci dłużej, niż powinny, gdy nikt ich nie usuwa.
Dlatego sprzątanie pamięci nie jest pozostawione przypadkowi. W naturalnych punktach końca, takich jak zakończenie zapytania lub ukończenie partii danych, pamięć jest wyraźnie zwalniana, zamiast polegać na tym, że sama się w końcu opróżni. W praktyce oznacza to usuwanie odniesień do dużych obiektów pośrednich, czyszczenie pamięci tymczasowej na każde zapytanie oraz, w przypadku modeli hostowanych lokalnie, zwalnianie całej pamięci, którą trzyma silnik wykonawczy.
To praca mało atrakcyjna, która nigdy nie pojawia się w demonstracji i jest zauważana tylko wtedy, gdy jej brakuje: usługa, której wydajność pogarsza się w miarę dłuższego czasu działania, zamiast odpowiadać na tysięczne zapytanie tak szybko jak na pierwsze. Prawidłowe jej działanie zależy mniej od pomysłowej inżynierii, a bardziej od dyscypliny – traktowania pamięci jako czegoś, co zarządza każdy etap, a nie jako czegoś, co na pewno załatwi system operacyjny. Obserwowanie zużycia pamięci podczas długiego testu obciążeniowego, a nie tylko krótkiego benchmarku, to najprostszy sposób na potwierdzenie skuteczności tej dyscypliny.
Główne wnioski
Fundament opisany tutaj to kompletny, solidny pipeline RAG: dzielenie na fragmenty z uwzględnieniem struktury, bezpieczne embedowanie tokenów, wieloetapowe filtrowanie, ponowne łączenie rozdzielonych części, precyzyjne instrukcje, które zmuszają model do szczerości co do tego, co wie, oraz dostarczanie wyników w czasie rzeczywistym. Decyzje o największym znaczeniu to te:
- Zachowuj dokumenty w uporządkowanym formacie tekstu prostego i przetwarzaj obrazy oraz inne media wyłącznie w momencie renderowania.
- Dziel treść według drzewa nagłówków i do każdej części dodawaj ślady nawigacyjne, etykiety typu oraz stabilne identyfikatory.
- Dodaj drugą fazę przetwarzania, która egzekwuje limit tokenów modelu embedding z marginesem bezpieczeństwa, oznaczona części oraz niewielkie nakładanie się fragmentów.
- Zawsze używaj tych samych modeli do embedowania zapytań, co przy ich wprowadzaniu, i ponownie je embeduj, gdy te modele ulegną zmianie.
- Pierwotnie pobieraj dużą ilość danych, a następnie filtrowaj je za pomocą progu podobieństwa, ponownego rankowania metodą czytania par oraz ścisłego końcowego ograniczenia.
- Zgromadź ponownie rozdzielone sekcje przed wysłaniem zapytania do modelu, aby ten nigdy nie analizował nieoznaczonego fragmentu.
- Pozwól mniejszemu, szybszemu modelowi wykonać syntezę po tym, jak proces pobierania danych już wykona ciężką pracę, i jasno określ jego rolę, zadanie, sposób radzenia sobie z lukami oraz ton.
Każda z tych opcji sama w sobie jest skromna. Razem tworzą one system, którego odpowiedzi są uzasadnione, spójne i szybkie, a także dostarczają solidnej bazy pod bardziej zaawansowane techniki wyszukiwania w przyszłości.
Literatura pokrewna
- Beyond Top-K: Relevance Thresholds, Hybrid Search and Reranking in RAG — Dowiedz się, dlaczego baza wektorowa w połączeniu z LLM nie stanowi gotowego systemu RAG, oraz jak dzielenie na fragmenty, progi podobieństwa, wyszukiwanie hybrydowe, ponowne sortowanie i ocena mogą zamknąć tę luki.
- Dlaczego production RAG przechodzi poza dedykowane bazy danych wektorowych — Analizuje, dlaczego samodzielne bazy danych wektorowych tracą na znaczeniu w systemach production RAG oraz jak hybrydowe metody wyszukiwania, filtrowania i zintegrowane warstwy danych przekształcają tę architekturę.
- Mapowanie RAG: etapy pipeline, kluczowe składniki i rodzaje wariantów — Wyjaśnia, jak działa end-to-end proces generacji wzbogaconej o wyniki wyszukiwania, jakie składniki są potrzebne w systemie RAG oraz jak liczne warianty RAG pasują do jednej struktury.