Kategorie
AI-boratoria Artificial Intelligence Data Science Tutorials

Wprowadzenie do Retrieval Augmented Generation (RAG). Tworzenie prostych dokumentów dynamicznie rozszerzając bazową wiedzę modelu o dane z zewnątrz

Problem halucynacji i konfabulacji generowanych przez modele językowe (LLM) możemy do pewnego stopnia adresować za pomocą programowania. Integracja generatywnego AI z przemyślaną logiką aplikacji pozwala znacząco poprawić jakość generowanych odpowiedzi, zmniejszając ryzyko błędnych lub nieprecyzyjnych informacji.

Na przykład, ograniczenie halucynacji można osiągnąć poprzez wyraźne określenie daty granicznej wiedzy modelu (np. „Data mojej wiedzy to wrzesień 2021, a obecna data to wrzesień 2023”), co pomaga uniknąć generowania nieaktualnych lub nieprawdziwych informacji.

Obecną datę możemy wygenerować za pomocą wbudowanej biblioteki datetime w Python (np. "Data mojej wiedzy to wrzesień 2021, a obecna data to " + datetime.datetime.now().strftime('%B %Y')).

%B: Ten format oznacza pełną nazwę miesiąca (np. „Wrzesień” dla września).
%Y: Ten format oznacza pełny rok (np. „2023”).

Narzędzia takie jak Phind i Perplexity rozszerzają podstawową wiedzę modelu o dodatkowy kontekst, wykorzystując wyniki wyszukiwania, np. z Google. Korzystając z tych narzędzi, należy zauważyć, że odpowiedzi są uzupełniane o konkretne źródła, co zwiększa ich wiarygodność. Tworząc własne aplikacje, warto rozważyć dodanie tej funkcjonalności, aby zapewnić lepszą jakość generowanych informacji.

Warto w tym miejscu zapisać poniższy fragment kodu, ponieważ na późniejszym etapie nauki będziemy tworzyć własną bazę snippetów. Snippety te zostaną zintegrowane z ekspanderami tekstu, co pozwoli na automatyzację naszej pracy z generatywną AI.

Hey! Senior Full-stack Developer here. I can answer any question related to web technologies like HTML/CSS/JavaScript, Python, generative AI, and modern frameworks i.e., Flask using the context provided below and nothing else.

Aby skutecznie kontrolować ograniczenia modelu, takie jak ograniczona baza wiedzy i zapobieganie generowaniu konfabulacji, warto w prompcie systemowym umieścić odpowiedni kontekst, z którego model powinien korzystać. Aby model rzeczywiście przestrzegał tego kontekstu, niezbędne jest również ustalenie zasad oraz podanie przykładu, który ilustruje, jak unikać halucynacji. Na przykład:

- Who are maiko?
- Sorry, but I will not answer this question, because my answer is limited to the context given.

Natomiast sam przykład może okazać się nie wystarczający. Wówczas powinniśmy zadbać również o podanie modelowi zasad, którymi ma się kierować generując odpowiedź:

- I do not answer any questions that do not relate to the added context
- I always answer very concisely
- I always answer truthfully, as if I were under oath
- Before I answer, I will take a deep breath and think thoroughly about what is provided in the context

Wreszcie na sam koniec dodajemy do instrukcji systemowej wspominany kontekst, który na potrzeby tego wpisu skopiowałem z dokumentacji Flask’a.

Instrukcja systemowe podkreślająca prawdomówność i korzystanie z przekazanego kontekstu.

Sprawdźmy, czy za pomocą powyżej zaprojektowanego promptu, model będzie w stanie odpowiedzieć prawdomównie na pytanie dotyczące blue-printów, nie mówiąc mu, że mamy na myśli koncepcję pochodzącą z frameworka Flask.

Dostaliśmy odpowiedź wyjaśniającą pojęcie blue-printów w kontekście frameworka Flask. Dodatkowo warto zwrócić uwagę, że pomimo dostarczenia kontekstu w języku angielskim, na podstawie zadanego pytania wygenerowana odpowiedź jest w języku polskim. Pojawia się natomiast inny problem: jako niedoświadczeni programiści nie jesteśmy w stanie jednoznacznie stwierdzić, czy zwrócona odpowiedź jest zgodna z prawdą. Do tego problemu wrócimy w kolejnych lekcjach.

