Strona główna / Artykuły / Projektowanie pipeline’u opartego na RAG: dzielenie tekstu, filtrowanie i przesyłanie danych w strumieniu.

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.

3895 słów

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”
  • Ze względu na niewielkie nakładanie się tekstu z sąsiednim fragmentem, nikt go czytający – ani człowiek, ani model – nie napotyka nagłego, pozbawionego kontekstu cięcia w środku myśli
  • 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:

    1. 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.
    2. 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.
  • Surowy dolny próg i sztywny górny limit. Po ponownym rankowaniu znacznie bardziej restrykcyjny próg odfiltrowuje to, co pozostało, a to, co przeżyje, jest ograniczone małym, stałym maksimum.
  • 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.
  • Zadanie. Jasno określa ono to, co należy zrobić: przeczytać uzyskany kontekst i odpowiedzieć wyłącznie na jego podstawie, bez odwoływania się do wiedzy ogólnej czy domysłów, nawet jeśli odpowiedź wydaje się oczywista.
  • Pewność i braki. Krótkie wprowadzenie określa, w jaki sposób powinny brzmieć odpowiedzi pewne lub ostrożne, oraz co robić, gdy kontekst rzeczywiście nie zawiera odpowiedzi: należy to jasno powiedzieć, zamiast coś wymyślać.
  • Ton i styl. Określono tu formalność, typową długość odpowiedzi oraz to, czy należy zacząć od bezpośredniej odpowiedzi, czy ją stopniowo rozwijać, zamiast pozostawiać to niejasne.
  • 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.
  • Klasyfikuj intencję, a nie tylko temat, i przypisz każdemu rodzajowi pytania odpowiedni sposób odpowiadania.
  • Odzyskuj pamięć na żądanie oraz przy granicach partii, aby usługa pozostawała tak szybka po godzinach pracy, jak w momencie uruchomienia.
  • 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