Strona główna / Artykuły

Tworzenie asystenta Slack RAG bez oddzielnego bazę danych wektorowej

Asystent Slack RAG bez oddzielnej bazy danych wektorowej: dokumenty znajdują się w jednej tabeli StarRocks, wyszukiwanie odbywa się za pomocą zwykłego SQL, a sześć lekcji omawia prompty i dzielenie na fragmenty.

Wydawca: REACTAPP.TOP9 min czytania1961 słów

Źródła: Lessons We Learned Building a RAG Assistant Without a Separate Vector Database

StarRocks to szybka baza danych analityczna o otwartym kodzie źródłowym, wyposażona w zintegrowany silnik wykonywania, elastyczne możliwości wdrażania oraz wysoką wydajność przy rzeczywistych obciążeniach. W miarę rozwoju społeczności rosło również obciążenie wsparciem: administratorzy wciąż otrzymują te same pytania dotyczące dokumentacji, zadań na GitHubie, notatek o nowych wersjach oraz starych tematów na Slacku.

Rocky to asystent w Slacku stworzony, by przejąć to obciążenie. Jego zadanie jest proste: usunąć powtarzające się pytania i odpowiedzi z obciążenia administratorów, jednocześnie zapewniając, że każda odpowiedź opiera się na dokumentacji projektu oraz powiązanych źródłach.

Ciekawą cechą jest architektura. Rocky działa na samym StarRocks. Fragmenty dokumentacji, wyszukiwanie słów kluczowych, odzyskiwanie wektorów oraz ocena podobieństwa znajdują się wszystkie w jednej tabeli OLAP, dzięki czemu asystent odpowiadający na pytania o bazę danych również działa na tej samej bazie. Całość składa się z około 600 linii kodu w Pythonie, jednej tabeli oraz klucza API Gemini.

Podstawa RAG

Zwykłym pierwszym krokiem przy tworzeniu systemu generacji wzbogaconej o wyszukiwanie jest wprowadzenie dedykowanej bazy danych wektorowych. Każdy dodatkowy komponent wymaga odrębnego wdrożenia, planu awaryjnego oraz własnego modelu spójności, a w przypadku małego botu społecznościowego takie obciążenie jest trudne do usprawiedliwienia.

Dlatego wszystko pozostało w StarRocks. Fragmenty dokumentów oraz ich 768-wymiarowe embeddingi Gemini znajdują się w zwykłej tabeli OLAP wykorzystującej model PRIMARY KEY, a wyszukiwanie odbywa się za pomocą zapytania SQL. Zasada jest prosta: jeśli twoja baza danych analityczna już obsługuje kolumny tablicowe i funkcje podobieństwa kosinowego, nie potrzebujesz drugiego systemu danych do wyszukiwania wektorowego.

Tabela wygląda jak każda inna:

CREATE TABLE docs (
    id     BIGINT NOT NULL,
    path   VARCHAR(512),
    `index` INT,
    `text`  STRING,
    vector ARRAY<FLOAT>   -- 768-dim Gemini embedding
) ENGINE = OLAP
PRIMARY KEY(id)
DISTRIBUTED BY HASH(id) BUCKETS 1;

A wyszukiwanie odbywa się za pomocą jednego zapytania wykorzystującego podobieństwo kosinowe:

SELECT path, `index`, `text`,
       approx_cosine_similarity([0.012, -0.034, ...], vector) AS similarity
FROM docs
ORDER BY similarity DESC
LIMIT 8;

Nie ma SDK, żadnej biblioteki klienta ani drugiego systemu. Z perspektywy Rocky’ego „baza danych wektorowych” to po prostu kolejna tabela, którą może już przeszukiwać.

Od zdarzenia w Slack do konkretnej odpowiedzi

Proces jest celowo minimalny. Wspomnienie @Rocky w Slack uruchamia bota, który przekazuje dane do Gemini 3 Flash za pomocą natywnych wywołań funkcji. Model decyduje, czy przeszukać dokumentację, czy zapytać Google, i może przeprowadzić do dziesięciu rund wywołań narzędzi w jednej turze.

  1. Slack dostarcza informację o wspomnieniu. Biblioteka slack_bolt dla Pythona otrzymuje zdarzenie przez Socket Mode i wyodrębnia zapytanie użytkownika wraz z kontekstem wątku.
  2. Gemini rozumuje i wykorzystuje narzędzia. Model otrzymuje instrukcje systemowe z surowymi zasadami uczciwości oraz dwa narzędzia: search_starrocks_doc, służące do wyszukiwania wektorowego po stronie klienta, oraz google_search, umożliwiające powiązanie z rzeczywistością po stronie serwera za pomocą wbudowanego wyszukiwarki internetowej Gemini.
  3. Wyszukiwanie odbywa się wewnątrz StarRocks. Narzędzie wyszukiwania włącza zapytanie z użyciem gemini-embedding-001, ustawiając parametr task_type=RETRIEVAL_QUERY, a następnie uruchamia wcześniej przedstawiony zapytanie SQL do obliczania podobieństwa kosinowego.
  4. Model pisze odpowiedź. Gemini łączy pozyskane fragmenty tekstu w odpowiedź w formacie Markdown, którą Rocky konwertuje na format mrkdwn używany w Slacku przed jej opublikowaniem.
  5. Dane telemetryczne trafiają do systemu monitoringu. Każde wezwanie modelu, użycie narzędzia oraz liczbę tokenów staje się elementem struktury OpenTelemetry, przy czym thread_ts pełni rolę identyfikatora sesji.

