Kategorie
Artificial Intelligence Data Science Programowanie w Python Tutorials

Integracja Oprogramowania z Generatywną AI. Nawiązanie połączenia z LLM za pomocą LangChain-Python. Wirtualne Środowisko Python — Praktyczne Kompendium wiedzy

Wstęp

W poprzedniej lekcji zapoznaliśmy się z implementacją OpenAI API w naszym kodzie za pomocą dedykowanego SDK-Python. Nawiązaliśmy również połączenie z OpenAI API za pomocą protokołu https i aplikacji Postman.

Natomiast wraz z rozbudową mniej bądź bardziej skomplikowanych rozwiązań korzystających z różnych API generatywnego AI spotkamy się z wyzwaniem w postaci integracji różnych modeli i ich promptów z naszą logiką w celu uzyskania planowanej odpowiedzi.

Warto w tym miejscu zwrócić uwagę, że aplikacje, które będziemy w przyszłości projektować, będą łączyć niedeterministyczną naturę dużych modeli językowych z kodem napisanego algorytmu, którego wynik z definicji można przewidzieć.

Osoby spotykające się z przedstawionymi powyżej ograniczeniami tworzą rozmaite narzędzia pomagające adresować złożoność opisywanych problemów.

Pomimo tego, że decydując się na implementację przytoczonej biblioteki w naszym kodzie, narzucamy dodatkową warstwę abstrakcji, zdecydowałem się poświęcić mu uwagę. Obecnie powstaje wiele ciekawych rozwiązań opartych o LangChain, które warto mieć na uwadze. Dodatkowo LangChain, jak i większość podobnych narzędzi, jest na tyle elastyczne, że kiedy zamierzamy ich użyć jako „środków transportu” do osiągnięcia wyznaczonego celu, w każdym momencie możemy „przesiąść się do samolotu” bądź iść dalej pieszo.

Nawiązanie połączenia za pomocą LangChain

LangChain zawiera interfejsy do wielu najpopularniejszych modeli, w tym tych, które możemy pobrać bezpośrednio na naszą lokalną instancję Ollamy (o czym więcej napiszę w kolejnych AI-boratoriach).

Podobnie jak w przypadku nawiązywania połączenia za pomocą OpenAI SDK-Python w LangChain również na początku tworzymy inicjalizację połączenia do modelu, a następnie wysyłamy zapytanie w formacie ChatML (system/user/assistant).

Różnice pomiędzy opisanymi podejściami zaczynają pojawiać się w momencie integrowania promptów (szablony promptów i ich kompozycja) oraz odpowiedzi od modelu (weryfikowanie formatu odpowiedzi) z naszą aplikacją. Oczywiście, możemy użyć różnych technik formatowania tekstu wbudowanych w język Python. Natomiast LangChain wychodzi nam naprzeciw, udostępniając wbudowane metody umożliwiające strukturyzowanie promptów. Ze względu na podatność modeli na zmiany w promptach powinniśmy mieć na uwadze utrzymywanie struktury promptów.

Podstawowe różnice pomiędzy OpenAI API SDK-Python a LangChain-Python.

Przykład nawiązania połączenia do modelu za pomocą LangChain.W pierwszej kolejności powinniśmy stworzyć w ramach naszego projektu (np. “02_lesson”) wirtualne środowisko cdn.

Wirtualne środowisko Python — Praktyczne wprowadzenie.

Mówiąc o wirtualnym środowisko w kontekście nauki Pythona mam na myśli stworzenie przestrzeni w ramach określonego projektu (stworzonego i nazwanego folderu gdzieś na naszym komputerze), która będzie odzwierciedleniem środowiska Python instalowanego globalnie na komputerze tj. z chwilą instalacji interpretera Python, którego mieliście już okazję instalować w 2. lekcji kursu Python). Jednocześnie umożliwiającą zainstalowanie wybranych zależności, które są wymagane do uruchomienia i prawidłowego działania tworzonego projekt, które nie będą dostępne z poziomu domyślnego zainstalowanego globalnie interpretera Python.

Żeby lepiej zrozumieć ideologię tworzenia wirtualnego środowiska w ramach projektu. Wyobraźmy sobie sytuację, w której pracujemy nad projektem, wymagającym doinstalowania szeregu skomplikowanych bibliotek, których użyjemy tylko w tej określonej aplikacji. Następnie po skończeniu prac nad naszym narzędziem wyślemy go do jakiegoś repozytorium, którym w najprostszej wersji może być nawet Google Drive. Gdybyśmy wszystkie wspomniane zależności zainstalowali globalnie, nieużywane zaśmiecały by naszą jednostkę (irytując naszą mentalną perfekcję). Natomiast instalując wirtualne środowisko mamy tą pewność, że po usunięciu projektu nieużywane już więcej biblioteki również zostaną usunięte.

