Główna nawigacja

Praktyczne przykłady niestandardowych rozwiązań RAG

Praktyczne przykłady pokazują, jak dostosowane systemy generowania wspomaganego wyszukiwaniem rozwiązują złożone problemy z wiedzą w firmach.

Obecnie RAG czasem cieszy się złą opinią: jedni uważają go za zupełnie banalny (łatwo zacząć, trudniej skalować), a inni sądzą, że wyparły go „systemy agentowe” (które po bliższym przyjrzeniu się często bardzo szybko zaczynają przypominać RAG…).

W tym artykule przedstawiamy kilka praktycznych przykładów pokazujących, jak radzimy sobie z typowymi wyzwaniami, takimi jak:

  • Obsługa mieszanych danych tekstowych i liczbowych oraz przyczyny, dla których zawodzą przy nich proste rozwiązania RAG: słowa kluczowe się powtarzają, a liczby nie niosą znaczenia semantycznego.

  • Dlaczego warto projektować osadzenia oparte na podsumowaniach: dla każdego fragmentu generujemy krótki opis, a następnie osadzamy i przeszukujemy właśnie to podsumowanie.

  • Jak generować podsumowania kontekstowe: uwzględniamy kontekst dokumentu nadrzędnego, aby rozróżniać podobnie wyglądające statystyki.

  • Kiedy polegać na kodzie i modelach Pydantic: gdy liczy się dosłowne zachowanie treści, łączymy własny kod lub model Pydantic z wywołaniami LLM, aby zapewnić niezawodność.

Tworzenie niestandardowych rozwiązań RAG

Podstawy

Systemy RAG napędzają różne rozwiązania — od botów obsługi klienta po wewnętrznych asystentów wiedzy.

Zwykle działają one w następujący sposób:

  1. Dzielisz dokumenty źródłowe na fragmenty

  2. Osadzasz każdy fragment w przestrzeni wektorowej

  3. Wyszukujesz K najlepiej dopasowanych fragmentów podczas przetwarzania zapytania

  4. Generujesz odpowiedź na podstawie tych fragmentów

Popularne zestawy narzędzi — LangChain, LlamaIndex i Filestore od OpenAI — sprawiają, że te kroki stają się niemal banalne. W rzeczywistych potokach napotkasz jednak dane, które nie są wyłącznie zwartym tekstem, a podstawowy RAG może sobie z nimi nie radzić. W kolejnych sekcjach pokażemy konkretne przykłady problematycznych danych i będziemy stopniowo rozwijać rozwiązanie wraz ze wzrostem złożoności.

Gdy sprawy się komplikują

  1. Gdy dane nie są wyłącznie tekstem (co zdarza się całkiem często)

Rozważmy poniższy fragment danych dotyczących gry:

JSON

{ "Attack": { "Range": { "default": 5, "with_Draconic_Ascension": 5.5, }, "Speed": { "default": "2 seconds", "with_Draconic_Ascension": "2.2 seconds", }, "Damage": { "default": 10, "with_Draconic_Ascension": 12, } }} 

Osadzenia działają dzięki wyuczonym zależnościom między słowami, wynikającym z ich znaczenia semantycznego i gramatyki. Powyższe dane stanowią połączenie tekstu i liczb, przy czym poza tym konkretnym kontekstem liczby nie mają związku ze słowami. Można więc powiedzieć, że ten fragment danych to w zasadzie zestaw w miarę opisowych słów, po których następują przypadkowe liczby.

Nie byłoby to problemem, gdybyśmy mieli tylko takie dane, ponieważ nadal moglibyśmy je wyszukiwać na podstawie osadzeń kilku dostępnych słów opisowych (albo po prostu użyć rozwiązania text-to-SQL). Co jednak, jeśli ten fragment ginie wśród wielu fragmentów zwartego tekstu, w których również występują te słowa? Na przykład:

JSON

{"Draconic_Ascension": { "description": "Transform into dragon form. Attack range and speed are increased by 10%, damage is boosted by 20%.", "details": { "activation_conditions": "Can only be activated when HP is below 50%or Fury meter is full.", "visual_effects": "Wings unfurl, scales shimmer with embers, voice lines change to echoing growls.", "lore": "An ancient bloodline awakens. The bearer of the mark channels the soul of the last Flamewing Wyrm, becoming a living storm of fire and fury." } }}