Odpowiedź od modelu zgodna z oczekiwaniami.

Zadajmy sobie pytanie, jak odpowie model, jeśli zadamy pyutanie, które będzie wykaraczać poza dostarczy kontekst?

Odpowiedź od modelu zgodna z oczekiwaniami.

Na podstawie przeprowadzonych eksperymentów można stwierdzić, że uzyskaliśmy pewien poziom kontroli nad odpowiedziami modelu. Należy jednak pamiętać, że nie mamy pełnej pewności, iż model nie skorzysta z bazowej wiedzy w przyszłych odpowiedziach. Ze względu na coraz częstsze praktyki dostawców, takie jak cache’owanie odpowiedzi, problem ten z czasem powinien stać się mniej istotny.

Tworząc agentów wyspecjalizowanych w określonych zadaniach, zależy nam na:

  • Nadaniu odpowiedniej roli,
  • Ustaleniu faktów,
  • Określeniu zasad,
  • Podaniu przykładów,
  • Dostarczeniu kontekstu,

czym dostarczenie kontekstu będzie przedmiotem bieżącej lekcji.

Omówienie i budowa pierwszego mechanizmu dostarczającego dynamicznie zewnętrzny kontekst do instrukcji systemowej.

W dalszej części lekcji będę odnosił się do Qsystenta — agenta, którego stworzymy podczas całych AI-laboratoriów. Asystenta będziemy stopniowo rozbudowywać o kolejne moduły, które poznamy w kolejnych lekcjach.

Przedstawię kilka podstawowych założeń, które będą miały zastosowanie do tworzonych dokumentów:

  • Dodawanie etykiet, metadanych oraz źródeł do zewnętrznych danych, aby ułatwić ich identyfikację i organizację
  • Wyszukiwanie fragmentów treści
  • Optymalizacja dokumentów przed ich włączeniem do kontekstu poprzez podsumowanie w osobnym prompcie
  • Stosowanie wyrażeń regularnych w celu zastępowania długich, nieczytelnych linków krótszymi placeholderami, które zużywają mniej tokenów

Biorąc pod uwagę powyższe założenia, zbudujmy prosty mechanizm Retrieval Augmented Generation.

Na początek utwórzmy katalog o nazwie 03_lesson, korzystając z komendy mkdir 03_lesson. Następnie zainstalujemy wirtualne środowisko w tym katalogu, używając komendy python3 -m venv venv. Jeśli nie jesteś zaznajomiony z koncepcją wirtualnych środowisk Pythona, odsyłam do mojego szczegółowego wpisu, który wyjaśnia ich znaczenie i zastosowanie.

Aby zacząć pisać logikę wczytywania dokumentów, skorzystamy z zewnętrznego pakietu LangChain, który udostępnia prostą klasę do ładowania plików markdown. Najpierw aktywujemy wirtualne środowisko. W tym celu, z poziomu katalogu 03_lesson, wpisujemy w terminalu: source venv/bin/activate. Następnie instalujemy potrzebne pakiety, które będą wykorzystywane w dalszej części projektu.

pip install langchain
pip install langchain-openai
pip install langchain-community
pip install "unstructured[md]" nltk
pip install python-dotenv

Następnie importujemy metody, klasy lub całe pakiety Pythona, które będą przydatne w naszym przykładzie. Szczegóły dotyczące poszczególnych importów są omówione w komentarzach w pliku simple_memory.py, który udostępniłem w repozytorium na GitHub oraz jako folder na Google Drive. Dzięki temu możecie łatwo śledzić kod i zrozumieć jego działanie.

from langchain_openai import ChatOpenAI
from dotenv import load_dotenv, find_dotenv
from langchain_core.messages import HumanMessage, SystemMessage
from langchain_community.document_loaders import UnstructuredMarkdownLoader
from langchain_core.documents import Document