Zanim jednak wyślemy nasz projekt w odległe zakamarki GitHuba powinniśmy zrobić reprezentację zainstalowanych bibliotek w formie pliku tekstowego w formie wylistowanych pod sobą nazw zależności wraz z ich wybraną wersją. Ponieważ będzie nam zależeć na szybkim wysłaniu aplikacji do repozytorium nie chcemy wysyłać wszystkich plików z nią związanych w tym naszego wirtualnego środowisko, które możemy bezpośrednio odtworzyć na innym komputerze za pomocą wspomnianej reprezentacji, którą mamy w pliku tekstowym ważącym niespełna ułamek tego co zawierał katalog z pobranymi bibliotekami.

Tworzenie wirtualnego środowiska w ramach wybranego projektu (“02_lesson”).

W celu stworzeniu wirtualnego środowiska potrzebujemy skorzystać z natywnego modułu czyli, takiego wbudowanego w Pythona, o wdzięcznej nazwie “venv”, wpisując w terminalu komendę python3 -m venv <nazwa wirtualnego środowiska>. Nazwę wirtualnego środowiska możemy wpisać dowolną. Natomiast bardzo wygodnie jest podpiać pod skrót klawiszowy komendę python3 -m venv venv, wówczas automatyzujemy swoją pracę. Jednak wirtualne środowisko w każdym naszym projekcie będzie nazywać się “venv”. W poniższym przykładzie z edytora kodu celowo wpisałem inną nazwę środowiska tj. “example-venv” żeby zilustrować, że możemy wpisać dowolną nazwą np. relatywną do naszej aplikacji.

Tworzenie wirtualnego środowiska w ramach wybranego projektu za pomocą terminala wbudowanego w edytor kodu Visual Studio Code.

Aktywacja wirtualnego środowiska.

Na tym etapie mamy już utworzone wirtualne środowisko, co oznacza, że w naszym projekcie pojawił się nowy folder o nazwie zadeklarowanej przy wywołaniu komendy python3 -m venv example-venv. Ten katalog zawiera pliki niezbędne do aktywacji wirtualnego środowiska oraz zarządzania zależnościami instalowanych bibliotek. Kluczowym krokiem w tym momencie jest aktywowanie stworzonego środowiska wirtualnego, aby komputer mógł rozpoznać kontekst, w którym będą instalowane pakiety Pythona.

Wirtualne środowisko aktywujemy komendą source example-venv/bin/activate, czyli korzystamy z słowa kluczowego source odpowiedzialnego za wczytanie pliku, do którego ścieżkę podajemy w drugim argumencie wpisanego polecenia. Wówczas maksymalnie z lewej strony nowej linii poleceń w terminalu pojawi się informacja w postaci nazwy wirtualnego środowiska umieszczonego pomiędzy nawiasy półokrągłe tj. (example-venv). W tym momencie mamy pewność, że poprawnie daliśmy radę stworzyć i aktywować wirtualne środowisko.

Aktywowanie wirtualnego środowiska w wierszu poleceń.

Instalacja zależności projektowych w ramach wirtualnego środowiska.

Nadaliśmy systemowi kontekst, w którym będziemy instalować pakiety Python istotne dla rozwijanej aplikacji. Teraz zainstalujemy przykładową bibliotekę, wpisując komendę pip install Flask.

Flask to narzędzie korzystające z architektury WSGI, które służy do uruchamiania serwera HTTP. “pip” jest narzędziem służącym do zarządzania pakietami Python dostępnymi pod adresem https://pypi.org/ Na przytoczonej stronie możemy się między innymi dowiedzieć, że bogate repozytorium oficjalnych pakietów Python to 582,647 projektów. Dla porównania ogólnodostępne projekty JavaScript, które zarządzane są analogicznym narzędziem tj. “npm” są dostępne pod domeną https://www.npmjs.com/ Natomiast środowisko JavaScript nie chwali się ilością dostępnych pakietów. W ramach treningu spróbujcie poszukać w obydwu repozytoriach następujących rozwiązań: “LangChain” i “OpenAI”.

Instalacja pierwszego pakietu.

Zainstalowana biblioteka znajduje się w katalogu “lib” naszego wirtualnego środowiska.

