1. Dlaczego w ChatGPT App błędy to norma, a nie incydent krytyczny
W poprzednim wykładzie mówiliśmy, jak dzielić zadanie na kroki i budować wieloetapowy workflow w ChatGPT App. Teraz dodamy do tego schematu trzeźwą rzeczywistość: błędy, timeouty i przerwania przez użytkownika.
W klasycznym webie logika często budowana jest wokół „szczęśliwej ścieżki”, a błędy postrzegane są jako coś rzadkiego i awaryjnego — czerwona strona 500 itd. W ChatGPT App obraz jest inny: pracujecie w rozproszonym systemie z LLM, zewnętrznymi API, MCP, widżetem, a do tego z użytkownikiem, który może zamknąć kartę w dowolnym momencie. Błędy i przerwania to codzienna rutyna.
Jest kilka cech, które komplikują życie:
- Po pierwsze, LLM jest niedeterministyczna. Nawet przy identycznym promcie może podjąć nieco inną decyzję: wywołać inne narzędzie, zmienić parametry albo uznać, że lepiej „dopytać”.
- Po drugie, ograniczenia sieciowe i infrastrukturalne. Tool‑call z ChatGPT ma timeouty (zwykle kilkadziesiąt sekund), tak samo jak wasz backend Next.js/Vercel. Jeśli zewnętrzne API zwalnia, wszystko może urwać się w połowie.
- Po trzecie, jest czynnik UX: użytkownik się rozproszył, zamknął czat, wrócił po dniu, a wy nie możecie utrzymywać otwartej transakcji w bazie przez cały ten czas.
Stąd główna teza wykładu:
Odporny na awarie workflow = scenariusz, w którym z góry zakładamy, że każdy krok może się wyłożyć, i jawnie definiujemy, co wtedy się dzieje.
Błąd to nie tylko pretekst do pokazania komunikatu użytkownikowi, ale też sygnał dla modelu, który może zmienić strategię, zaproponować rollback, spróbować innego narzędzia albo bezpiecznie domknąć scenariusz.
2. Pejzaż błędów w workflow: jakie występują
Aby poprawnie obsługiwać awarie, trzeba najpierw nauczyć się je rozróżniać. W aplikacji LLM opartej na ChatGPT Apps typowo spotyka się kilka klas błędów.
Błędy techniczne. To cała klasyka systemów rozproszonych: timeouty sieciowe, 5xx od waszych lub zewnętrznych API, pad MCP‑serwera, bug w kodzie handlera narzędzia. Na przykład w GiftGenius wasz MCP‑tool search_products odwołuje się do katalogu, a ten odpowiada 503 Service Unavailable. To kandydat do automatycznego ponowienia (retry).
Błędy logiczne (modelowe). Tu wchodzą odmowy modelu (uznała, że żądanie narusza politykę), halucynacje lub np. zepsuty JSON przy odpowiedzi narzędzia. Model mógł wygenerować niepoprawne argumenty dla tool‑call i wasza walidacja JSON ich nie przepuściła. To najczęściej błąd danych wejściowych, a nie infrastruktury.
Błędy biznesowe. Chodzi o sens: produkt się skończył, budżet użytkownika jest zbyt mały dla wybranych filtrów, kod promocyjny jest nieważny, rezerwacja już wygasła. W GiftGenius to sytuacja „z 500 kandydatów żaden nie spełnia zadanych ograniczeń”. Tu retry rzadko pomaga: trzeba albo zmienić parametry, albo wytłumaczyć użytkownikowi, że ograniczenie jest nierealistyczne.
Przerwania UX. Użytkownik sam rwie scenariusz: zamyka ChatGPT, klika „Wstecz” w widżecie, anuluje akcję, zmienia odpowiedź na poprzedni krok. To także należy traktować jako normalny przepływ, a nie błąd. Ważne jest odtwarzanie i rollback stanu w takich przypadkach, o czym powiemy za chwilę.
Osobny problem na styku błędów logicznych i technicznych — nieskończone pętle agenta: model dostaje błąd, myśli „hmm, spróbuję jeszcze raz”, znów błąd i tak aż skończy się kontekst lub budżet. Obrona przed takim zachowaniem to ważna część projektowania obsługi błędów.
3. Podstawowe strategie: retry, fail‑fast, rollback, zaangażowanie użytkownika
Każdy błąd można traktować jako punkt rozgałęzienia: albo próbujemy powtórzyć krok, albo się wycofujemy, albo angażujemy użytkownika. I, co ważne, te strategie się łączą.
Dla awarii technicznych i przejściowych (sieć mignęła, API zwróciło 503) logiczne jest ograniczone retry z backoffem. Dla błędów logicznych i biznesowych („walidator nie przyjął budżetu”, „produkty się skończyły”) powtarzanie nie ma sensu — trzeba fail‑fast i poprosić użytkownika o zmianę danych lub parametrów.
Dla operacji, które już coś zmieniły w świecie zewnętrznym (utworzyły zamówienie, zrobiły rezerwację), potrzebny jest rollback — albo w postaci „kroku wstecz” w UI/kontekście, albo w postaci realnych działań kompensujących (anulowanie zamówienia, zwrot środków).
Wreszcie, są decyzje, które z założenia wymagają udziału użytkownika: np. przy odmowie systemu płatności z powodu „karta odrzucona przez bank” nie naprawicie tego automatycznie. Model powinien jasno wyjaśnić, co się stało, i zaproponować opcje: spróbować innej karty, obniżyć kwotę lub zrezygnować z zakupu.
Dla niezawodnego workflow bardzo pomaga wypisanie dla każdego kroku: jakie typy błędów są możliwe i co robicie w każdym z nich — auto‑retry, rollback, prośba do użytkownika lub po prostu log i zakończenie gałęzi.
4. Ponowienia (retry) i backoff: kiedy i jak je stosować
Zacznijmy od najbardziej naturalnej reakcji dewelopera: „No to po prostu spróbujmy jeszcze raz”. Sama idea jest poprawna, ale, jak zawsze, diabeł tkwi w szczegółach.
Jakie błędy warto ponawiać
Dobra heurystyka z praktyki integracji brzmi tak: błędy sieciowe i 5xx można spróbować ponowić z przerwą, a 4xx — raczej nie.
Czyli jeśli dostaliście 503, 504 albo po prostu nie doczekaliście się odpowiedzi z zewnętrznego API, powtórzenie żądania z niewielkim opóźnieniem ma sens. Jeśli zaś serwer zwrócił 400 Bad Request lub 422 Unprocessable Entity, najpewniej problem tkwi w danych i powtórzenie z tymi samymi parametrami nic nie zmieni.
Prosta utilka callWithRetry w TypeScript
Napiszmy małą utilkę dla warstwy MCP lub backendu, której można użyć w narzędziach:
type RetryOptions = {
maxRetries: number;
baseDelayMs: number;
};
async function callWithRetry<T>(
fn: () => Promise<T>,
{ maxRetries, baseDelayMs }: RetryOptions
): Promise<T> {
let attempt = 0;
// nie potrzebujemy nieskończonych pętli
while (true) {
try {
return await fn();
} catch (err: any) {
attempt++;
const status = err?.status ?? err?.response?.status;
// nie ponawiamy dla 4xx
const isClientError = typeof status === "number" && status >= 400 && status < 500;
if (attempt > maxRetries || isClientError) {
throw err;
}
const delay = Math.min(baseDelayMs * 2 ** (attempt - 1), 10_000);
// mała pauza, by nie uderzyć w API całym stadem
const jitter = Math.random() * 200;
await new Promise((r) => setTimeout(r, delay + jitter));
}
}
}
Ta funkcja:
- ponawia wywołanie fn ograniczoną liczbę razy;
- używa wykładniczego backoffu z niewielkim losowym szumem (jitter), aby uniknąć efektu „stada” przy jednoczesnych retry;
- przerywa ponowienia (retry) dla 4xx.
Dobrze jest używać jej np. wewnątrz MCP‑narzędzia, które odwołuje się do katalogu produktów lub do wewnętrznego API rekomendacji.
Gdzie dokładnie wykonywać retry
Częsty błąd — próbować ponawiać wszystkie żądania jak leci, także na poziomach, nad którymi nie macie kontroli. W ekosystemie ChatGPT macie kilka miejsc na retry:
- wewnątrz własnego backendu/MCP (jak zrobiliśmy w callWithRetry);
- wewnątrz pracownika w tle/kolejki (w przyszłych modułach omówimy dokładniej kolejki zadań i DLQ);
- czasem — w samym widżecie, gdy chodzi o lekkie żądanie „odśwież listę” bez efektów ubocznych.
Ważne, by nie dublować logiki: jeśli wasz worker zadań już robi trzy retry z backoffem, nie ma sensu dokładać kolejnych pięciu w widżecie. I oczywiście nigdy nie róbcie while(true) { try ... } — to pewny sposób na zrobienie sobie DDoS‑a.
5. Idempotencja kroków: ochrona przed duplikatami
Ponowienia tworzą drugi problem: jak nie wykonać dwa razy tej samej akcji. W świecie LLM to szczególnie dotkliwe: model może przypadkowo wywołać to samo narzędzie kilka razy, ChatGPT może powtórzyć tool‑call po timeoutcie, użytkownik może kliknąć „Regenerate”, a potem UI lub agent dorzucą jeszcze jedno wywołanie.
Idea idempotencji jest prosta: krok uznaje się za idempotentny, jeśli jego ponowne wykonanie z tymi samymi danymi wejściowymi nie tworzy dodatkowych efektów ubocznych. Pobranie product feed — ok, przeliczenie rekomendacji — ok, ale ponowne pobranie środków lub utworzenie drugiego zamówienia na te same dane — zdecydowanie nie ok.
Idempotency key w ChatGPT App
Klasyczny wzorzec: dla każdego logicznego kroku z efektami ubocznymi generujecie idempotency_key (zwykle UUID), przekazujecie go przez model do MCP‑narzędzia i tam przechowujecie mapowanie „klucz → wynik”. Jeśli narzędzie wywołano drugi raz z tym samym kluczem, nie powtarza działania, tylko zwraca już zapisany wynik.
W naszym GiftGenius jest krok create_order. Wyobraźcie sobie, że użytkownik kliknął przycisk „Zapłać”, model wywołał narzędzie, płatność przeszła, ale gdzieś po drodze odpowiedź zaginęła. Model albo platforma decydują o powtórzeniu wywołania i jeśli nie mamy idempotencji, dostaniemy duplikat zamówienia albo podwójne obciążenie.
Prosty przykład idempotentnego narzędzia w TypeScript
Zróbmy bardzo uproszczony handler MCP‑narzędzia create_order z kluczem idempotency. Dla prostoty użyjemy in‑memory Map; w prawdziwym życiu będzie to baza danych lub cache.
type CreateOrderInput = {
userId: string;
items: Array<{ sku: string; qty: number }>;
idempotencyKey: string;
};
type CreateOrderResult = { orderId: string; status: "created" };
const idempotencyStore = new Map<
string,
{ paramsHash: string; result: CreateOrderResult }
>();
export async function createOrderTool(input: CreateOrderInput): Promise<CreateOrderResult> {
const { idempotencyKey, ...rest } = input;
const paramsHash = JSON.stringify(rest);
const existing = idempotencyStore.get(idempotencyKey);
if (existing) {
// jeśli klucz już istniał, upewniamy się, że parametry się zgadzają
if (existing.paramsHash !== paramsHash) {
throw new Error("Idempotency key reuse with different params");
}
return existing.result;
}
// tutaj wykonujemy faktyczne utworzenie zamówienia i płatność
const result: CreateOrderResult = {
orderId: "order_" + Math.random().toString(36).slice(2),
status: "created",
};
idempotencyStore.set(idempotencyKey, { paramsHash, result });
return result;
}
Tu:
- wymagamy idempotencyKey w danych wejściowych narzędzia;
- przechowujemy z nim hash parametrów (tu dla prostoty JSON.stringify);
- przy ponownym wywołaniu z tym samym kluczem, ale innymi danymi — traktujemy to jako błąd;
- przy ponownym wywołaniu z tymi samymi danymi — zwracamy poprzedni wynik.
W realnym projekcie warto:
- trzymać klucze w bazie z TTL (by tabela nie urosła do nieba);
- logować idempotency_key i dołączać go do pól _meta komunikatów MCP, aby wygodnie śledzić w Inspectorze i na dashboardach.
6. Rollback kroków i wzorzec Saga
Idempotencja chroni przed duplikatami, ale nie rozwiązuje innego przypadku: co zrobić, jeśli któryś z kroków w środku scenariusza się wyłoży.
W e‑commerce to klasyczny problem: już utworzyliście zamówienie i zarezerwowaliście towar w magazynie, a na etapie płatności coś poszło nie tak. Nie możecie po prostu „o tym zapomnieć” — trzeba jakoś wycofać poprzedni stan.
Rollback logiczny vs techniczny
W ChatGPT‑workflow są dwa poziomy rollbacku.
Rollback logiczny — to powrót do poprzedniego kroku scenariusza i korekta kontekstu. Na przykład na kroku „płatność” wystąpił błąd i decydujecie się cofnąć do kroku „wybór metody płatności” albo nawet „wybór prezentu”. Wówczas ważne jest:
- zaktualizować WorkflowContext na backendzie (bieżący krok, wybrane parametry);
- poinformować model o zmianie kroku przez tool‑call/ToolOutput, aby „zapomniał” starą gałąź i dostosował dalsze zachowanie;
- zaktualizować UI widżetu, aby kroki i przyciski odpowiadały nowemu stanowi.
Rollback techniczny — to już poziom biznesowy: anulowanie utworzonych bytów, kompensacja efektów zewnętrznych. Przykładowo: anulować zamówienie, zdjąć rezerwację z magazynu, zainicjować zwrot płatności. To jest właśnie wzorzec Saga: dla każdego „ryzykownego” kroku z góry wymyślacie działanie kompensujące.
Schemat forward/compensate dla GiftGenius
Dla uproszczonego checkoutu GiftGenius możemy narysować taką sekwencję:
flowchart TD A[Krok 1: create_order] --> B[Krok 2: reserve_items] B --> C[Krok 3: charge_card] C -->|sukces| D[Status: completed] C -->|błąd| E[Kompensacja: cancel_reservation] E --> F[Kompensacja: cancel_order] F --> G[Status: failed + komunikat dla użytkownika]
Każdemu działaniu, które zmienia świat zewnętrzny (utworzenie zamówienia, rezerwacja, płatność), odpowiada działanie kompensujące (anulowanie zamówienia, zdjęcie rezerwacji, zwrot środków). Nie zawsze są symetryczne i nie zawsze możliwe jeden do jednego, ale ogólna zasada jest właśnie taka.
Mini‑przykład z kompensacją w kodzie
Spójrzmy na niewielki fragment kodu, który wykonuje te kroki:
async function completeCheckout(ctx: { userId: string }) {
const order = await createOrderInDb(ctx.userId);
try {
await reserveItems(order.id);
await chargeCard(order.id);
return { orderId: order.id, status: "paid" as const };
} catch (err) {
// działania kompensujące
await safeCancelReservation(order.id);
await safeCancelOrder(order.id);
throw err;
}
}
Tu:
- createOrderInDb, reserveItems, chargeCard — kroki forward;
- safeCancelReservation i safeCancelOrder — kroki kompensujące, które same w sobie powinny być idempotentne (jeśli spróbujemy anulować coś już anulowanego, nic złego się nie stanie).
Zwróćcie uwagę, że przy błędzie nie ukrywamy go, tylko propagujemy dalej. Model (przez ToolOutput) powinien dostać zrozumiały komunikat o błędzie i już w ludzkiej formie wyjaśnić to użytkownikowi oraz zaproponować kolejny krok.
7. Rollback kroków i synchronizacja stanu: jak uniknąć desynchronizacji
Jest szczególny rodzaj „błędu”, który łatwo niedoszacować: desynchronizacja stanu między UI, backendem i modelem.
Typowy scenariusz:
- Użytkownik przechodzi kroki 1 → 2 → 3.
- Na kroku 3 coś idzie nie tak, użytkownik naciska przycisk „Wstecz” w widżecie.
- Widżet uczciwie cofa swój lokalny stan do kroku 2.
- Ale model „pamięta”, że byliśmy na kroku 3 i już próbowaliśmy płacić. W następnym komunikacie dalej mówi o płatności, choć użytkownik widzi ekran wyboru prezentu.
Aby do tego nie dochodziło, warto wprowadzić jawne zdarzenie rollbacku kroku. Wysyła je widżet do MCP/modelu — albo jako wywołanie narzędzia, albo jako ToolOutput.
Na przykład można zrobić proste narzędzie user_navigated_to_step, które utrwala bieżący krok i jego stan:
type NavigateInput = {
workflowId: string;
stepId: string;
};
export async function userNavigatedToStep(input: NavigateInput) {
await workflowRepo.setCurrentStep(input.workflowId, input.stepId);
return {
message: `User moved to step ${input.stepId}`,
};
}
Widżet przy kliknięciu „Wstecz” wywołuje to narzędzie; model widzi jego wynik w historii tool‑calli i rozumie, że teraz należy kontynuować dialog, wychodząc z nowego kroku.
Po stronie UI będzie to mniej więcej taki handler:
async function handleBackClick() {
const { workflowId, prevStepId } = widgetState;
await window.openai.tools.call("user_navigated_to_step", {
workflowId,
stepId: prevStepId,
});
setWidgetState((s) => ({ ...s, currentStepId: prevStepId }));
}
Ważny punkt: to właśnie backend/agent jest źródłem prawdy co do bieżącego kroku, a model widzi go przez tools. Dzięki temu nawet przy późniejszym odtworzeniu sesji możecie poprawnie zsynchronizować kontekst.
8. UX błędów: co widzi użytkownik, a co widzi model
Już umiemy technicznie przetrwać błędy (retry, rollback, idempotencję, synchronizację stanu). Zostało sprawić, by wyglądało to sensownie zarówno dla użytkownika, jak i dla modelu.
Nawet idealnie zrealizowane retry i rollback nie pomogą, jeśli UX błędów będzie „jak w starych serwletach Java”: czerwony tekst, stack trace i zagadkowe „Unexpected error”.
Dla ChatGPT App są dwie grupy odbiorców komunikatu o błędzie:
- użytkownik, który musi zrozumieć, co się stało i co może zrobić dalej;
- model, który musi dostać wystarczająco ustrukturyzowaną informację, by podjąć decyzję: ponawiać, zmieniać parametry, proponować alternatywę czy kończyć scenariusz.
Dobra praktyka:
- na poziomie MCP/narzędzi zwracać ustrukturyzowany błąd z kodem, typem, flagą retryable i krótkim tekstem technicznym;
- dawać modelowi właśnie tę strukturę (np. w result.structuredContent), a nie kilometr stack trace;
- w UI pokazywać użytkownikowi ludzki, krótki komunikat.
Mini‑przykład struktury błędu zwracanej przez narzędzie:
type ToolError = {
code: string; // np. "PAYMENT_TIMEOUT"
message: string; // krótki opis techniczny
retryable: boolean; // czy można spróbować ponownie
};
throw {
isError: true,
error: <ToolError>{
code: "PAYMENT_TIMEOUT",
message: "Payment provider did not respond in time",
retryable: true,
},
};
Model widzi retryable: true i może spróbować innego narzędzia lub zaproponować użytkownikowi powtórkę.
Po stronie widżetu po prostu mapujecie te kody na zrozumiałe teksty dla użytkownika:
function ErrorBanner({ code }: { code: string }) {
const text =
code === "PAYMENT_TIMEOUT"
? "Usługa płatnicza nie odpowiedziała na czas. Spróbuj ponownie za minutę."
: "Coś poszło nie tak. Spróbuj ponownie.";
return <div className="error-banner">{text}</div>;
}
I jeszcze jedna ważna rzecz: nie pokazuj użytkownikowi stack trace’ów wyjątków, tokenów, sekretów. To i brzydkie, i niebezpieczne. Informację techniczną logujcie u siebie, a użytkownikowi dawajcie krótki, bezpieczny komunikat.
Insight
W systemach LLM, takich jak ChatGPT, niepoprawne wywołania narzędzi są raczej normą niż wyjątkiem. Model regularnie generuje argumenty, które nie przechodzą walidacji: pomylone typy, brakujące pola, nieprawidłowe wartości, popsute struktury. To nie błąd w zwyczajnym inżynierskim sensie — to część natury stochastycznego modelu i pod nią trzeba dostosować cały interfejs błędów.
Kluczowa idea: komunikat o błędzie to nie sygnał „zepsuło się”, lecz instrukcja naprawy następnej próby. Jego główną „publicznością” jest sam model. Jeśli komunikat jest ustrukturyzowany i zawiera precyzyjne wskazówki, model potrafi automatycznie skorygować parametry i powtórzyć wywołanie już poprawnie. Na tym opierają się techniki Tool‑Reflection: poprawna informacja zwrotna ulepsza kolejne działanie agenta bez udziału człowieka.
Zalecam trzymać się takich wymagań co do formatu błędów:
- komunikat powinien wskazywać konkretne pole, które nie przeszło walidacji — bez ogólników typu „Invalid parameters”;
- warto jawnie opisać oczekiwany format lub dozwolone wartości, by model mógł dobrać właściwe;
- komunikat powinien być krótki, formalny i ustrukturyzowany: pola takie jak error_type, field, expected czy allowed_values bardzo pomagają modelowi;
- jeśli to możliwe, podaj minimalny przykład poprawnego wejścia — często zwiększa to celność naprawy po stronie modelu.
Idealny feedback błędu dla modelu zawiera dwa fakty: co poszło nie tak oraz instrukcję jak to naprawić.
9. Logowanie i metryki błędów workflow
Nawet jeśli UX błędów jest dopracowany, aby zrozumieć, co realnie się psuje, same komunikaty dla użytkownika nie wystarczą. Potrzebne są logi strukturalne i metryki per krok.
Minimalnie przydatny zestaw przy logowaniu każdego kroku workflow:
- user_id albo przynajmniej session_id;
- workflow_id i step_id;
- status kroku (success, failed, retry, rolled_back);
- error_code (jeśli był);
- idempotency_key i correlation_id, jeśli krok jest powiązany z wywołaniami zewnętrznymi.
W MCP i Agents są pola _meta; wygodnie tam wkładać idempotency_key i correlation_id, by były widoczne i w logach, i w Inspectorze.
Najprostszy przykład logowania w Node.js/TypeScript (można użyć console, można — winston/pino):
function logStepFailure(params: {
userId?: string;
workflowId: string;
stepId: string;
errorCode: string;
idempotencyKey?: string;
}) {
console.error(
JSON.stringify({
level: "error",
event: "workflow_step_failed",
...params,
timestamp: new Date().toISOString(),
})
);
}
Takie logi łatwo parsować, budować na nich dashboardy i liczyć:
- konwersję między krokami;
- najczęstsze typy błędów;
- udział kroków zakończonych retry vs ostateczną porażką.
Nie każdy błąd powinien trafiać do alertów na produkcji. Krytyczne — pad MCP, systematyczne timeouty, masowe porażki na określonym kroku — tak, trzeba przenieść do monitoringu. A „brak wyników wyszukiwania prezentów” — to zdarzenie biznesowe, nie incydent.
10. Rozwijamy GiftGenius: odporny krok checkout
Zbierzmy teraz wszystko razem: retry, idempotencję, Sagę, synchronizację stanu, UX błędów i logowanie — na przykładzie jednego kroku w naszej aplikacji szkoleniowej GiftGenius — składania zamówienia.
Co już mamy
W tym momencie mamy już:
- wieloetapowy workflow: zbieranie informacji → dobór pomysłów → wybór prezentu → checkout;
- skonfigurowany tool gating: na kroku checkout dostępny jest tylko zestaw narzędzi commerce (create_order, get_payment_methods itd.);
- WorkflowContext, w którym przechowywany jest wybrany prezent, budżet, userId i bieżący krok.
Co dodamy na tym wykładzie
Dla kroku checkout wdrożymy:
- idempotency_key dla narzędzia create_order;
- retry przy przejściowych błędach dostawcy płatności;
- kompensację przy częściowo udanych operacjach;
- poprawny UX błędów w widżecie.
Generowanie klucza idempotency w widżecie po kliknięciu przycisku „Zapłać”:
import { v4 as uuid } from "uuid";
async function handlePayClick() {
const idempotencyKey = uuid();
setWidgetState((s) => ({ ...s, idempotencyKey }));
await window.openai.tools.call("create_order", {
userId: widgetState.userId,
items: [/* ... */],
idempotencyKey,
});
}
Po stronie narzędzia create_order — ten sam idempotentny handler, który pisaliśmy wyżej: przechowuje klucz i wynik, a przy powtórzeniu nie tworzy nowego zamówienia.
Kod integracji z API płatności można opakować w callWithRetry, aby kilka razy spróbować pobrać środki przy problemach sieciowych. I nie zapomnieć dodać flagi retryable: true w błędzie, by model rozumiał, że można zaproponować powtórkę.
Jeśli po pomyślnym utworzeniu zamówienia i obciążeniu karty coś się psuje (np. zewnętrzny webhook nie przychodzi na czas), logujemy to z correlation_id i workflow_id i dalej:
- próbujemy retry w tle (w przyszłym module o kolejkach i zdarzeniach);
- albo jawnie oznaczamy krok jako failed, wywołujemy działania kompensujące i wyjaśniamy użytkownikowi, co się stało.
11. Typowe błędy przy projektowaniu odpornych workflow
Błąd №1: „Ponawiamy wszystko, aż zadziała”.
Automatyczne powtarzanie każdego kroku do skutku — niezawodny sposób na urządzenie sobie lokalnego piekła. Błędy sieciowe i 5xx można próbować ponawiać z backoffem i limitem prób. Ale 4xx, błędy biznesowe i logiczne porażki modelu trzeba albo naprawiać danymi, albo tłumaczyć użytkownikowi. Inaczej dostaniecie niestabilne zachowanie, dziwne rachunki i zaśmiecone logi.
Błąd №2: Brak idempotencji tam, gdzie są pieniądze i zamówienia.
Jeśli narzędzie typu create_order lub charge_card nie jest idempotentne, każde ponowne wywołanie (z powodu timeoutu, Regenerate, buga w agencie) może prowadzić do duplikatów. W scenariuszach LLM powtórzenia zdarzają się znacznie częściej niż w klasycznym froncie REST, dlatego idempotency_key to nie „miły dodatek”, a warunek konieczny dla płatności i innych krytycznych kroków.
Błąd №3: Brak działań kompensujących (brak Sagi).
Utworzyliście zamówienie, zarezerwowaliście towar, a na płatności padło i po prostu pokazaliście użytkownikowi „coś poszło nie tak”. W efekcie w systemie wiszą pół‑zamówienia, rezerwy, „ogonki” finansowe. Dla każdego kroku, który zmienia świat zewnętrzny, warto przemyśleć, co zrobicie przy porażce kolejnego kroku: anulować, zwrócić, oznaczyć jako „expired” itd.
Błąd №4: Pozwolenie agentowi wejść w nieskończoną pętlę retry.
Jeśli nie ograniczycie liczby prób (np. przez maxRetries w helperach lub max_iterations w logice agenta) i nie będziecie oznaczać błędów jako retryable: false tam, gdzie retry nie ma sensu, model może się zapętlić: „Spróbuję jeszcze raz… i jeszcze…”. To pali tokeny, czas i nerwy.
Błąd №5: Desynchronizacja stanu między UI a modelem przy rollbacku.
Często deweloperzy implementują przycisk „Wstecz” tylko w UI, zapominając zsynchronizować krok z backendem i modelem. W efekcie użytkownik widzi krok 2, a model dalej żyje na kroku 3 i proponuje dziwne rzeczy. Rozwiązanie — jawne zdarzenia typu user_navigated_to_step i aktualizacja WorkflowContext przy każdym przejściu.
Błąd №6: Techniczne komunikaty dla użytkownika i brak logów dla deweloperów.
Użytkownik dostaje „Error: ECONNRESET at TcpSocket.onEnd…”, a wy — zero informacji o tym, który dokładnie krok i dla jakiego workflow_id się wyłożył. Prawidłowe podejście: dla użytkownika — krótki, zrozumiały tekst i propozycja, co zrobić dalej; dla dewelopera — log strukturalny z workflow_id, step_id, error_code, idempotency_key i correlation_id.
Błąd №7: Brak strategii alertowania.
Albo alertuja wszystko jak leci, łącznie z „brak odpowiednich prezentów pod bardzo wąski filtr”, albo nie alertują nic, łącznie z realnym upadkiem MCP. Starajcie się odróżniać krytyczne awarie systemowe (pad serwisu, masowe timeouty, utrata webhooków) od oczekiwanych zdarzeń biznesowych. Pierwsze idą do monitoringu i on‑call, drugie — po prostu liczone są w analityce.
GO TO FULL VERSION