Wyobraźmy sobie, że chcemy wyszukać odpowiedź na pytanie „What is the attack range with Draconic Ascension?”Najprawdopodobniej nie uda nam się znaleźć właściwego fragmentu, ponieważ zginie on w szumie innych fragmentów zawierających te same słowa kluczowe.

Sedno problemu polega na tym, że nie potrafimy dobrze rozróżnić tych fragmentów danych, choć zawierają one różne rodzaje informacji na ten sam temat. Czy możemy jakoś wzbogacić lub ulepszyć te dane? Oczywiście, że tak :smile:

  1. Wzbogać dane, podsumowując je — tak, dobrze czytasz

Zamiast bezpośrednio osadzać sam fragment, możemy najpierw wygenerować podsumowanie opisujące dane, a następnie osadzić i przeszukiwać właśnie to podsumowanie. Na etapie generowania nadal korzystamy z oryginalnych danych powiązanych z podsumowaniem.

Dla dwóch przedstawionych wyżej fragmentów wygenerowalibyśmy więc podsumowania w rodzaju:

  1. Statystyki zasięgu, szybkości i obrażeń ataku (domyślne oraz z Draconic Ascension).

  2. Opis i szczegóły Draconic Ascension, w tym warunki aktywacji, efekty wizualne i fabuła.

Następnie rozszerzamy zapytanie, aby „dopasować” je do podsumowania. Na przykład pytanie „What is the attack range with Draconic Ascension?”zmienilibyśmy na „What are the statistics of attack range with Draconic Ascension?”Jest to szczególnie ważne, gdy zapytania wyszukiwawcze pochodzą od użytkowników spoza branży technicznej, którzy posługują się ~~„swobodnym”~~ zwykłym, naturalnym językiem. Nie muszą przecież wiedzieć ani przejmować się tym, jak działa RAG i jak maksymalizuje precyzję oraz pełność wyników.

Diagram przedstawiający sytuację, w której sprawy zaczynają się komplikować.

  1. Nie wyrywaj niczego z kontekstu (to ogólnie dobra zasada życiowa)

Kolejny scenariusz dotyczy obsługi mnóstwa fragmentów danych, które wyglądają tak samo, jak poniżej:

Plain Text

# Chunk one{ "Attack": { "Range": { "default": 9, }, ... }}# Chunk two{ "Attack": { "Range": { "default": 6, }, ... }}# Chunk three{ "Attack": { "Range": { "default": 7, }, ... }}

Trzymając się tego samego podejścia, wyobraźmy sobie pytanie „what is character X’s attack range?”Przy wygenerowanych właśnie podsumowaniach odpowiedź byłaby loterią, ponieważ one również wyglądałyby bardzo podobnie. Jak możemy je zatem rozróżnić?

Odpowiedź jest prosta: należy dodać kontekst. Możemy po prostu dodać do fragmentu danych odwołanie do dokumentu nadrzędnego, w tym przypadku np. {„postać”: „X”}. Dzięki temu możemy precyzyjnie wyszukać właściwe dane postaci X, nawet jeśli mamy takie same dane dla postaci Y i Z.

Lepszym i bardziej uniwersalnym rozwiązaniem byłoby jednak wygenerowanie kontekstowego podsumowania fragmentu. Innymi słowy, zamiast generować podsumowanie wyłącznie samego fragmentu danych, możemy przekazać zarówno jego dokument nadrzędny, jak i sam fragment, aby utworzyć ogólne podsumowanie kontekstowe. Wyjaśniamy w nim, jak dany fragment wpisuje się w dokument nadrzędny, na przykład:

  1. Ten fragment zawiera szczegółowe statystyki… postaci X. Fragment wpisuje się w cały dokument, pokazując przewagę X pod względem szybkości ataku…

  2. Ten fragment zawiera szczegółowe statystyki… postaci Y. Fragment wpisuje się w cały dokument, pokazując zwiększone statystyki Y po użyciu jej specjalnej zdolności…

  3. Ten fragment zawiera szczegółowe statystyki… postaci Z. Fragment wpisuje się w cały dokument, pokazując statystyki Z, dzięki którym świetnie sprawdza się jako tank w rozgrywkach drużynowych…