Pakiety Python zainstalowane w ramach wirtualnego środowiska.

Docelowo katalogu reprezentującego środowisko Python lokalnie w naszym projekcie nie będziemy wysyłać wraz z aplikacją do repozytorium. Co więc możemy zrobić, żeby mieć informację o zainstalowanych bibliotekach jednocześnie móc swobodnie zapomnieć o katalogu “lib”? W tym momencie warto wspomnieć o komendzie pip freeze > <dowolna nazwa>.txt np. pip freeze > requirements.txt, która jak się domyślacie zamrozi aktualny stan aplikacji w kontekście instalowanych pakietów do pliku tekstowego z wybraną nazwą. Wówczas za pomocą, tak stworzonego drzewa zależności w postaci nazwy pakietu wraz z jego wersją. Możemy komendą pip install -r requirements.txt odtworzyć zapamiętany stan wymaganych pakietów.

Instalacja zależności w nowym środowisku wirtualnym z pliku tekstowego.

Jak częściowo wspomniałem w poprzednim akapicie biblioteki z pliku tekstowego requirements instalujemy za pomocą wydania polecenia pip install -r requirements.txt. Warto natomiast w tym miejscu zwrócić uwagę, że przed instalacją pakietów z pliku powinniśmy ponownie stworzyć i aktywować wirtualne środowisko. Na tym etapie projekt zawiera wyłącznie pliki z logiką aplikacji.

Po tym krótkim wprowadzeniu do zaawansowanego zagadnienia, jakim jest wirtualne środowisko Python, wróćmy do głównej części 2. lekcji AI-boratoriów.

cd. Inicjalizacja połączenia do modelu za pomocą LangChain, wraz z wygenerowaną odpowiedzią.

Gdy już mamy stworzone i aktywowane wirtualne środowisko możemy przystąpić do instalacji interesującej nas paczki tj. omawianego narzędzia jakim jest zyskujący na popularności LangChain.

W terminalu, za pomocą CLI (Command Line Interface), wpisujemy następującą komendę: pip install langchain.

Ponieważ będziemy porównać bibliotekę LangChain z OpenAI API SDK-Python, a za pomocą omawianego pakietu nawiążemy połączenie z modelem od OpenAI, takim jak gpt-4o.

W następstwie powyższego, warto w tym miejscu zainstalować dodatkową paczkę, którą udostępnia nam framework LangChain: pip install langchain-openai. Wówczas uzyskamy dostęp do interfejsu integrującego modele OpenAI.

Warto zaznaczyć, że interfejsy komunikacyjne z LLM (Large Language Models) od różnych dostawców zazwyczaj wymagają dedykowanego klucza dostępu. W związku z tym warto poświęcić chwilę na zainstalowanie biblioteki python-dotenv, która ułatwia zarządzanie zmiennymi środowiskowymi. Instalacja tej biblioteki jest prosta: wystarczy użyć polecenia pip install python-dotenv.

Dzięki python-dotenv można bezpiecznie przechowywać klucze dostępu oraz inne poufne informacje w pliku .env, co zwiększa bezpieczeństwo aplikacji i ułatwia jej konfigurację.

W zmiennej środowiskowej będziemy przechowywać OPENAI_API_KEY. W katalogu głównym omawianego projektu tworzymy plik .env, który powinien mieć następującą wartość: OPENAI_API_KEY="twój_dedykowany_klucz_wygenerowany_w_ustawieniach_konta_openai".

Konfiguracja pliku „.env” przechowującego wrażliwe dane, które są eksportowane, jako zmienne środowiskowe.

Na tym etapie powinniśmy mieć aktywne wirtualne środowisko oraz zainstalowane następujące zależności:

pip install python-dotenv
pip install langchain
pip install langchain-openai

Podstawowa interakcja z modelem za pomocą LangChain jest bardzo podobna do tej, którą umożliwia OpenAI API SDK-Python.

Przykład inicjalizacji połączenia do modelu za pomocą LangChain, wraz z wygenerowaną odpowiedzią.

W katalogu głównym projektu tworzymy plik Pythona z rozszerzeniem .py. W tym momencie powinniśmy mieć dwa pliki: .env z poprzedniego kroku oraz na przykład langchain_example.py, który będzie zawierał główną logikę nawiązania połączenia z OpenAI API za pomocą LangChain. Na początek importujemy funkcje load_dotenv i find_dotenv z zainstalowanej biblioteki dotenv: from dotenv import load_dotenv, find_dotenv. Te funkcje będą odpowiedzialne za ładowanie zmiennych środowiskowych z pierwszego napotkanego pliku .env, zaczynając od katalogu głównego i idąc w górę struktury katalogów. Następnie zaimportujemy funkcję ChatOpenAI z pakietu langchain_openai, a także HumanMessage i SystemMessage z pakietu langchain_core.messages.

