RAG Chatbot NeoGadżet — inteligentny asystent sklepu z elektroniką
Projekt Python · RAG · Flask · ChromaDB · OpenAI

RAG Chatbot NeoGadżet — zaawansowany pipeline retrieval-augmented generation

Inteligentny asystent dla fikcyjnego sklepu z elektroniką, zbudowany na zaawansowanym czterostopniowym pipeline RAG: przepisywanie zapytania przez LLM, podwójne wyszukiwanie wektorowe, LLM-reranking i generowanie odpowiedzi z cytatami źródeł. Osadzony jako popup chatbot na stronie sklepu — Flask + ChromaDB + GPT-4.1-nano + LiteLLM.

Python 3.10+ Flask 3.0 ChromaDB OpenAI GPT-4.1-nano LiteLLM text-embedding-3-large Pydantic v2 tenacity

Projekt powstał jako dowód koncepcji: jak podłączyć chatbota opartego na LLM do własnej bazy wiedzy firmy — i zrobić to lepiej niż prymitywny RAG. Zamiast prostego “weź embedding zapytania → wyszukaj → wyślij do GPT”, pipeline zawiera cztery etapy, z których każdy poprawia jakość odpowiedzi: przepisanie zapytania, podwójne wyszukiwanie, LLM-reranking i asemblowanie kontekstu ze źródłami.

Baza wiedzy to zestaw plików Markdown opisujących produkty (AeroSound X2, HomeCam Mini 2K, WiLink AX1800, FitTime Pro), regulaminy dostawy, politykę zwrotów, gwarancję i FAQ — 10 dokumentów, które LLM dzieli na chunki z nakładaniem (overlap ~25%) i własnoręcznie generowanymi nagłówkami i streszczeniami.

01Pipeline RAG — cztery etapy

To co wyróżnia ten projekt to fakt, że każde pytanie przechodzi przez cztery oddzielne wywołania LLM zanim użytkownik zobaczy odpowiedź. Każdy etap jest niezależny, ma retry z exponential backoff i structured output przez Pydantic.

KROK 01

Query Rewriting

LLM (GPT-4.1-nano) przepisuje pytanie użytkownika na krótkie, precyzyjne zapytanie do bazy wiedzy uwzględniając historię rozmowy.

KROK 02

Dual Retrieval

Wyszukiwanie wektorowe w ChromaDB dla oryginalnego i przepisanego pytania — K=20 wyników dla każdego, merge z deduplikacją.

KROK 03

LLM Re-ranking

Drugi LLM call: model ocenia trafność każdego chunka i zwraca posortowaną listę ID przez structured output (Pydantic RankOrder).

KROK 04

Answer Generation

Top-10 chunków jako kontekst, trzeci LLM call generuje odpowiedź. Frontend pokazuje źródła (plik, typ, fragment).

Dlaczego Dual Retrieval? Oryginalne pytanie użytkownika bywa potoczne (“ile płacę za przesyłkę?”), przepisane jest precyzyjne (“koszty dostawy kurierem i paczkomatem”). Oba wyszukiwania wyłapują inne chunki, merge ich łączy bez duplikatów — pokrycie bazy wiedzy jest znacznie szersze niż przy jednym zapytaniu.

02Ingest — LLM-based chunking

Większość tutoriali RAG używa prostego splitu po X znakach lub zdaniach. Ten projekt robi inaczej: LLM sam dzieli dokumenty na chunki, generując dla każdego fragmentu nagłówek (headline), streszczenie (summary) i oryginalny tekst (original_text). Wynik zapisywany do ChromaDB to konkatenacja tych trzech pól — wyszukiwanie trafia na nagłówki dopasowane semantycznie do typowych pytań użytkownika.

class Chunk(BaseModel):
    headline:      str  # "Nagłówek prawdopodobnie będzie użyty w zapytaniu"
    summary:       str  # kilka zdań streszczenia
    original_text: str  # oryginalny fragment dokumentu

    def as_result(self, document):
        # wektor jest tworzony z headline + summary + original_text
        # → embedding "rozumie" kontekst, nie tylko surowy tekst
        return Result(
            page_content=self.headline + "\n\n" + self.summary + "\n\n" + self.original_text,
            metadata={"source": document["source"], "type": document["type"]}
        )

Przetwarzanie dokumentów jest zrównoleglone przez multiprocessing.Pool(processes=3) z progress barem (tqdm). Każde wywołanie ma retry z exponential backoff przez dekorator @retry(wait=wait_exponential(min=10, max=240)) — odporność na rate limiting OpenAI przy bulk processingu.

