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.
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.
Query Rewriting
LLM (GPT-4.1-nano) przepisuje pytanie użytkownika na krótkie, precyzyjne zapytanie do bazy wiedzy uwzględniając historię rozmowy.
Dual Retrieval
Wyszukiwanie wektorowe w ChromaDB dla oryginalnego i przepisanego pytania — K=20 wyników dla każdego, merge z deduplikacją.
LLM Re-ranking
Drugi LLM call: model ocenia trafność każdego chunka i zwraca posortowaną listę ID przez structured output (Pydantic RankOrder).
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
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_idw 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
07REST API
strona główna sklepu — renderuje
index.html przez Jinja2wysłanie wiadomości → pipeline RAG → odpowiedź + lista źródeł (plik, typ, fragment 200 znaków)
wyczyszczenie historii sesji (
session_id z JSON body)status: liczba dokumentów w ChromaDB, nazwa kolekcji, timestamp
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 GitHub10Aplikacja 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.