Jak przyspieszyłem reakcję agentów AI w AOC: od powolnego sprawdzania bazy do lekkich procesów w tle
W projekcie Agent Operations Center (AOC) buduję panel do zarządzania pracą roju autonomicznych agentów AI - od planowania zadań, przez pisanie kodu, aż po audyty bezpieczeństwa i testy.
Cały przepływ w teorii wygląda prosto: wchodzę do panelu w przeglądarce, zatwierdzam zadanie albo zmieniam jego status na tablicy Kanban, a system powinien od razu uruchomić odpowiedniego bota do pracy.
Problem: Dlaczego ciągłe odpytywanie bazy danych to ślepa uliczka
Na początku zrobiłem to najprościej, jak się dało: ustawiłem w kodzie zegar (pętlę setInterval), który co kilka sekund zaglądał do bazy SQLite i pytał: „Czy pojawiło się jakieś nowe zadanie do zrobienia?”.
Taki sposób (w żargonie IT nazywany pollingiem) przypomina sytuację, w której co pięć sekund pytasz kuriera przez domofon, czy już przyjechał z paczką, zamiast po prostu poczekać, aż zadzwoni dzwonkiem do drzwi.
W praktyce szybko zaczęło to przeszkadzać z dwóch powodów:
- Denerwujące opóźnienie (lag): Klikasz w panelu przycisk „Uruchom”, a agent startował dopiero po 3-5 sekundach, bo musiał poczekać na kolejny cykl zegara.
- Marnowanie zasobów: Nawet gdy nikt nie korzystał z systemu przez pół dnia, serwer w kółko bezsensownie odpytywał bazę danych i zapełniał logi tysiącami pustych zapytań.
Co zrobiłem: Natychmiastowe odpalenie w tle zamiast czekania
Zamiast pytać w kółko bazę danych, odwróciłem tę logikę na model sterowany zdarzeniami (Event-Driven): uruchamiamy agenta dokładnie w tym ułamku sekundy, w którym użytkownik kliknie przycisk w panelu.
Pojawił się jednak typowy problem aplikacji webowych: przeglądarka wysyła zapytanie do serwera i czeka na odpowiedź. Jeśli w tym samym zapytaniu każemy modelowi AI analizować pliki i pisać kod przez 30 sekund, przeglądarka użytkownika po prostu „zawiśnie”.
Rozwiązaniem okazał się mechanizm odczepionego procesu potomnego (detached: true) oraz metoda child.unref() wbudowana w Node.js.
1. Lekki broker zadań z child.unref()
import path from "node:path";
import { execFile } from "node:child_process";
/**
* Uruchomienie brokera zadań w tle zaraz po kliknięciu użytkownika.
* Odpala proces i natychmiast zwraca kontrolę do przeglądarki,
* dzięki czemu interfejs działa błyskawicznie i bez żadnych zacięć.
*/
export function triggerBroker(): void {
if (process.env.NODE_ENV === "test") return;
const scriptPath = path.join(process.cwd(), "scripts", "process-commands.mjs");
// 1. Odpalamy skrypt jako niezależny proces w tle
const child = execFile("node", [scriptPath], {
detached: true,
stdio: "ignore"
});
// 2. Mówimy serwerowi: "Zwróć odpowiedź do użytkownika, nie czekaj na zakończenie skryptu"
child.unref();
}
Co robi ta jedna linijka child.unref()? Mówi serwerowi: „Wyślij użytkownikowi informację 'Przyjęto do realizacji', a ten proces w tle niech spokojnie wykonuje swoje obliczenia własnym tempem”. Użytkownik widzi natychmiastową reakcję w ułamku sekundy.
2. Porządek w rolach: 8 wyspecjalizowanych agentów zamiast jednego bota od wszystkiego
Kolejnym krokiem był podział obowiązków. Jeden bot, który miałby znać się na wszystkim, gubi się w zbyt długich instrukcjach i zaczyna halucynować. Podzieliłem pracę tak, jak w zespole programistycznym:
pm(Product Lead) - rozbija pomysły na małe zadania techniczne i pilnuje tablicy Kanban.coder-backend- pisze logikę serwerową i obsługuje bazę danych.coder-frontend- buduje interfejs w Next.js i dba o wygląd oraz wygodę użytkownika.coder(Fullstack) - łączy frontend z backendem i tworzy szybkie prototypy.security(AppSec) - sprawdza kod pod kątem luk bezpieczeństwa i złośliwych promptów.qa(Tester) - pisze testy automatyczne, by upewnić się, że nowe funkcje niczego nie zepsuły.dba / ops- dba o bazę danych i bezpieczne kopie zapasowe.digest-reviewer- niezależny recenzent weryfikujący jakość pracy pozostałych botów.
Dlaczego to ważne (Use Case)
Dlaczego w tym przypadku warto było zrezygnować z gotowych, ciężkich kolejek zadań (jak Redis czy RabbitMQ) na rzecz prostego child.unref()?
- Znakomity czas reakcji dla użytkownika: Czas od kliknięcia „Akceptuj” na tablicy Kanban do rozpoczęcia pracy przez agenta spadł z 4 sekund do zaledwie 50 milisekund.
- Zerowy narzut infrastruktury: Cały system działa na pojedynczym małym serwerze VPS. Baza SQLite w nowoczesnym trybie WAL (Write-Ahead Logging) pozwala na jednoczesny odczyt danych przez stronę WWW i bezkolizyjny zapis postępów przez bota działającego w tle.
- Brak kosztów utrzymania dodatkowych usług: Zamiast płacić za klastry kolejek i skomplikowane kontenery, wykorzystaliśmy proste, sprawdzone narzędzia systemowe.
Czego się nauczyłem
- Najprostsze rozwiązania są często najlepsze: Zanim postawisz ciężką infrastrukturę (RabbitMQ, Redis), sprawdź możliwości wbudowane w Twój runtime. Funkcja
child.unref()w Node.js załatwiła temat w kilku linijkach kodu. - Wąska specjalizacja bije ogólny prompt: Zamiast tworzyć jednego "mądrego" bota ze stustronicową instrukcją, znacznie lepiej sprawdza się zespół wyspecjalizowanych ról z krótkimi, konkretnymi zadaniami.
- Prawdziwa architektura to kultura pracy z danymi: Zastosowanie SQLite w trybie WAL dało stabilność transakcyjną, której nie powstydziłyby się znacznie większe bazy danych.