03LLM Re-ranking — zamiast modelu cross-encoder

Klasyczny reranking w RAG używa dedykowanego modelu (np. cross-encoder z Hugging Face). Tutaj zastosowałem inne podejście: LLM jest poproszone o posortowanie chunków według trafności i zwrócenie listy ich ID przez response_format=RankOrder. Structured output przez Pydantic zapewnia, że model nie może zwrócić “na oko” — musi podać konkretną listę liczb całkowitych w poprawnej strukturze JSON.

class RankOrder(BaseModel):
    order: list[int] = Field(
        description="Kolejność istotności fragmentów, od najbardziej do najmniej istotnych"
    )

def rerank(question, chunks):
    # prompt z numerowanymi chunkami → LLM sortuje → Pydantic waliduje
    response = completion(model=MODEL, messages=messages,
                          response_format=RankOrder, timeout=30, max_retries=0)
    order = RankOrder.model_validate_json(reply).order

    # filtracja błędnych indeksów + fallback na oryginalne chunks
    valid_chunks = [chunks[i-1] for i in order if 1 <= i <= len(chunks)]
    return valid_chunks if valid_chunks else chunks  # bezpieczny fallback

Zabezpieczenie edge case: jeśli model zwróci pustą listę lub indeksy poza zakresem, funkcja wraca do niesortowanych chunków. Żaden błąd modelu nie powoduje awarii — odpowiedź zawsze dociera do użytkownika.

04Architektura systemu

Strona NeoGadżet HTML + CSS + JS popup chat widget Flask App app.py · sessions /api/chat · /api/health Baza Wiedzy 10 plików .md produkty · polityki · FAQ RAG Pipeline (answer.py) rewrite_query → dual_retrieval → rerank → generate ChromaDB PersistentClient vector_db/ (lokalnie) ingest.py LLM chunking · Pool(3) embedding-3-large OpenAI API GPT-4.1-nano (LiteLLM) text-embedding-3-large

05Interfejs — sklep + popup chatbot

Frontend to statyczny HTML/CSS/JS renderowany przez Jinja2 (Flask). Strona symuluje prawdziwy sklep z elektroniką NeoGadżet — działający header z nawigacją, hero z floating card produktu, siatka kategorii (Audio, Kamery, Sieć, Wearables, Komputery, Smart Home), 4 karty produktów z cenami i przyciskami koszyka, sekcja benefitów (dostawa, zwroty, gwarancja, wsparcie 24/7) i stopka.

Chatbot siedzi jako popup widget w prawym dolnym rogu — przycisk z ikoną i badge “1 nowa wiadomość”. Kliknięcie otwiera okno czatu z:

  • Avatar robota i status “Online” z zieloną kropką
  • Sugerowane pytania — 3 przyciski-skróty (“Koszt dostawy?”, “Jak zwrócić?”, “AeroSound X2 ANC?”)
  • Wiadomości z źródłami — po odpowiedzi AI frontend renderuje listę plików-źródeł z typem (products/policies/faq) i fragmentem tekstu
  • Historia sesji — Flask trzyma ostatnie 20 wiadomości per session_id w pamięci serwera
  • Przycisk czyszczenia historii wywołujący POST /api/clear

Pełna separacja: chatbot jest w 100% oddzielony od reszty strony — można go wpiąć do dowolnego projektu HTML zmieniając tylko adres backendu w chat.js. Sklep NeoGadżet to tylko scenografia.

06Struktura projektu

rag_chatbot_neogadzet/ ├── app.py # Flask — serwer, endpointy, sesje │ ├── implementation/ │ ├── answer.py # ★ RAG pipeline (4 etapy) │ │ ├── rewrite_query() # LLM przepisuje pytanie │ │ ├── fetch_context() # dual retrieval + merge │ │ ├── rerank() # LLM re-ranking (RankOrder) │ │ └── answer_question() # generowanie odpowiedzi │ └── ingest.py # ★ pipeline ingestion │ ├── fetch_documents() # ładowanie plików .md │ ├── process_document() # LLM chunking → Chunk(headline, summary, text) │ ├── create_chunks() # Pool(3) multiprocessing │ └── create_embeddings() # text-embedding-3-large → ChromaDB │ ├── knowledge-base/ │ └── sections/ # 10 plików .md │ ├── produkty_katalog.md # AeroSound X2, HomeCam Mini 2K, WiLink AX1800… │ ├── dostawa_i_koszty.md # paczkomat 12.99zł, kurier 16.99zł, darmowa od 199zł │ ├── zwroty.md # polityka 14-dniowych zwrotów │ ├── reklamacje.md # procedura reklamacyjna + rękojmia │ ├── gwarancja_producenta.md │ ├── platnosci.md │ ├── bezpieczenstwo_konta.md │ ├── o_firmie.md │ ├── slownik_pojec.md # definicje: rękojmia, reklamacja, BLIK… │ └── przyklady_testowe.md │ ├── templates/ │ └── index.html # strona sklepu + popup chat widget │ ├── static/ │ ├── style.css # projekt UI sklepu (Inter, indigo #6366F1) │ └── chat.js # logika widgetu (fetch, render, sesja) │ ├── vector_db/ # ChromaDB persistent storage (lokalnie) ├── requirements.txt # zależności Python └── .env example # OPENAI_API_KEY, SECRET_KEY, PORT