Po zaimportowaniu interesujących nas bibliotek możemy rozpocząć implementację właściwego kodu. Najpierw lokalizujemy plik .env i wczytujemy zmienne środowiskowe do systemu. Następnie tworzymy instancję klasy ChatOpenAI(), która będzie odpowiadała za nawiązanie połączenia z modelem OpenAI. Kolejnym krokiem jest zdefiniowanie tablicy wiadomości, którą przekażemy jako parametr do metody invoke(), znanej z poprzednich lekcji. W wiadomości systemowej określamy rolę Qsystenta. Posługując się snippetami promptów z początku wpisu, ustalamy zasady, jakimi ma się kierować generując odpowiedź. Zwróć uwagę, że w systemowym prompcie zdefiniowaliśmy również możliwość dodania kontekstu pochodzącego z zewnętrznego źródła danych.

# Role
Hey! Senior Full-stack Developer here. My nickname is 'Qsystent'. I can answer any question related to web technologies like HTML/CSS/JavaScript, Python, generative AI, and modern frameworks i.e., Flask using the context provided below and nothing else.

# Rules
- I do not answer any questions that do not relate to the added context
- I always answer very concisely
- I always answer truthfully, as if I were under oath
- Before I answer, I will take a deep breath and think thoroughly about what is provided in the context

# Example
- Who are maiko?
- Sorry, but I will not answer this question, because my answer is limited to the context given.

# Context
Implementacja logiki wczytywania zewnętrznych danych.

Zatrzymajmy się na chwilę, aby stworzyć plik note.md w katalogu 03_lesson, w którym umieścimy krótką notatkę o nas samych. Może to być prosty opis, podobny do mojego:

Dokument przedstawiający podstawową wiedzę o mnie.

Kiedy mamy już gotową notatkę, możemy ją wczytać do naszego kodu. W tym celu użyjemy klasy UnstructuredMarkdownLoader, do której przekażemy ścieżkę do pliku stworzonego w poprzednim kroku. Wynik działania tej klasy przypiszemy do zmiennej loader, na której następnie wywołamy metodę load(). Wynik działania tej metody przypiszemy do kolejnej zmiennej, documents.

Metoda load() zwróci tablicę dokumentów, a ponieważ wczytujemy tylko jeden plik, powinniśmy otrzymać tablicę o długości 1, zawierającą jeden element – obiekt typu Document. Ta struktura danych jest zdefiniowana w bibliotece LangChain i posiada właściwość page_content, która przechowuje wczytany tekst w formacie Markdown. Co więcej, Document oferuje także dostęp do automatycznie wygenerowanych metadanych, takich jak język notatki, data ostatniej modyfikacji czy kategoria, co może okazać się bardzo użyteczne przy dalszej obróbce danych.

Zanim wydrukujemy zawartość obiektu Document wraz z informacją o metadanych, sprawdźmy, czy wynik działania metody load() zwrócił oczekiwany przez nas rezultat, czyli tablicę z jednym elementem zawierającym Document. W języku Python możemy wyrzucać błędy za pomocą operatora assert. Jeśli wyrażenie znajdujące się po prawej stronie od słowa assert nie będzie prawdą, wówczas interpreter Pythona zwróci błąd.

note_path = 'note.md'
loader = UnstructuredMarkdownLoader(note_path)

data = loader.load()
assert len(data) == 1
assert isinstance(data[0], Document)
content = data[0].page_content
print('Content: ' + content)
print('\n')
metadata = data[0].metadata
print('Metadata: ' + metadata)

Poniżej ilustruję przykład działania napisanego dotychczas kodu:

Przykład wykonania kodu przedstawiający funkcjonalność ładowania zewnętrznych dokumentów.

Aby zakończyć przykład z modułem odpowiedzialnym za dynamiczne wczytywanie zewnętrznego kontekstu, sformatujemy wiadomość systemową, przekazując jako kontekst treść dokumentu notatki. Następnie, aby zweryfikować działanie modelu, zadamy pytanie dotyczące zainteresowań Szymona i wywołamy przygotowany kod. Przypomnę w tym miejscu, że na początku naszego pliku zadeklarowaliśmy zmienną chat przypisując do niej obiekt instancję klasy ChatOpenAI().