Bot składa się z około 600 linijek kodu w Pythonie w jednym pliku. Narzędzia pomocnicze do dzielenia na fragmenty, generowania embeddingów oraz budowania indeksu dodają zaledwie kilka tysięcy linijek, a przechowywanie, wyszukiwanie i ocenianie podobieństwa są przekazywane do StarRocks.

Dlaczego ten zestaw nadaje się do lekkiej aplikacji RAG

Trzy rozwiązania pozwalają utrzymać niewielki wpływ na zasoby operacyjne, jednocześnie dostarczając przydatnych odpowiedzi.

Tabela klucza głównego w połączeniu z Stream Load do wymienialnych dokumentów

Korpus nie stanowi obciążenia typu strumieniowego. Gdy dokumenty się zmieniają, cały katalog docs/en/ jest ponownie dzielony na fragmenty, embeddingi regenerowane, a cała tabela ponownie ładowana za pomocą Stream Load:

curl --location-trusted -u root: \
     -H "format: json" \
     -H "strip_outer_array: true" \
     -T docs_with_embedding.json \
     -XPUT http://host:8030/api/starsage/docs/_stream_load

Jedna prośba HTTP ładuje około 5 000 wierszy, z których każdy zawiera tablicę liczb zmiennoprzecinkowych o 768 wymiarach. W takim skali Stream Load doskonale się sprawdza: nie ma konieczności utrzymywania brokera, nie trzeba uruchamiać pipeline’u Flink, a także nie wymaga stałej infrastruktury do pobierania danych. Dla tysięcy, a nawet dziesiątek tysięcy wierszy zawierających wektory to najprostsze rozwiązanie.

Model klucza głównego gwarantuje atomowe zastąpienie danych. Zapytania przychodzące w trakcie ładowania otrzymują spójny zrzut stanu, a nie częściowo zaktualizowany zbiór danych. To główny powód wyboru tabeli z kluczem głównym zamiast tabeli z kluczem duplikatowym. Nie chodzi tu o aktualizacje na poziomie poszczególnych wierszy, ponieważ obciążenie polega na całkowitej przebudowie danych. Klucz główny w połączeniu z Stream Load sprawia, że każde ładowanie jest atomowe – Rocky serwuje albo stary zbiór danych, albo kompletnie nowy, a nigdy ich mieszaninę.

Pobieranie danych jako narzędzie, a nie wypełnianie kontekstu

Dokumenty nie są wcześniej ładowane do interfejsu systemu. Funkcja search_starrocks_doc jest dostępna jako narzędzie, które można wywołać, a model sam decyduje, kiedy i jak często z niego korzystać.

W praktyce wiadomość taka jak „cześć Rocky” w ogóle nie powoduje żadnego wyszukiwania i nie kosztuje prawie nic. Złożone pytanie, np. o różnice w zachowaniu funkcji GROUP_CONCAT między wersjami 3.5 a 4.0, powoduje kilka wywołań: model sprawdza każdą wersję osobno i porównuje wyniki. Liczba wywołań wyszukiwania jest ograniczona do ośmiu na rundę. Serwerowa funkcja google_search w Gemini obsługuje notatki o nowościach oraz problemy na GitHubie poza dokumentacją, natomiast Rocky obejmuje szeroki zakres tematów bez konieczności używania oddzielnego mechanizmu scrapowania stron.

Bufor embeddingów dla przewidywalnych kosztów

Pytania społeczności skupiają się w określonych tematach. Kilka razy w tygodniu pojawiają się pytania o dodawanie indeksu bitmapowego oraz o wdrażanie rozwiązania na Kubernetes. Lokalna pamięć cache SQLite, klasyfikowana według tekstu zapytania i typu zadania (ponieważ RETRIEVAL_QUERY i RETRIEVAL_DOCUMENT generują różne wektory), eliminuje zbędne wywołania funkcji embedding, dzięki czemu popularne zapytania nie generują żadnych kosztów.