Ta metoda (częściowo inspirowana rozwiązaniem Anthropic) może wydawać się przesadą w powyższym przykładzie, ale jest bardzo skuteczna w przypadku fragmentów, które można błędnie zinterpretować „poza kontekstem”. Zapewnia też jednolite podejście do wszystkich fragmentów i pozwala zachować uporządkowany potok inżynieryjny.

Diagram przedstawiający sytuację, w której sprawy zaczynają się komplikować.

  1. Gdy trzeba być ~~maniakiem kontroli~~ skrupulatnym

Zwykle otrzymujemy kompletne dane i dzielimy je na fragmenty na potrzeby systemu RAG. W tym przykładzie pokazujemy coś nieco innego: dane podzielone na fragmenty, ale niewłaściwie. Są to przypadkowe części logicznej całości, które trzeba ponownie połączyć. Fragment logiczny to część treści, która z natury powinna tworzyć całość, na przykład podsekcja dokumentu lub spójny akapit.

Diagram przedstawiający sytuację, w której sprawy zaczynają się komplikować.

Najpierw przekazaliśmy wszystkie te dane do LLM i poprosiliśmy go, aby pogrupował je według własnej oceny, a następnie zwrócił połączoną treść. LLM powinien być w tym całkiem dobry, prawda? I tak, i nie.

W tym i kilku innych przypadkach odkryliśmy, że LLM mają tendencję do chodzenia na skróty i stają się zawodne, gdy wymagamy pełnej, dokładnej treści — zwłaszcza przy długim kontekście. Co jest całkowicie zrozumiałe. W tym konkretnym zastosowaniu było to jednak nie do przyjęcia, ponieważ potrzebowaliśmy dokładnej treści słowo w słowo — bez podsumowań i pomijania jakichkolwiek fragmentów oryginału. Nie możemy pominąć żadnych szczegółów.

Odpowiedź „tak” wynikała oczywiście z tego, że LLM świetnie rozumiał semantykę i strukturę uszkodzonych fragmentów. O ile tylko nie odmawiał dokładnego przytoczenia treści. A niech to :/

Jak zatem wykorzystać mocne strony LLM, a jednocześnie uniknąć obszarów, w których jest zawodny? Zwróciliśmy się do naszego starego, dobrego przyjaciela — kodu (czytaj: niestandardowej funkcji w Pythonie). Oraz maksymalnie prostego modelu Pydantic. Oto rozwiązanie:

  • Przechodź kolejno przez sekcje, zachowując bieżący fragment logiczny

  • Przy każdej sekcji zapytaj LLM: „Czy ta sekcja należy do bieżącego fragmentu logicznego?”. Odpowiedź ma brzmieć „tak” lub „nie” zgodnie z modelem Pydantic.

  • Jeśli tak, dołącz sekcję do fragmentu. Jeśli nie, zapisz ukończony bieżący fragment logiczny i rozpocznij nowy od tej sekcji.

Diagram przedstawiający sytuację, w której sprawy zaczynają się komplikować.

Oczywiście zużywamy tu nieco więcej tokenów niż przy jednokrotnym przetworzeniu całej treści. Jednak w tym zastosowaniu, gdzie najważniejsze było dokładne zachowanie treści, zdecydowanie warto było ponieść ten niewielki dodatkowy koszt.

To bardzo proste rozwiązanie, ale opiera się na ważnej zasadzie: gdy potrzebna jest skrupulatność, nie należy polegać wyłącznie na LLM, ponieważ z natury działają probabilistycznie.

Własny kod i funkcje oraz modele Pydantic pozwalają uzyskać przewidywalne, niezawodne wyniki, a zarazem w pełni wykorzystać możliwości LLM.

Podsumowanie

Tworzenie rozwiązania opartego na generatywnej AI jest w równym stopniu wyzwaniem inżynieryjnym, co wyzwaniem z dziedziny AI. Mamy nadzieję, że te przykłady zainspirują Cię do zmierzenia się z własnymi, nietypowymi wyzwaniami. Więcej informacji o projektowaniu rozwiązań wykorzystujących generatywną AI znajdziesz w naszym artykule o projektowaniu systemów agentowych opartych na routerach.

Autor

Cynthia Yu