PABLO.ENGINEERING
// WRÓĆ DO WSZYSTKICH WPISÓW
// 4 min

Jak przyspieszyłem reakcję agentów AI w AOC: od powolnego sprawdzania bazy do lekkich procesów w tle

#aoc#architecture#ai-agents#event-driven#typescript#sqlite#nodejs

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:

  1. pm (Product Lead) - rozbija pomysły na małe zadania techniczne i pilnuje tablicy Kanban.
  2. coder-backend - pisze logikę serwerową i obsługuje bazę danych.
  3. coder-frontend - buduje interfejs w Next.js i dba o wygląd oraz wygodę użytkownika.
  4. coder (Fullstack) - łączy frontend z backendem i tworzy szybkie prototypy.
  5. security (AppSec) - sprawdza kod pod kątem luk bezpieczeństwa i złośliwych promptów.
  6. qa (Tester) - pisze testy automatyczne, by upewnić się, że nowe funkcje niczego nie zepsuły.
  7. dba / ops - dba o bazę danych i bezpieczne kopie zapasowe.
  8. 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

  1. 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.
  2. 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.
  3. 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.