Tworzenie interaktywnego czatu konsolowego LLM w Pythonie z Ollama i LiteLLM
W obliczu rosnącego zainteresowania lokalnymi dużymi modelami językowymi (LLM) i ich integracją z własnymi aplikacjami, kluczowe staje się nie tylko uruchomienie modelu, ale także stworzenie pełnoprawnego, interaktywnego interfejsu. Ten artykuł, będący kontynuacją serii, poświęcony jest transformacji podstawowego skryptu do jednorazowego zapytania LLM w funkcjonalny czat konsolowy w Pythonie. Omówimy kluczowe kroki w strukturyzacji kodu, implementacji promptów systemowych, dodawaniu interaktywności i obsłudze błędów, wykorzystując połączenie Ollama i LiteLLM. Celem jest pokazanie, jak przejść od prostego wykonania komendy do stworzenia stabilnej i zarządzalnej aplikacji, zdolnej do interakcji z użytkownikiem w czasie rzeczywistym, a także jak przygotować podstawy do dalszego rozszerzania funkcjonalności, w tym zarządzania kontekstem dialogu.
Od jednorazowego skryptu do interaktywnej aplikacji
Poprzednia część naszego przewodnika zademonstrowała podstawową integrację lokalnego LLM, uruchomionego za pomocą Ollama, z kodem Pythona przy użyciu biblioteki LiteLLM. Rezultatem był skrypt zdolny do wysłania jednego, z góry zdefiniowanego zapytania do modelu i wyświetlenia otrzymanej odpowiedzi, po czym program się kończył. Dla początkowego zapoznania się z technologią to wystarczy, jednak do stworzenia jakiejkolwiek praktycznej aplikacji takie podejście jest nieodpowiednie. Prawdziwy chatbot powinien zapewniać ciągłą interakcję: przyjmować dane wejściowe od użytkownika, przetwarzać je, otrzymywać odpowiedź od modelu i ponownie oczekiwać na kolejne pytanie, dopóki użytkownik nie zdecyduje się zakończyć sesji. Właśnie tę fundamentalną transformację zrealizujemy, przekształcając statyczny skrypt w dynamiczny czat konsolowy.
Kluczowe ulepszenia, które zostaną wdrożone w tej części, obejmują:
- Wyodrębnienie logiki zapytania do LLM w osobną, wielokrotnego użytku funkcję.
- Wykorzystanie promptu systemowego do zdefiniowania roli i zachowania asystenta.
- Organizację pętli komunikacji, umożliwiającej użytkownikowi zadawanie wielu pytań pod rząd.
- Wdrożenie pomiaru czasu odpowiedzi modelu w celu oceny wydajności.
- Podstawową obsługę wyjątków w celu zwiększenia odporności aplikacji na błędy.
Zrozumienie formatu wiadomości, z którymi pracują API LLM, jest podstawą efektywnej interakcji. Modele nie operują pojedynczą linią tekstu; przetwarzają listę wiadomości, z których każda ma określoną rolę:
system: To instrukcja dla modelu, określająca jego osobowość, styl odpowiedzi, ograniczenia lub ważne dane kontekstowe. Użytkownik bezpośrednio nie widzi tej wiadomości, jest ona formowana na poziomie kodu.user: Wiadomość lub pytanie pochodzące od użytkownika.assistant: Odpowiedź wygenerowana przez sam model. W przyszłych iteracjach to właśnie tutaj będą dodawane poprzednie odpowiedzi modelu w celu utrzymania kontekstu.
Architektura konsolowego czatu LLM w Pythonie
Przejdźmy teraz do implementacji. Zmodyfikujemy plik main.py, aby hermetyzować logikę interakcji z LLM i zorganizować pętlę wprowadzania danych przez użytkownika. Poniżej przedstawiono zaktualizowany kod, którego można użyć do zastąpienia zawartości pliku main.py.
# -*- coding: utf-8 -*-
import time
from typing import Optional
from litellm import completion
MODEL = "ollama_chat/qwen2.5:3b"
API_BASE = "http://localhost:11434"
SYSTEM_PROMPT = "Jesteś pomocnym asystentem. Odpowiadaj zwięźle i na temat."
def send_request_to_llm(user_message: str) -> Optional[str]:
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_message},
]
try:
start_time = time.time()
response = completion(
model=MODEL,
messages=messages,
api_base=API_BASE,
request_timeout=120,
)
duration = time.time() - start_time
print(f"\n⏱ Czas generacji: {duration:.2f} sek")
return response.choices[0].message.content
except Exception as e:
print(f"\n❌ Błąd podczas zapytania: {e}")
return None
def main() -> None:
print("🤖 Lokalny asystent AI uruchomiony.")
print("Wpisz pytanie lub 'wyjscie' aby zakończyć.\n")
while True:
user_input = input("👤 Ty: ").strip()
if user_input.lower() in ("wyjscie", "exit", "quit"):
print("Do widzenia!")
break
if not user_input:
print("⚠️ Wpisz pytanie.")
continue
print("\n🤔 Model myśli...")
answer = send_request_to_llm(user_input)
if answer is not None:
print(f"\n🦙 AI: {answer}\n")
else:
print("\n⚠️ Nie udało się uzyskać odpowiedzi. Sprawdź, czy Ollama jest uruchomiona.\n")
if __name__ == "__main__":
main()
Analiza kluczowych komponentów kodu
Rozważmy szczegółowo, jak działa każdy blok tego skryptu.
1. Stałe konfiguracyjne:
W początkowej części pliku zdefiniowano stałe: MODEL, API_BASE i SYSTEM_PROMPT. Takie podejście pozwala na łatwą zmianę parametrów konfiguracyjnych, na przykład użycie innego modelu LLM lub skorygowanie zachowania asystenta, bez zagłębiania się w logikę głównego kodu. SYSTEM_PROMPT odgrywa tutaj kluczową rolę, ustalając początkowe instrukcje dla modelu, które wpływają na ogólny ton i styl jego odpowiedzi.
2. Funkcja send_request_to_llm:
Ta funkcja (def send_request_to_llm(user_message: str) -> Optional[str]:) jest centralnym elementem interakcji z LLM. Przyjmuje wiadomość od użytkownika, tworzy pełną listę wiadomości (w tym SYSTEM_PROMPT i user_message) i wysyła ją za pośrednictwem litellm.completion do lokalnego modelu Ollama.
- Hermetyzacja: Logika wysyłania zapytania i odbierania odpowiedzi jest w pełni hermetyzowana, co sprawia, że główna pętla
mainjest czystsza i bardziej zrozumiała. - Pomiar czasu: Wbudowany pomiar czasu (
time.time()) pozwala ocenić wydajność modelu, pokazując, ile sekund zajęło wygenerowanie odpowiedzi. Jest to szczególnie przydatne podczas eksperymentowania z różnymi modelami lub parametrami. - Obsługa błędów: Blok
try...exceptzapewnia podstawową odporność na błędy. W przypadku wystąpienia błędu podczas zapytania do LLM (na przykład, jeśli Ollama jest niedostępna), funkcja zwracaNone, a nie powoduje krytycznego błędu, co pozwala funkcjimainna eleganckie obsłużenie sytuacji i kontynuowanie działania czatu.
3. Funkcja main i pętla interaktywna:
Funkcja main zarządza interfejsem użytkownika i ogólnym przepływem czatu.
- Nieskończona pętla (
while True): Tworzy ciągłą sesję czatu, która trwa, dopóki użytkownik wyraźnie nie zdecyduje się wyjść. - Wprowadzanie danych przez użytkownika:
input("👤 Ty: ").strip()oczekuje na dane wejściowe od użytkownika. Metoda.strip()usuwa początkowe i końcowe spacje, zapobiegając wysyłaniu pustych lub nieprawidłowych zapytań. - Komendy wyjścia: Sprawdzenie
user_input.lower() in ("wyjscie", "exit", "quit")pozwala użytkownikowi zakończyć program, używając różnych wariantów komendy wyjścia. - Puste dane wejściowe: Sprawdzenie
if not user_input:zapobiega wysyłaniu pustych wiadomości do modelu, sugerując użytkownikowi wprowadzenie pytania. - Interakcja:
mainwywołujesend_request_to_llmz danymi wejściowymi użytkownika i przetwarza otrzymaną odpowiedź, wyświetlając ją w konsoli lub zgłaszając błąd. Podział odpowiedzialności międzymain(zarządzanie interfejsem) asend_request_to_llm(interakcja z modelem) jest fundamentalną zasadą dobrego projektowania oprogramowania.
Zarządzanie zachowaniem LLM poprzez prompt systemowy
Jednym z najpotężniejszych narzędzi w pracy z LLM jest prompt systemowy. Pozwala on nadać modelowi określoną rolę, styl komunikacji lub zestaw ograniczeń, znacząco zmieniając jego odpowiedzi bez modyfikacji samego modelu czy jego parametrów. Eksperymentowanie z SYSTEM_PROMPT to doskonały sposób, aby zrozumieć, jak precyzyjnie można dostosować zachowanie asystenta do konkretnych zadań. Rozważmy kilka przykładów, jak zmiana jednej linii może kardynalnie wpłynąć na dialog:
SYSTEM_PROMPT = "Jesteś sarkastycznym asystentem. Odpowiadaj z ironią."— model będzie odpowiadał z humorem i złośliwością.SYSTEM_PROMPT = "Jesteś surowym nauczycielem. Poprawiaj błędy w pytaniach."— asystent będzie nie tylko odpowiadał, ale także wskazywał na błędy gramatyczne lub faktyczne w zapytaniach.SYSTEM_PROMPT = "Odpowiadaj tylko po polsku. Zawsze podawaj przykład kodu."— model będzie ściśle przestrzegał języka polskiego i starał się włączać przykłady kodu do swoich odpowiedzi, jeśli to możliwe.
Te przykłady pokazują, jak prompt systemowy przekształca statyczny model w elastyczne narzędzie, zdolne do adaptacji do różnych scenariuszy użycia. Jest to kluczowe dla tworzenia wyspecjalizowanych chatbotów, niezależnie od tego, czy jest to asystent techniczny, kreatywny partner czy narzędzie edukacyjne.
Co ważne
Na obecnym etapie rozwoju nasz konsolowy czat LLM osiągnął następujące ważne rezultaty:
- Interaktywność: Użytkownik może prowadzić ciągły dialog, zadając pytania i otrzymując odpowiedzi w czasie rzeczywistym.
- Zarządzane zachowanie: Prompt systemowy pozwala łatwo dostosować rolę i styl komunikacji modelu.
- Modułowość kodu: Logika interakcji z LLM jest oddzielona od logiki interfejsu użytkownika, co upraszcza dalszy rozwój i debugowanie.
- Wydajność: Wbudowany pomiar czasu generowania odpowiedzi pomaga monitorować szybkość działania modelu.
- Niezawodność: Podstawowa obsługa błędów zapobiega awaryjnemu zakończeniu programu w przypadku problemów z połączeniem lub modelem.
Typowe trudności i ich rozwiązania
Podczas pracy z lokalnymi LLM i aplikacjami konsolowymi mogą wystąpić pewne typowe problemy:
- Zawieszanie się programu po wprowadzeniu pytania: Najczęściej dzieje się tak, ponieważ Ollama nie jest uruchomiona lub model nie został jeszcze w pełni załadowany do pamięci. Upewnij się, że Ollama aktywnie działa (
ollama listw terminalu) i daj modelowi czas na inicjalizację. - Zbyt krótkie odpowiedzi od modelu: Jeśli w
SYSTEM_PROMPTwskazano "odpowiadaj zwięźle", model będzie przestrzegał tej instrukcji. Aby uzyskać bardziej rozbudowane odpowiedzi, zmień prompt systemowy, usuwając to ograniczenie lub wyraźnie prosząc o "daj rozbudowaną odpowiedź z przykładami". KeyboardInterruptpo naciśnięciu Ctrl+C: Jest to standardowe zachowanie Pythona. Dla projektu edukacyjnego nie jest to krytyczne, ale w środowisku produkcyjnym można opakowaćmain()wtry/except KeyboardInterruptdla bardziej eleganckiego zakończenia.- Problemy z kodowaniem (krzaczki w Windows): Można to rozwiązać, ustawiając kodowanie terminala na UTF-8, na przykład za pomocą komendy
$OutputEncoding = [System.Text.Encoding]::UTF8w PowerShell.
Następne kroki: w kierunku pełnoprawnego dialogu
Chociaż nasz konsolowy czat LLM stał się znacznie bardziej funkcjonalny, nadal nie ma "pamięci". Każde zapytanie jest przetwarzane niezależnie, a model nie pamięta poprzednich wypowiedzi dialogu. Oznacza to, że jeśli zapytasz "A możesz zrobić to samo, ale krócej?", model nie zrozumie, o co chodzi. W następnej części omówimy, jak zaimplementować historię wiadomości, przekazując cały kontekst dialogu w każdym nowym zapytaniu, co przekształci go w prawdziwie inteligentnego rozmówcę.
— Editorial Team
Brak komentarzy.