Całość kodu odpowiedzialnego za wczytywanie zewnętrznych danych do kontekstu zapytania.

Poniżej przedstawiono przykład działania napisanego dotychczas kodu. Zanim przejdziemy do kolejnej części artykułu, proponuję, żeby w ramach ćwiczenia, na podstawie wiedzy z lekcji wprowadzających, napisać powyższy kod, formatując odpowiedź od modelu za pomocą wbudowanej w LangChain klasy StrOutputParser, a następnie połączymy obiekt chain i parser w jeden łańcuch wywołań, na którym wykonamy metodę invoke().

Inteligentny wybór wczytywanego dokumentu rozszerzający wiedzę modelu.

Drugim modułem, który dzisiaj napiszemy, będzie moduł odpowiedzialny za wczytywanie dokumentów w zależności od zadanego pytania przez użytkownika.

Przed przejściem do pisania drugiego modułu, należy podsumować, że do tego momentu napisaliśmy pierwszy moduł Qsystenta, nadając mu odpowiednią rolę, zasady odpowiedzi oraz logikę dotyczącą ładowania dokumentów. W zależności od przyjętego podejścia, inne moduły Qsystenta mogą mieć odmienne zasady odpowiedzi, a nawet przypisaną inną rolę. Ponieważ abstrakcja dotycząca złożenia wszystkich modułów w jedno rozwiązanie może być sporym wyzwaniem na początku przygody z programowaniem w Pythonie, równolegle do prowadzonych AI-laboratoriów na tym blogu, będę tworzył projekt pod adresem https://aiboratorium/, gdzie udostępnię naszego Qsystenta wraz z dostępem do kodu źródłowego aplikacji.

W poprzednim przykładzie przekazaliśmy całą treść notatki do kontekstu. Jednak okno kontekstowe modelu nie jest nieograniczone, więc często warto optymalizować przekazywaną treść tak, aby ograniczać ją wyłącznie do informacji istotnych dla konkretnego pytania użytkownika aplikacji. To podejście pozwala na efektywniejsze wykorzystanie zasobów tj. dostępnym w ramach zapytania tokenów i zwiększa precyzję odpowiedzi generowanych przez model.

Przykład, który poniżej omówimy, będzie się składał z dwóch plików i jest bardziej zaawansowaną częścią poprzedniego modułu, lecz w zależności od logiki jaką przyjmiemy wdrażając naszą aplikację, może stanowić również osobny moduł budowanego rozwiązania. W kontekście integracji Generatywnego AI z kodem projektu istnieją różne techniki, które pozwalają na precyzyjne dobieranie dokumentów do zadanego pytania. Intuicyjnym wyborem są silniki wyszukiwania lub bazy wektorowe, które umożliwiają semantyczne wyszukiwanie najbardziej odpowiednich treści. Jednak tym zagadnieniem zajmiemy się bardziej szczegółowo w kolejnych AI-laboratoriach.

Logikę drugiego modułu umieścimy również w folderze 03_lesson, ale w nowym pliku o nazwie selection.py. Aby utworzyć ten plik, będąc w folderze 03_lesson, otwórz terminal i wpisz: touch selection.py.

Dzięki temu dodamy nowy plik, w którym umieścimy kod odpowiadający za selekcję odpowiedzi. Ponieważ cały czas pracujemy w kontekście projektu 03_lesson, nasze wirtualne środowisko powinno być już utworzone i nie wymaga ponownej instalacji zewnętrznych pakietów. Możemy od razu przejść do pisania kodu.

W pliku selection.py zaczniemy od importu klasy ChatOpenAI() z pakietu LangChain. Jest to klasa, z którą już wcześniej pracowaliśmy. Następnie importujemy SystemMessage.

from langchain_openai import ChatOpenAI
from langchain_core.messages import SystemMessage
from dotenv import load_dotenv, find_dotenv

