Odporność agenta konwersacyjnego: Sekwencyjna kolejka, idempotencja SRS i akumulacyjny sync do Anki
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ę
.apkgbez 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 zapytanieINSERTzostanie 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
- 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.
- 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. - 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).