Z punktu widzenia modeli LLM, tokeny wejściowe przechowywane w cache Gemini są obliczane po cenie 25 procent standardowej stawki. Dzięki pamięci cache embedding koszt jednej odpowiedzi wynosi około 0,001 dolara, co jest zrównoważone dla projektu finansowanego przez społeczność, który działa nieprzerwanie.

Sześć lekcji z rzeczywistego ruchu

Pochodzą one z faktycznego użytkowania przez społeczność, a nie z testów benchmarkowych, i większość z nich ma zastosowanie w każdym asyście w platformach czatowych.

1. Zmiany w promptach są skuteczniejsze od optymalizacji procesu wydobywania informacji w walce z halucynacjami

W wczesnych wersjach wymyślano nazwy lustr Red Hat, tworzono fałszywe adresy URL z notatkami o wydaniu oraz zmyślane numery wersji, przez co członkowie społeczności trafiali na strony 404. Dla bota do pytań i odpowiedzi pewna błędna odpowiedź jest gorsza od żadnej.

Rozwiązaniem nie była lepsza wyszukiwarka informacji. Była to zmiana instrukcji: wyraźna klauzula dotycząca uczciwości, która nakazywała modelowi nie tworzyć fałszywych faktów, a gdy nie może odpowiedzieć pewnie, powinien powiedzieć, że nie jest pewien, wskazać autorytatywne źródła i zasugerować zapytanie społeczności. Efekt był natychmiastowy. Wyszukiwanie informacji jest ważne, ale inżynieria instrukcji jest często szybszym i tańszym sposobem na ograniczenie halucynacji.

2. Naprawa procesu dzielenia na fragmenty przed modyfikacją wyszukiwania

Dokumentacja StarRocks zawiera wiele linii importu MDX, tabel Markdown przecinających się między fragmentami oraz bardzo długie zapytania SQL DDL. Podawanie takich danych bezprzetworzonych do systemów embeddingów skutkuje błędnymi wynikami. W procesie dzielenia na fragmenty zainwestowano znaczne zasoby:

  • Hierarchiczne dzielenie według nagłówków. Fragmenty są dzielone przy nagłówkach Markdown, a cała ścieżka nagłówka jest przechowywana jako metadane, co pozwala modelowi zrozumieć, gdzie dany fragment należy w hierarchii dokumentu.
  • Filtrowanie szumów. Linie importu MDX są usuwane, a bloki kodu przekraczające 4000 znaków jest pomijanych, ponieważ zmniejszają semantyczny sygnał bez wpływu na jakość odpowiedzi.
  • Prostsze tabele wyjściowe. Przykładowe wyniki przedstawione jako tabele w stylu MySQL są sprowadzane do ustrukturyzowanych opisów zamiast surowych tabel ASCII.

Dzielenie tekstu na fragmenty zajęło mniej więcej dziesięć razy więcej czasu niż integracja wyszukiwania wektorowego. Jeśli wyniki wyszukiwania nie są zadowalające, sprawdź, w jaki sposób tekst został podzielony, zanim zaczniesz zmieniać modele embeddingów lub ustawienia indeksu.

3. RAG to narzędzie, a nie cały proces

Gdy pobieranie danych polega na wywołaniu funkcji, a nie jest wbudowane we każdą prośbę, model może je pominąć, gdy nie są potrzebne. Powitania, dodatkowe wyjaśnienia oraz proste pytania faktograficzne nigdy nie dotykają indeksu wektorowego. Połączenie narzędzia po stronie klienta (search_starrocks_doc) z narzędziem po stronie serwera (google_search), co jest w pełni obsługiwane przez SDK Gemini od wersji 1.70, zapewnia również Rocky możliwość użycia alternatywy dla treści spoza korpusu, gdy nie ma dostępnego specjalnego skrapera.

4. Postępuj zgodnie z modelem sesji platformy

Wątki w Slack posiadają identyfikatory thread_ts. Rocky wykorzystuje je bezpośrednio jako identyfikator sesji, przechowując historię w pliku JSON o nazwie odpowiadającej temu identyfikatorowi oraz zawierającym sześć najnowszych odpowiedzi w każdej prośbie:

contents = []
for turn in history[-MAX_CONTEXT_MESSAGES:]:
    contents.append({"role": turn["role"], "parts": [{"text": turn["text"]}]})
contents.append({"role": "user", "parts": [{"text": query}]})

To około dwudziestu wierszy kodu, co jest znacznie prostsze niż obsługa bazy danych sesji.