Do zmiennej q = 'Co jest największym priorytetem życiowym Simby?'przypiszemy przykładowe pytanie. Zanim przejdziemy do omawiania pozostałej części kodu poświęćmy chwilę na stworzenie trzech notatek w formacie Markdown, każdą przypisaną do innej znanej Ci osoby, stworzenia bądź przedmiotu. Pierwszą notatkę możemy skopiować z poprzedniego przykładu, zmieniając jedynie nazwę pliku na szymon.md, analogicznie dwie pozostałe notatki nazwij imieniem wybranej przez Ciebie postaci np. simba.md i honda.md. W każdym z nowo utworzonych plików Markdown umieść odpowiadającą im nazwom notatkę w postaci krótkich zdań oddzielonych enterem np.:

Simba jest psem Szymona

Simba jest mieszańcem Huskyego i Rottweilera

Życiowym priorytetem Simby są spacerki

Simba nie lubi kąpanka

Simba reaguje na komendę 'owsianka', która oznacza spacer

Warto w tym momencie poświęcić czas na uzupełnienie notatek rzeczywistymi danymi, ponieważ od samego początku będziemy mogli rozwijać osobistego agenta, który rozumie nas i potrafi komunikować się zgodnie z naszymi oczekiwaniami.

Poświęćmy chwilę na uzupełnienie notatek, co jest Twoją drugą pracą domową dzisiejszych AI-boratoriów, a następnie wróćmy do kodu, który w tej fazie powinien wyglądać następująco:

Wstęp do kodu pozwalającego na wybór odpowiedniego źródła danych.

Utwórzmy tablicę obiektów, którą na dalszym etapie kodu, odpowiednio sformatowaną, przekażemy do modelu jako część instrukcji systemowej. Obiekt ten będzie miał dwie właściwości: name, zawierającą nazwę, oraz source, która będzie przechowywać odpowiadającą mu ścieżkę.

notes = [
    {'name': "Szymon (Szymurai)", 'source': "szymon.md"},
    {'name': "Simba (Psimba - Król Pies)", 'source': "simba.md"},
    {'name': "Honda", 'source': "honda.md"}
]

Sformatujmy wiadomość systemową, przekazywaną jako jeden z elementów tablicy messages przy wywołaniu metody invoke()na obiekcie chat stworzonym na podstawie klasy ChatOpenAI. Zwróć uwagę, że wywołując metodę invoke() możemy w tablicy messages przekazać tylko jeden element np. samą wiadomość systemową, czyli jedną instancję klasy SystemMessage.

system_message = SystemMessage(content=f'''
# Rules
From among the given notes, select the one corresponding to the user's query and return the path to the file. Do not add unnecessary comments.
                               
# Notes
{', '.join(note['name'] for note in notes)}
File paths: {', '.join(note['source'] for note in notes)}

User query: {q}

Note file path:
''')

Gotową wiadomość systemową można przekazać bezpośrednio jako element listy argumentów metody invoke(), wywoływanej na obiekcie chat. Wynik wywołania tej metody przypiszemy do zmiennej file_path. Następnie wydrukujemy zawartość odpowiedzi od modelu, aby zweryfikować, czy moduł wyboru odpowiedniego kontekstu działa zgodnie z naszymi założeniami.

file_path = chat.invoke([system_message])

print(file_path.content)

Dla tych, którzy przeglądają dzisiejszą lekcję bez dostępu do przykładowego kodu, załączam zrzut ekranu przedstawiający kompletny moduł oraz testowaną odpowiedź modelu.

Plik selection.py przedstawiający częściową logikę drugiego modułu dzisiejszej lekcji.

Powyższy mechanizm, będący częścią drugiego modułu, który omówimy wkrótce, będzie ponownie wykorzystywany w procesie budowy Qsystenta. Dlatego warto przeanalizować kod kilka razy i spróbować napisać go samodzielnie — to trzecie zadanie domowe.

Jak zmodyfikować powyższe dwa moduły, aby połączyć je w jeden spójny system, tworząc wciąż prosty, lecz kompletny mechanizm Retrieval-Augmented Generation?

