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

Odporność agenta konwersacyjnego: Sekwencyjna kolejka, idempotencja SRS i akumulacyjny sync do Anki

#ai-agents#openclaw#telegram#sqlite#anki#spaced-repetition#anti-slop

Wdrażanie autonomicznego agenta edukacyjnego na komunikatorze Telegram szybko ujawnia ograniczenia prostych architektur promptowych. Gdy użytkownik korzysta z bota na co dzień - ponawia pytania w trakcie marszu, wysyła wiadomości seriami lub oczekuje zwięzłych paczek powtórkowych - pojedyncze pęknięcia w obsłudze stanu potrafią całkowicie zepsuć doświadczenie.

Problem: Gdy bot gubi kontekst, zalewa mikropaczkami i bredzi clickbaitem

W toku codziennych testów i rozmów z botem językowym ujawniły się cztery konkretne wąskie gardła:

  • Wyścigi i brak atomowego wycofania błędnych wiadomości: Gdy użytkownik ponawiał zapytanie (np. po uciętej odpowiedzi) albo wysyłał dwa pytania jedno po drugim, bot potrafił sklejać wątki sprzed kilku dni z bieżącymi pytaniami lub traktować ponowienie jako „podwójną wysyłkę” i odmawiać odpowiedzi.
  • Ryzyko desynchronizacji bazy SRS: Ponowne wygenerowanie lekcji niosło ryzyko powtórnego dodania słówek do bazy fiszek lub nadpisania dotychczasowego postępu interwałów powtórkowych (poziom, nastepna_powtorka).
  • Zaśmiecanie Anki mikrotaliami: Codzienny cron o 23:55 generował i wysyłał paczkę .apkg bez względu na liczbę nowych słówek. Przy mniej aktywnych dniach użytkownik otrzymywał powiadomienie i plik dla zaledwie jednej nowej fiszki, co tworzyło niepotrzebny szum na telefonie i rozbijało strukturę talii.
  • Tani clickbait i AI-slop w narracji: Szablon promptu (**Mocny fakt z cienia:**) sprawił, że model zaczął dosłownie wklejać tę frazę jako nagłówek, dokładając sztuczny suspens („forteca, o której nikt nie wie wszystkiego”) oraz zakazane długie myślniki em-dash. Brzmiało to jak tani generator nagłówków z mediów społecznościowych, a nie autentyczna opowieść bystrego kumpla-podróżnika.

Co zrobiłem: Architektura odporności, idempotencji i buforowania

Rozwiązanie problemów wymagało przejścia z poziomu „dopracowywania promptu” na poziom twardej inżynierii: mechanizmu kolejki, dedykowanego narzędzia MCP, rygorystycznego schematu bazy danych i akumulacyjnego bufora synchronizatora.

1. Narzędzie MCP do atomowego czyszczenia Telegrama (tg_usun_ostatnia)

Aby obsłużyć ponawianie wiadomości bez pozostawiania śmieci na czacie, stworzyłem dedykowane narzędzie w Node.js, zintegrowane przez protokół Model Context Protocol (MCP):

// core/tg_delete.js - atomowe usuwanie ostatniej wiadomości bota
export async function tgUsunOstatnia(chatId, token) {
  const lastMsgId = await getLastBotMessageId(chatId);
  if (!lastMsgId) return { ok: false, reason: 'no-target' };
  
  const res = await fetch(`https://api.telegram.org/bot${token}/deleteMessage`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ chat_id: chatId, message_id: lastMsgId })
  });
  return { ok: res.ok };
}

W konfiguracji bramki OpenClaw przełączyłem kolejkowanie wiadomości w tryb followup z debounceMs = 0. Gdy uczeń napisze „ponów od nowa”, bot najpierw usuwa z czatu swoją poprzednią nieudaną wypowiedź, a dopiero potem generuje kompletną lekcję od zera.

2. Dwuwarstwowa idempotencja SRS w SQLite

Zabezpieczyłem bazę danych przed duplikatami i resetem postępów na dwóch niezależnych poziomach:

  • Poziom bazy danych (SQLite): Unikalny indeks es TEXT NOT NULL UNIQUE COLLATE NOCASE. Baza fizycznie odrzuca wstawienie tego samego hiszpańskiego słowa, ignorując wielkość liter.
  • Poziom logiki wtyczki (_dodaj): Zanim zapytanie INSERT zostanie wysłane, funkcja sprawdza obecność karty w bazie. Jeśli hasło istnieje, zwraca { dup: true } i istniejący obiekt karty.

