AI self-hosted
RAG na Ollama, vLLM i OpenWebUI z bazy wiedzy Confluence
Po migracji Confluence do Cloud mieliśmy w końcu jedną, uporządkowaną bazę wiedzy — setki stron dokumentacji technicznej, procedur, runbooków. Problem był prozaiczny: nikt tego nie czytał, bo wyszukiwarka Confluence radzi sobie dobrze z dokładnymi frazami, a fatalnie z pytaniami w stylu „jak skonfigurować VPN dla nowego oddziału”. Odpowiedzią był wewnętrzny system RAG (retrieval-augmented generation) podpięty pod czat w OpenWebUI — coś na kształt ChatGPT, ale odpowiadający na podstawie naszej dokumentacji, a nie ogólnej wiedzy z internetu.
Dlaczego self-hosted, a nie API OpenAI czy Anthropic
To było pierwsze pytanie, jakie padło, i słusznie. Odpowiedź jest prosta: w Confluence leżą dane, których nie możemy wysyłać do zewnętrznego API — topologia sieci, adresacja wewnętrzna, procedury bezpieczeństwa, czasem dane, które podlegają wewnętrznym politykom klasyfikacji informacji. Nie chodzi o nieufność wobec dostawców API — chodzi o to, że część dokumentacji z definicji nie powinna opuszczać naszej infrastruktury, punkt. Self-hosted LLM na własnym sprzęcie (serwer z GPU) rozwiązuje to jednoznacznie: żadne zapytanie, żaden fragment kontekstu nie wychodzi poza sieć firmową.
Architektura
Cały pipeline dzieli się na dwie osobne ścieżki, które łatwo pomylić na starcie: ingest (budowanie i aktualizacja indeksu) oraz serwowanie zapytań (retrieval + generacja odpowiedzi). Trzymanie ich jako niezależnych procesów było jedną z lepszych decyzji na starcie — ingest może się wywalić albo działać wolno, a czat dla użytkowników ma działać cały czas.
Confluence Cloud (REST API)
│
▼
ingest-worker (cron, co 30 min)
- pobiera zmienione strony (CQL: lastmodified > X)
- czyści HTML/makra Confluence → czysty tekst
- dzieli na chunki
│
▼
embedding model (bge-m3, przez Ollama)
│
▼
Qdrant (wektorowa baza danych)
│
▼
OpenWebUI ──retrieval──► Qdrant
│
▼
vLLM (serwowanie modelu głównego, np. Qwen2.5-32B-Instruct)
│
▼
odpowiedź + cytowane źródła (linki do stron Confluence)
Dlaczego Ollama i vLLM naraz
To pytanie, które zadaje sobie prawie każdy, kto zaczyna — po co dwa silniki inferencji. Odpowiedź: robią inne rzeczy dobrze. Ollama jest wygodna do modelu embeddingowego i do szybkich eksperymentów — jedna komenda, model ściągnięty i gotowy, minimalna konfiguracja. Ale jej throughput przy wielu równoległych zapytaniach (kilku użytkowników pytających czat jednocześnie) jest gorszy niż dedykowanego serwera inferencji. vLLM z kolei jest zbudowany właśnie pod ciągły, wielo-zapytaniowy ruch — continuous batching, PagedAttention, sensownie wykorzystuje pamięć GPU przy wielu jednoczesnych sesjach. Model generujący odpowiedzi w czacie stoi na vLLM, model embeddingowy (mniejszy, rzadziej odpytywany poza godzinami ingestu) zostaje na Ollama.
# uruchomienie vLLM z modelem głównym, ograniczonym kontekstem i quantyzacją AWQ
docker run --gpus all -p 8000:8000 \
-v /data/models:/models \
vllm/vllm-openai:latest \
--model /models/Qwen2.5-32B-Instruct-AWQ \
--quantization awq \
--max-model-len 16384 \
--gpu-memory-utilization 0.90
Ingest i chunking — tu jest najwięcej realnej pracy
Największym błędem na starcie było dzielenie dokumentów na chunki „na sztywno”, po stałej liczbie znaków. Strony Confluence z tabelami konfiguracyjnymi (np. lista portów i usług, macierz uprawnień) rozjeżdżały się w połowie tabeli — model dostawał połówkę tabeli jako kontekst i generował odpowiedzi, które brzmiały pewnie, a były po prostu niepełne.
Rozwiązaniem było chunkowanie strukturalne — dzielenie po nagłówkach Confluence (h2/h3) zamiast po liczbie znaków, i traktowanie tabel jako niepodzielnych bloków, nawet jeśli robi to chunk większym niż zakładany limit. Overlap między sąsiednimi chunkami (ok. 15% pokrycia) dodatkowo łagodzi przypadki, gdy istotna informacja i tak leży na granicy dwóch sekcji:
def chunk_confluence_page(html: str) -> list[str]:
sections = split_by_headings(html, levels=("h2", "h3"))
chunks = []
for section in sections:
if contains_table(section) and len(section) > MAX_CHUNK:
chunks.append(section) # tabela zawsze cała, bez cięcia
else:
chunks.extend(split_with_overlap(section, size=MAX_CHUNK, overlap=0.15))
return chunks
Aktualizacja indeksu — problem, który nie kończy się po wdrożeniu
RAG na statycznym zrzucie dokumentacji jest łatwy. Trudny jest RAG na dokumentacji, która zmienia się codziennie, bo ludzie normalnie edytują Confluence. Pierwsza wersja ingestu robiła pełny reindex co noc — działało, dopóki baza wiedzy była mała. Przy kilkuset stronach pełny reindex zaczął trwać zbyt długo i, co gorsza, marnował większość pracy na strony, które się nie zmieniły.
Zmiana na ingest przyrostowy oparty o CQL (lastmodified > "2026-09-20") i webhooki Confluence (event page_updated wywołujący od razu reindeks konkretnej strony) rozwiązała problem świeżości — teraz zmiana w Confluence trafia do indeksu w ciągu kilku minut, nie następnego dnia. Osobny problem to usuwanie: strona skasowana w Confluence musi też zniknąć z Qdrant, inaczej model odpowiada na podstawie danych, które już nie istnieją, i cytuje martwy link.
OpenWebUI jako warstwa czatu
OpenWebUI dało nam gotowy interfejs czatu, zarządzanie użytkownikami przez SSO, i — co ważne — wbudowaną obsługę RAG (podłączenie kolekcji dokumentów albo, jak w naszym przypadku, zewnętrznego API retrievalu z Qdrant przez pipeline). Nie musieliśmy pisać własnego frontendu czatu od zera, tylko skonfigurować connector do modelu w vLLM i pipeline retrievalu. To był świadomy wybór „zbuduj to, co odróżnia, kup/użyj gotowego to, co nie odróżnia” — interfejs czatu nie jest naszą przewagą, jakość dokumentacji i chunking są.
RAG nie jest magicznym rozwiązaniem na „AI, które wie wszystko o firmie”. To tyle warte, ile warta jest twoja dokumentacja i sposób, w jaki ją potniesz na kawałki.
Największa korzyść okazała się nie tam, gdzie się jej spodziewaliśmy — nie w oszczędności czasu doświadczonych administratorów, tylko w onboardingu nowych osób w zespole, które zamiast przeszukiwać dziesiątki stron Confluence, zadają pytanie w czacie i dostają odpowiedź z linkiem do dokładnego miejsca, gdzie mogą sprawdzić szczegóły.