Jedna z częstych opinii użytkowników brzmi: „Rocky nie pamięta”. Wynika to z faktu, że w Slack Rocky odpowiada tylko wtedy, gdy jest o nim wspomniane @, co sprawia wrażenie, że jest zapominalski. To problem z interakcją z produktem, a nie techniczny.

5. Wdrożenie narzędzi wcześnie

Po uruchomieniu zespół chciał odpowiedzi na podstawowe pytania. Ile zapytań obsłużył Rocky w tym tygodniu? Gdzie koncentrowały się wywołania narzędzi według tematu? Ile kosztowała typowa odpowiedź? Które odpowiedzi zostały odrzucone, skorygowane lub wymagały dalszych działań? Bez śladów system był czarną skrzynką, dlatego później zaimplementowano pełną warstwę OpenTelemetry. Lekcją jest dodanie funkcji śledzenia przed uruchomieniem. Bez niej użycie tokenów, koszty oraz wzorce wywołań narzędzi pozostają niewidoczne, a optymalizacja kosztów, przypisywanie halucynacji oraz analiza słabych pytań stają się znacznie trudniejsze.

6. Agresywne wykorzystywanie pamięci cache i śledzenie każdego dolara

API do wgrania wydają się tanie za pojedynczy wywołanie, ale częste pytania społeczności szybko się kumulują. Pamięć cache do wgrania SQLite w połączeniu z systemem cenowym opartym na tokenach w Gemini utrzymuje koszt jednostkowy na poziomie około 0,001 dolara za odpowiedź. Dla projektu utrzymywanego przez społeczność oznacza to różnicę między „możliwością działania nieograniczenie” a „koniecznością omówienia budżetu”.

Uczciwe kompromisy

Rocky to lekki asystent typu RAG, a nie system wsparcia w produkcji, dlatego warto wymienić kilka ograniczeń:

  • Halucynacje są zmniejszane, ale nie eliminowane. Zasady bezpieczeństwa wprowadzane poprzez prompty pomagają, ale nie gwarantują poprawności. Najlepiej radzą sobie dobrze udokumentowane tematy, natomiast przypadki krawędziowe i specyficzne cechy poszczególnych wersji nadal powodują problemy.
  • Długie odpowiedzi. Podobnie jak większość botów typu RAG, ten również zbyt szczegółowo wyjaśnia. Planuje się dodanie opcji na krótkie, zwięzłe odpowiedzi w formie streszczenia.
  • Brak pamięci między wątkami. Kontekst istnieje tylko w obrębie jednego wątku w Slacku, więc użytkownik, który wczoraj pytał o widoki materializowane, zaczyna od zera dzisiaj.
  • Jeszcze brak eskalacji do człowieka. Po kilku nieudanych próbach system powinien automatycznie oznaczyć osobę odpowiedzialną za utrzymanie, ale ten proces nie został jeszcze wdrożony.
  • Ograniczone możliwości rozwiązywania problemów. Głębsza diagnostyka wymaga połączenia z narzędziem do debugowania o otwartym kodzie, które projekt niedawno opublikował.
  • Wszystkie te elementy znajdują się na aktualnej liście zadań i zostały opracowane na podstawie opinii społeczności.

    Co pokazuje Rocky

    Rocky to mały projekt – 600 linijek kodu w Pythonie, jedna tabela oraz jeden klucz API – ale służy do realizacji ważniejszych celów. W przypadku lekkich aplikacji AI baza danych analityczna z kolumnami tablicowymi i funkcjami podobieństwa może zastąpić osobną bazę wektorową. Jedna tabela zarządza przechowywaniem dokumentów, wyszukiwaniem według słów kluczowych oraz odzyskiwaniem wektorów, dzięki czemu złożoność operacyjna pozostaje niska, a do obsługi wystarcza jeden system, a nie dwa.

    Bot codziennie obsługuje prawdziwy ruch w społeczności, a według projektu użytkownicy oceniają go średnio na 4,2 na 5 punktów; koszt każdej odpowiedzi wynosi mniej niż jedną dziesiątą centa. Dla wszystkich, którzy tworzą podobnego asystenta w swojej dziedzinie i potrzebują prostej infrastruktury, model jednej tabeli jest godzien rozważenia, szczególnie jeśli w już używanym zestawie narzędzi znajduje się StarRocks.

    Literatura pokrewna

  • Hybrydowe szukanie obrazów w skali masowej: SigLIP, SPLADE, YOLOv8 i opisy Gemini — Projekt systemu do wyszukiwania miliona obrazów, który łączy gęste i rzadkie embeddingi z wykrywaniem obiektów, rozpoznawaniem entytetów, bezpiecznym wyszukiwaniem oraz opisami napisanymi przez LLM.