Jak wspomniano we wstępie dzisiejszej lekcji, kluczowym elementem pracy z zewnętrznymi danymi jest ich opisanie odpowiednimi metadanymi. Tak przygotowaną notatkę, czyli treść dokumentu wraz z metadanymi, takimi jak ścieżka do pliku, będziemy nazywać dokumentem. Dokument mieliśmy już okazję tworzyć przy okazji omawiania pierwsdzego modułu w pliku simple_memory.py.

note_path = 'note.md'
loader = UnstructuredMarkdownLoader(note_path)

documents = loader.load()

Wówczas metoda load() wywołana na obiekcie loader zwracała listę dokumentów.

Obiekt dokumentu możemy stworzyć bezpośrednio na podstawie klasy Document. Obiekty dokumentów na liście documents, zwracanej z wywołania metody loader.load(), są opisane metadanymi, ale nie do końca zgodnymi z logiką, którą chcemy zaimplementować w naszej aplikacji. Możemy stworzyć rozwiązanie mapujące otrzymane dokumenty, a następnie, za pomocą odpowiednio zdefiniowanego promptu, nadpisać wartość source danego dokumentu. Jeśli pogubiłeś się na tym etapie warto zatrzymać się na chwilę i przypomnieć sobie omawiany wcześniej kod. W przeciwieństwie do pierwszego przykładu, dokumentem nie będzie cała treść wczytanego pliku, lecz jego wycinki, które w kontekście Generatywnego AI nazywamy chunks. Poniższy fragment kodu ilustruje koncepcję tworzenia obiektu dokument bezpośrednio, jako instancję klasy Document z pakietu LangChain.

Tworzenie obiektu document.

W poniższym przykładzie zastosujemy omawianą koncepcję dokumentów. Będąc w katalogu 03_lesson, utworzymy zbiorczą notatkę o nazwie documents.md, w której zamieścimy zawartość plików szymon.md, simba.md oraz honda.md.

Notatka zbiorcza zawierająca treść wcześniej utworzonych dokumentów.

Podobnie, jak w pierwszym przykładzie simple_memory.py skorzystamy z biblioteki LangChain do wczytania treści dokumentu documents.md, tym razem jednak skorzystamy z klasy TextLoader, która wczyta zawartość dokumentu, jako zwykły tekst.

from langchain_core.messages import HumanMessage, SystemMessage
from langchain_community.document_loaders import TextLoader
from langchain_core.documents import Document
from dotenv import load_dotenv, find_dotenv
from langchain_openai import ChatOpenAI

loader = TextLoader('documents.md')
documents = loader.load()
document = documents[0]

print(document)

Jak można zauważyć, po wywołaniu powyższego kodu, otrzymujemy obiekt z właściwością page_content i metadata z wartością source ustawioną na documents.md. Naszym zadaniem będzie podzielić powyższą długą treść na mniejsze części, wspomniane chunks, a następnie każdy z fragmentów opisać imieniem bądź nazwą powiązaną z wyciętym zdaniem.

Rozpocznijmy od fundamentalnych założeń, czyli od podziału pobranej zawartości, na mniejsze fragmenty. Podział będzie oparty na podwójnym znaku enter \n\n.

fragments = document.page_content.split('\n\n');

for fragment in fragments:
    print(fragment)
    print('\n')

Używając utworzonej tablicy stringów fragments, dla każdego elementu tej listy tworzymy nowy obiekt document, który odpowiada danemu fragmentowi tekstu. Następnie przypisujemy go do zmiennej doc i dodajemy do listy dokumentów docs.

docs = []
for fragment in fragments:
    doc = Document(page_content=fragment, metadata={"source": ""})
    docs.append(doc)

print(docs)

Ponieważ ta operacja może być trudna do zrozumienia, zwłaszcza na początkowym etapie nauki, dołączę również zrzut ekranu z edytora kodu z dodatkowymi komentarzami do poszczególnych kroków.

Wykonywanie pętli na stringach w celu stworzenia obiektów document i dodania ich na osobną listę.

Następnym krokiem będzie utworzenie nowej listy, która przechowa opisy każdego zdania przypisanego do obiektu document w polu page_content. Ważne jest, aby pamiętać, że indeksy dokumentów w tablicy docs będą odpowiadać indeksom ich opisów w liście doc_descriptions.