Na tym etapie struktura projektu oraz zawartość pliku langchain_example.py powinny wyglądać następująco:

Importowanie bibliotek w pliku Python.

Przechodzimy teraz do pisania głównej logiki naszego programu w Pythonie. Na początek ładujemy zmienne środowiskowe, używając funkcji load_dotenv(). Ta funkcja jest odpowiedzialna za wczytanie zmiennych z pliku .env do środowiska wykonawczego. W przypadku, gdy plik .env znajduje się w katalogu nadrzędnym, np. gdy lesson_02 jest sub-projektem katalogu aiboratoria w funkcji load_dotenv() wywołujemy kolejną funkcję tj. find_dotenv(), której wynikiem wykonania jest argument mówiący funkcji load_dotenv() co ma dokładnie załadować doi zmiennych  środowiskowych. Za pomocą powyższego możemy precyzyjnie załadować odpowiednie zmienne środowiskowe, nawet jeśli plik .env jest umieszczony wyżej w strukturze katalogów.

Zaimportowana w poprzednim kroku klasa ChatOpenAI z pakietu langchain_openai, do której jako argument przekażemy parametr model z wartością gpt-4, umożliwia stworzenie obiektu, nazwijmy go chat, abyśmy mogli później odwoływać się do niego w kodzie. Stworzony w ten sposób obiekt posiada metodę invoke, która przyjmując tablicę z zadeklarowanymi wiadomościami system i user, nawiązuje połączenie z OpenAI API, zwracając obiekt JSON. Ten obiekt JSON jest ustrukturyzowanym formatem przypominającym obiekt Pythona, zawierającym pary właściwość-wartość. Odpowiedź JSON zawiera klucz content, który przechowuje odpowiedź od modelu.

Tworzymy listę wiadomości, przypisując do zmiennej messages tablicę w Pythonie. Pierwszym elementem tej tablicy jest instancja klasy SystemMessage, do której przekazujemy wiadomość systemową jako ciąg znaków (string). Wiadomość ta jest przypisywana do parametru content, co pozwala na stworzenie odpowiedniej wiadomości systemowej. Drugim elementem tablicy jest instancja klasy HumanMessage, która reprezentuje wiadomość pochodzącą od użytkownika.

messages = [
    SystemMessage(content="Hi! How are you?"),
    HumanMessage(content="I'm go")
]

Kiedy mamy gotową listę wiadomości, możemy użyć obiektu chat, aby nawiązać połączenie z OpenAI API i przypisać wynik do zmiennej response za pomocą wywołania: response = chat.invoke(messages).

Za pomocą wbudowanej w Pythona funkcji print(), wyświetlmy otrzymaną odpowiedź: print(response.content).

Odpowiedź od modelu po wykonaniu połączenia do OpenAI API za pomocą LangChain.

Porównując nawiązanie połączenia do OpenAI API za pomocą LangChain, z Python SDK od OpenAI, na pierwszy rzut oka nie widać znaczących różnic. Jednak warto rozważyć użycie LangChain podczas projektowania bardziej rozbudowanych interakcji z modelem. Różnice uwidaczniają się przede wszystkim wraz ze wzrostem złożoności logiki dotyczącej formatowania odpowiedzi oraz budowania struktury połączeń. LangChain oferuje dodatkowe możliwości organizacji przepływu informacji w postaci tzw. chainów.

Przykład nawiązania połączenia wraz z odpowiedzią do OpenAI API za pomocą Python-SDK od OpenAI.

Przykład użycia łańcuchów, tzw. chainów w LangChain.

Omawiana biblioteka udostępnia klasę StrOutputParser znajdującą się w pakiecie langchain_core.output_parser. Wartość przekazana do obiektu zbudowanego na podstawie klasy StrOutputParser umożliwia odczytanie odpowiedzi od modelu w postaci odpowiednio przygotowanego stringa. Wówczas mamy pewność, że odpowiedź otrzymana od modelu będzie w oczekiwanym formacie.

Importujemy klasę StrOutputParser z pakietu langchain_core.output_parser:

from langchain_core.output_parsers import StrOutputParser

Tworzymy instancję omawianej klasy tworząc obiekt parser.

parser = StrOutputParser()

Analizując pełny kod, można dostrzec, że korzystamy z dwóch obiektów: chat oraz parser. W poprzednim przykładzie metoda invoke była bezpośrednio wywoływana na obiekcie chat, z przekazaną listą wiadomości. W tej wersji dodatkowo angażujemy obiekt parser. W pracy z LangChain możliwe jest łączenie obiektów w sekwencje, tzw. chainy, które pozwalają na wywołanie metody invoke bezpośrednio na utworzonym łańcuchu, przekazując tablicę wiadomości. Takie podejście upraszcza zarządzanie przepływem danych.

chain = chat | parser

print(chain.invoke(messages))
Koncepcja łańcuchów korzystając z rozwiązania LangChain.

Kończąc dzisiejszą lekcję, warto również zwrócić uwagę na klasę ChatPromptTemplate, która udostępnia szablony promptów. Możemy je również włączyć do łańcucha wywołań, co strukturyzuje pracę z promptami. Warto w tym miejscu zwrócić uwagę, że bardzo zbliżone efekty do omawianej techniki uzyskamy również za pomocą wbudowanych w Pythona, mechanizmami formatowania stringów.

Natomiast ze względu na podatność LLmów na zmiany w promptach. Warto rozważyć utrzymanie struktury wiadomości za pomocą szablonów promptów od LangChain.

Stwórzmy teraz dwa stringi z dedykowanymi placeholderami, które wskazują, co ma się znaleźć w ich wnętrzu, odpowiednio przypisując je do zmiennych system_message i user_message. Następnie, przekazując te zmienne do wywołania metody from_messages na klasie ChatPromptTemplate, tworzymy obiekt, który przypisujemy do zmiennej prompt_template. Metoda from_messages przyjmuje jako argument listę zawierającą krotki, z których każda powinna mieć dwie wartości: jedną określającą typ wiadomości (system, user, assistant), a drugą odpowiadający jej string stworzony wcześniej. Przykład tworzenia szablonu ilustruje następujący kod:

from langchain_core.prompts import ChatPromptTemplate

system_message = "Translate the following into {language}."

user_message = "Text to translate: {text}"

prompt_template = ChatPromptTemplate.from_messages([
    ("system", system_message),
    ("user", user_message)
])

messages = prompt_template.invoke({
    'language': 'italian',
    'text': 'Cześć! Mam na imię Szymon.'
}).to_messages()

Zwróćcie uwagę, że wywołując połączenie do modelu za pomocą metody invoke przekazujemy obiekt Python, który ma odpowiednio dwie właściwości, odpowiadające nazwom umieszczonym w placeholderach, które wpisaliśmy do tworzonych stringów wiadomości system_message i user_message. Co dobrze ilustruje możliwości, jakie oferuje biblioteka LangChain dotyczące utrzymywania struktury promptów.

Wspominałem również, że szablony promtów możemy podłączyć do łańcucha. Ponieważ obiekt chat wymaga podania wiadomości, powinien znaleźć się za obiektem prompt_template, a obiekt parser na samym końcu, ponieważ formatuje on odpowiedzi od modelu.

chain = prompt_template | chat | parser

print(chain.invoke({
    'language': 'italian',
    'text': 'Cześć! Mam na imię Szymon.'
}))
Użycie łańcucha obiektów przy pomocy LangChain.

Logikę zaprezentowaną w przykładzie z użyciem Python SDK od OpenAI oraz LangChain można także zaimplementować w narzędziach automatyzujących, takich jak Make czy n8n. Ponadto, twórcy LangChain oferują znacznie więcej niż przedstawiono w dotychczasowych przykładach, jak na przykład streaming odpowiedzi, tworzenie API wraz z front-endem za pomocą LangServe, czy budowanie pamięci modelu. Warto zauważyć, że do tej pory w przykładach przekazywaliśmy jedynie wiadomość systemową wraz z pojedynczą wiadomością użytkownika i odbieraliśmy odpowiedź od modelu, co kończyło naszą interakcję z LLM. W praktyce jednak często zależy nam na zbudowaniu ciągu wiadomości. Możemy to osiągnąć poprzez ponowne wywoływanie funkcji w Python SDK bądź korzystając z mechanizmów pamięci dostępnych w LangChain. Ponieważ dzisiejsza lekcja obejmowała również szczegółowe omówienie wirtualnych środowisk w Pythonie i jest już dość obszerna, zakończymy na tym dzisiejsze AI-boratoria. Do omawianych tematów powrócimy w kolejnych zajęciach.

Dodaj komentarz

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