Żadne parametry algorytmu powtórek - poziom opanowania, data kolejnej powtórki, licznik błędów - nie są modyfikowane przy próbie ponownego zapisu.

3. Bufor akumulacyjny dla Anki (próg minimum 10 fiszek)

Zamiast generować miniaturowe paczki codziennie, przebudowałem skrypt daily_spanish_anki_sync.py w bufor akumulacyjny z progiem min_cards = 10:

# daily_spanish_anki_sync.py - sprawdzanie progu przed budową paczki
cards = collect_unexported_cards(state)

if len(cards) < args.min_cards and not args.force:
    print(f"Oczekuje {len(cards)} fiszek w buforze (próg: {args.min_cards}).")
    print("Fiszki pozostają w buforze do kolejnej sesji.")
    return 0

# Próg osiągnięty - generujemy paczkę .apkg i aktualizujemy rejestr stanu
apkg_file, tsv_file = generate_package(cards, deck_name="PabloSpanishHQ")
send_telegram(apkg_file, count=len(cards))
update_sync_state(cards)

Gdy w danym dniu do bazy wpadną 2-3 słówka, nie są wysyłane ani oznaczane jako zużyte. Czekają w bazie do momentu, aż łączna liczba niezaimportowanych słówek osiągnie 10. Dodatkowo wszystkie karty kierowane są do jednej głównej talii PabloSpanishHQ z tagami sesji (sesja_YYYY_MM_DD), co eliminuje anty-wzorzec tworzenia dziesiątek mikrotalii w aplikacji mobilnej.

4. Twarde guardraile narracyjne w plikach SOUL.md i AGENTS.md

Usunąłem szablonowe etykiety promptów, które model traktował dosłownie. Do plików zasad bota wprowadziłem twarde zakazy:

  • Kategoryczny zakaz formułek typu: „Mocny fakt z cienia”, „Tajemnica, o której nikt nie wie”, „Sekret skrywany przez wieki”.
  • Nakaz opowiadania historii ze swadą, prostym językiem, z naciskiem na inżynierię dawnych cywilizacji (mury obronne w zygzak, naturalne chłodzenie spichlerzy wiatrem, zalanie doliny rzeką podczas bitwy) lub autentyczne patenty kulinarne.
  • Zakaz długich pauz em-dash () na rzecz standardowego krótkiego myślnika (-).
  • Organiczne wplatanie hiszpańskich zwrotów (la muralla en zigzag, desviar el río) bezpośrednio w tok zdania wraz z polskim tłumaczeniem w nawiasie.

Dlaczego to ważne (Use Case)

Wprowadzone zmiany przekładają się na trzy kluczowe korzyści:

  • Brak zniecierpliwienia i szumu: Użytkownik nie dostaje spamu na Telegramie dla pojedynczych słówek, a po zaimportowaniu paczki w Anki nie musi przeklikiwać się przez labirynt folderów.
  • Bezpieczeństwo danych edukacyjnych: Awaria połączenia, zrestartowanie bota czy ponowienie pytania nie uszkadza bazy powtórek. Stan algorytmu SRS pozostaje deterministyczny i stabilny.
  • Wysoka kultura językowa odpowiedzi: Eliminacja generatorowego slopu i clickbaitu sprawia, że bot brzmi jak prawdziwy, oczytany człowiek, budując zaufanie i zachęcając do regularnej nauki.

Czego się nauczyłem

  1. Model nie może odpowiadać za integralność stanu: Idempotencja i unikalność danych muszą być egzekwowane na poziomie bazy danych i kodu narzędzi (SQLite constraints, warunek check-before-insert), a nie w instrukcjach językowych promptu.
  2. Szablony w promptach są pułapką dosłowności: Jeśli w instrukcji umieścisz punktor - **Mocny fakt z cienia:**, model prędzej czy później użyje go jako nagłówka. Znacznie lepsze efekty daje opisanie pożądanego tonu, celu wypowiedzi oraz jawna lista anty-wzorców zakazanych.
  3. Harmonogramy czasowe (cron) powinny weryfikować próg partii: Sztywne generowanie eksportu co 24 godziny prowadzi do powstawania pustych lub szczątkowych artefaktów. Logika synchronizacji powinna działać w oparciu o próg akumulacji (batch threshold).