07REST API

GET
/
strona główna sklepu — renderuje index.html przez Jinja2
POST
/api/chat
wysłanie wiadomości → pipeline RAG → odpowiedź + lista źródeł (plik, typ, fragment 200 znaków)
POST
/api/clear
wyczyszczenie historii sesji (session_id z JSON body)
GET
/api/health
status: liczba dokumentów w ChromaDB, nazwa kolekcji, timestamp
GET
/api/suggest
lista 8 sugerowanych pytań do chatbota (statyczna)

Przykładowa odpowiedź /api/chat

{
  "response": "Dostawa do paczkomatu InPost kosztuje 12.99 zł...",
  "sources": [
    {
      "type": "sections",
      "source": "knowledge-base/sections/dostawa_i_koszty.md",
      "content": "Koszty dostawy: Paczkomat InPost - 12.99 zł, Kurier DPD..."
    }
  ],
  "session_id": "user123",
  "timestamp": "2026-04-21T20:15:32.000Z"
}

08Kluczowe decyzje techniczne

🔄

LiteLLM jako wrapper

Wywołania LLM przez LiteLLM zamiast bezpośrednio przez SDK OpenAI — łatwa podmiana modelu (GPT, Claude, Mistral) bez zmiany kodu.

🧩

Structured output + Pydantic

response_format=Chunk i response_format=RankOrder — LLM musi zwrócić walidowalny JSON. Zero parsowania regex, zero halucynacji formatu.

Multiprocessing ingest

Pool(processes=3) z imap_unordered — dokumenty przetwarzane równolegle, tqdm pokazuje postęp. Retry chroni przed rate limitingiem.

💾

ChromaDB persistent

PersistentClient(path="vector_db") — baza wektorowa zapisana na dysku. Restart serwera nie wymaga re-embeddingu.

📝

Sesje in-memory

Dict chat_sessions w pamięci Flask — proste, bez bazy danych. Historia ograniczona do 20 wiadomości, session_id jako klucz.

🛡️

Tenacity retry

@retry(wait=wait_exponential, stop=stop_after_attempt(3)) na każdej funkcji LLM — odporność na chwilowe błędy API bez crashu całego pipeline.

09Czego dowodzi ten projekt

Advanced RAG

Nie tutorial, lecz produkcyjny wzorzec: query rewriting, dual retrieval, LLM reranking z fallbackiem.

Prompt engineering

Trzy różne systemy promptów dla trzech ról: chunker, reranker i asystent — każdy z inną specyfiką.

Structured outputs

Pydantic jako kontrakt między LLM a kodem — model nie może zwrócić malformowanego JSON.

Reużywalność

Wymiana bazy wiedzy (inne pliki .md) i modelu (string w config) — chatbot dla dowolnej firmy.

Wbudowana odporność

Retry na każdym LLM callu, fallbacki na edge case, error handlery w Flask — nie crashuje na pierwszym błędzie API.

Integracja e-commerce

Osadzony chatbot w działającej stronie sklepu — nie demo CLI, lecz realistyczny kontekst produktowy.

Zobacz pełny kod na GitHubie

Repozytorium zawiera pełny pipeline RAG, bazę wiedzy NeoGadżet (10 plików .md), frontend sklepu i konfigurację środowiska.

Zobacz kod na GitHub

10Aplikacja w akcji

Poniżej wideo pokazujące pipeline w działaniu — zadawanie pytań chatbotowi, wyświetlanie źródeł i logowanie kolejnych kroków RAG w konsoli.

· · ·
Udostępnij jeśli spodobał Ci się mój projekt