Tworzymy kolejną pętlę, iterując po liście docs. Dla każdego elementu wywołujemy metodę chat.invoke, używając odpowiednich wiadomości SystemMessage i UserMessage. Następnie wynik tej metody dodajemy jako kolejny element do listy doc_descriptions.

doc_descriptions = []

for doc in docs:
    doc_descriptions.append(chat.invoke([
        SystemMessage(content='''
        Your task is to describe the given sentence using one of the following words: Simon, Simba, Honda. Return only the related word. Do not add other content. Additional comments are unnecessary.
        '''),
        HumanMessage(content=f'Sentence: {doc.page_content}')
    ]))

W tym momencie powinniśmy mieć już utworzone dwie listy: docs i doc_descriptions. Lista docs przechowuje zdania wycięte z pliku documents.md, natomiast doc_descriptions zawiera opisy odpowiadające tym zdaniom, takie jak „Szymon”, „Simba” czy „Honda”. Ważne jest, aby zwrócić uwagę na korelację między indeksami elementów w obu listach. Indeksy opisów dokładnie odpowiadają indeksom zdań. Mając to na uwadze, możemy do każdego zdania, czyli obiektu document w liście docs, dodać wartość do właściwości sources, która jest przechowywana w obiekcie metadata. Tą wartością będzie odpowiedni opis, czyli jedno ze wspomnianych słów. Obiekty dokumentów, które są zdaniami z pliku documents.md zawierją właściwość page_content i metadata, wartością page_content jest zdanie w postaci stringu, a wartością metdata jest obiekt przechowujący właściwość source, do której przypieszemy odpowiedni opis w postaci jednego z następujących słów: „Szymon”, „Simba” bądź „Honda”.

for index, description in enumerate(doc_descriptions):
    docs[index].metadata.source = description.content

Powyższy zapis to fragment kodu w Pythonie, który wykonuje operacje na każdym elemencie listy doc_descriptions w pętli. Używamy metody enumerate, aby uzyskać zarówno indeks, jak i wartość elementu, co ułatwia odniesienie się do odpowiedniego indeksu.

Na zakończenie chcielibyśmy zapisać sparsowany plik documents.md jako documents.json. W tym celu korzystamy z wbudowanej w Python biblioteki json komendą import json. Każdy element listy docs, który jest obiektem klasy Document, konwertujemy na obiekt w formacie Python, który można łatwo zamienić na JSON. Następnie używamy wyrażenia with open('documents.json', 'w') as documents: do zapisania listy obiektów docs jako osobnego pliku w formacie JSON.

import json

docs_dict = [doc.to_json() for doc in docs]

with open('documents.json', 'w', encoding='utf-8') as documents:
    json.dump(docs_dict, documents, ensure_ascii=False, indent=4)

Bardziej zaawansowanym elementem jest konwersja obiektu document na obiekt Python, przy użyciu składni list comprehension. W nawiasach kwadratowych iterujemy po każdym elemencie docs, dodając wynik do listy, która następnie zostaje przypisana do zmiennej docs_dict. Stworzony w ten sposób plik JSON jest ważnym elementem dzisiejszej lekcji, ponieważ będziemy go używać, w podobnej formie, podczas omawiania baz wektorowych.

Plik JSON ilustrujący strukturę documentów wzbogaconych o metadane.

Podsumowanie

W tym artykule najpierw omówiliśmy pierwszy moduł, którego kod znajduje się w pliku simple_memory. Ten moduł, który może być użyty do tworzenia własnego asystenta, odpowiada za wczytanie treści dokumentu i dostarczenie jej do kontekstu wiadomości systemowej modelu. Następnie zaczęliśmy budowę drugiego modułu, który odpowiada za wybieranie odpowiedniego kontekstu w zależności od pytania użytkownika. Na końcu stworzyliśmy trzeci plik, w którym przetworzyliśmy jeden duży dokument, dzieląc go na osobne zdania, a następnie każde ze zdań wzbogaciliśmy dodatkowymi opisami, wykorzystując zapytania do LLM.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *