Przejdź do głównej zawartości

Wzorce odzyskiwania dla agentów AI to receptury, w których Claude Code, Codex lub Cursor chronią jedną niestabilną zależność za pomocą utrzymywanej biblioteki (opossum, cockatiel, Resilience4j lub tenacity) zamiast ręcznie pisanej maszyny stanów: circuit breakerem, ponowieniem, które nie pogłębia awarii, i uporządkowanym fallbackiem. Działanie każdego z nich potwierdza test kontraktu, który celowo otwiera obwód.

Ta strona jest dla programistów, którzy odpowiadają za usługę z kruchym wywołaniem zależności, i dla tech leadów, którzy decydują, co musi udowodnić pull request dodający odporność. Twój dostawca płatności ma 90-sekundową awarię o 2 w nocy. Każde żądanie checkoutu czeka na martwą zależność, pula połączeń się wyczerpuje i pada cała aplikacja, nie tylko płatności. Poproszony o „dodanie circuit breakera” agent pisze 200 linii class CircuitBreaker, których nikt nie testuje. Poniższe receptury dają mu zamiast tego nazwaną bibliotekę, nazwane wywołanie i test do spełnienia.

  • Kontrakt odporności zapisany jako test, zanim agent zmieni wywołanie, więc „breaker się otwiera” i „błąd 400 nigdy nie jest ponawiany” to wynik zaliczony albo nie
  • Pięć promptów do skopiowania: test kontraktu, circuit breaker z opossum, ponowienie z cockatiel nałożone na breaker, fallback ścieżki odczytu i review gotowego diffu
  • Dokładne polecenia do wykonania tej pracy w Claude Code i Codeksie oraz odpowiednik w Cursorze
  • Pętlę z serwerem Sentry MCP, która wybiera wywołanie do ochrony na podstawie danych z produkcji, a nie zgadywania
  • Sposoby, w jakie napisany przez agenta kod odpornościowy przechodzi review i pogłębia awarię, z wyjściem z każdego z nich

Którego wzorca odzyskiwania potrzebuje to wywołanie?

Dział zatytułowany „Którego wzorca odzyskiwania potrzebuje to wywołanie?”

Decyduj dla każdego wywołania osobno, nie dla całej usługi. Wzorzec zależy od tego, czy wywołanie odczytuje, czy zapisuje, i czy jego powtórzenie jest bezpieczne.

WywołanieWzorzecBiblioteka w NodeSprawdzenie, które to dowodzi
Wolna lub martwa zależność na ścieżce żądaniaCircuit breaker z timeoutem i fallbackiemopossum 10.0.0Breaker otwiera się po N błędach, fallback działa, próba half-open go zamyka
Idempotentny odczyt (GET, wyszukiwanie po ID)Ponowienie z backoffem z jitterem, nałożone na breakercockatiel 4.0.05xx jest ponawiane, 4xx nigdy, łączna liczba wywołań ma limit
Nieidempotentny zapis (POST /charge)Breaker i kolejka; żadnych ślepych ponowieńopossum plus twoja kolejka zadańTen sam klucz idempotencji trafia do dostawcy przy każdej próbie
Odczyt, dla którego istnieje zdegradowana odpowiedźUporządkowany fallback: główna baza, replika, cache, wartość domyślnaZwykłe funkcje asynchroniczne404 jest propagowane; dalej przechodzą tylko błędy infrastruktury

Wersje to tagi latest z npm sprawdzone 2026-09-26. Na JVM wskaż Resilience4j i jego adnotację Spring Boot @CircuitBreaker(name = ..., fallbackMethod = ...). W Pythonie wskaż tenacity (9.1.4) do ponowień i pybreaker (1.4.1) do breakera. Właśnie wskazanie biblioteki z nazwy powstrzymuje agenta przed pisaniem własnej.

Circuit breaker, którego nigdy nie otworzyłeś w teście, to circuit breaker, który debugujesz w trakcie incydentu. Najpierw napisz kontrakt, zobacz, jak nie przechodzi na niechronionym wywołaniu, i dopiero wtedy pozwól agentowi zmienić kod. Kontrakt jest tym, co recenzent czyta zamiast diffu.

Test steruje breakerem przez fabrykę, która przyjmuje nadpisania opcji. Ta decyzja projektowa ma znaczenie: test przekazuje resetTimeout: 50 i używa prawdziwych timerów, bo sztuczne timery mogą zablokować asynchroniczne I/O zaślepki HTTP i zawiesić test.

Przypadki 5 i 6 to te, które agenci pomijają. Timeout w opossum odrzuca wywołanie i uruchamia fallback, ale nie anuluje żądania, więc dostawca może dokończyć obciążenie, które twój kod już wysłał do kolejki fallbacku. opossum potrafi przerywać żądania przez AbortController, ale w opossum 10.0.0 ten kontroler jest wspólny dla całego breakera: jeden timeout przerywa wszystkie obciążenia w toku, a autoRenewAbortController odnawia go dopiero wtedy, gdy breaker przechodzi w half-open albo się zamyka, więc kolejne wywołania startują z już przerwanym sygnałem. Przypadek 6 to wyłapuje; sygnał tworzony osobno dla każdego wywołania go przechodzi. Nawet przerwane żądanie mogło dotrzeć do dostawcy, więc to klucz idempotencji sprawia, że ponowienie z kolejki jest nieszkodliwe.

Gdy kontrakt jest czerwony, implementacja to mała zmiana o jasnych granicach. Prompt jest taki sam dla każdego narzędzia; różni się sposób uruchomienia.

Okno terminala
# Terminal, repository root. Flags checked against Claude Code 2.1.283; the
# v2.1.285 change is from the Claude Code changelog (checked 2026-10-12).
# claude -p starts in Manual mode (auto mode on third-party providers or with
# telemetry off, from v2.1.285); --permission-mode dontAsk denies anything that
# would prompt, on every provider. Reads and read-only commands still run.
claude -p "$(cat prompts/payment-breaker.txt)" \
--permission-mode dontAsk \
--allowedTools "Read" "Edit" "Write" "Bash(npm test *)"

Zapisz poniższy prompt jako prompts/payment-breaker.txt. Najpierw sam zainstaluj opossum, żeby agent nie potrzebował dostępu do sieci i nie mógł dodawać pakietów. Reguła Bash(npm test *) nie ogranicza tego, co uruchamiają testy: npm test wykonuje to, co wpisano w package.json, a Write może ten plik zmienić. Przejrzyj w diffie każdą zmianę package.json albo dodaj --disallowedTools "Edit(package.json)" "Write(package.json)".

Wynik powinien być krótki. Z opossum to konfiguracja, filtr i fallback, bez żadnej maszyny stanów:

import CircuitBreaker from 'opossum';
export function createPaymentBreaker({ charge, enqueue }: PaymentDeps, overrides = {}) {
const breaker = new CircuitBreaker(
// A signal per call: aborting it cancels this charge and no other
(order: Order, key: string) =>
charge(order, key, { signal: AbortSignal.timeout(2500) }),
{
name: 'payments.charge',
timeout: 3000, // backstop above the per-call abort
errorThresholdPercentage: 50,
volumeThreshold: 20, // never open on a handful of requests
resetTimeout: 30000,
errorFilter: (err: HttpError) => err.status >= 400 && err.status < 500,
...overrides,
},
);
// opossum calls the fallback with the same arguments, so the key is reused
breaker.fallback(async (order: Order, key: string) => {
await enqueue('payments', { order, idempotencyKey: key });
return { status: 'queued' as const };
});
for (const state of ['open', 'halfOpen', 'close'] as const) {
breaker.on(state, () => recordBreakerState(breaker.name, state));
}
return breaker;
}
// In the route: breaker.fire(order, order.idempotencyKey)

opossum 10.0.0 nie dostarcza typów TypeScript, a @types/opossum jest w wersji 8.1.9 (oba sprawdzone 2026-09-26). Opcja nowsza niż typy, na przykład coalesce, nie przejdzie sprawdzania typów, choć opossum ją odczytuje. Każ agentowi rozszerzyć typ lokalnie, zamiast rzutować cały obiekt opcji na any.

Naiwne ponowienia mnożą obciążenie w czasie częściowej awarii: trzy ponowienia na każdym kliencie to samodzielnie wywołany skok ruchu na zależność, która już ledwo działa. Zapobiegają temu dwie zasady. Backoff z jitterem sprawia, że klienci nie ponawiają w tym samym rytmie, a ścisły predykat sprawia, że 400 ani nieidempotentny POST w ogóle nie są ponawiane.

Nałóż ponowienie na zewnątrz breakera, żeby każde ponowienie liczyło się do współczynnika błędów breakera, a długotrwała awaria go otwierała. W cockatiel robi to jedna linia: wrap(retryPolicy, breakerPolicy). Jeden szczegół psuje to połączenie: jeśli breaker ma fallback, ponowienie nigdy nie widzi błędu i nigdy nie ponawia. Umieść fallback na zewnątrz obu.

Zapis płatności zostaw poza tą recepturą. POST /charge dostaje breaker i kolejkę, a worker kolejki ponawia z tym samym kluczem idempotencji. Jeśli twój dostawca płatności nie przyjmuje klucza idempotencji, w ogóle nie ponawiaj obciążenia automatycznie.

Przy odczytach łańcuch fallbacków utrzymuje funkcję przy życiu, gdy główne źródło leży: główna baza, potem replika do odczytu, potem cache oznaczony jako nieaktualny, potem bezpieczna wartość domyślna. Każdy poziom musi być oznaczony w odpowiedzi, inaczej wdrażasz cichą nieaktualność. Zostaw to jako uporządkowaną listę funkcji asynchronicznych, a nie framework FallbackChain<T>.

Reguła ponownego rzucenia odróżnia odporność od fabryki błędów. Fallback, który połyka uzasadnione 404 i serwuje profil z cache, jest gorszy niż awaria, którą miał ukryć.

Niech błędy z produkcji wskażą wywołanie do ochrony

Dział zatytułowany „Niech błędy z produkcji wskażą wywołanie do ochrony”

Bez danych z produkcji agent zgaduje, które wywołanie jest niestabilne. Podłącz serwer Sentry MCP, a agent przeczyta prawdziwe zgłoszenie (stack trace, liczbę zdarzeń, zawodzący span), zanim zaproponuje breaker. Serwer Sentry jest zdalny, więc nie ma czego instalować; między narzędziami różni się tylko rejestracja. Pełna konfiguracja tylko do odczytu, opcje tokenów i zakresy są na stronie Serwery MCP do obserwowalności.

Okno terminala
# Checked against Claude Code 2.1.283
claude mcp add -s project --transport http sentry https://mcp.sentry.dev/mcp
claude mcp login sentry # or run /mcp inside a session

Na ekranie OAuth zostaw Triage Issues odznaczone: agent ma tu czytać zgłoszenia, a nie je zamykać. Jeśli wolisz gotowy skill od gołego serwera, plugin Sentry (sentry@claude-plugins-official lub codex plugin add sentry@openai-curated dla Codeksa, sprawdzone z pomocą 0.157.1; wymaga zalogowanego Codeksa) łączy ten sam serwer ze skillem sentry-debug-issue. Zainstaluj jedno albo drugie, nie oba, bo inaczej serwer zarejestruje się dwa razy.

Przed podłączeniem „dodaj circuit breaker do checkoutu” daje breaker wokół wywołania, które tylko wygląda na ryzykowne. Po podłączeniu:

Jak udowodnić, że kod odzyskiwania działa, bez czytania każdej linii?

Dział zatytułowany „Jak udowodnić, że kod odzyskiwania działa, bez czytania każdej linii?”

Pull request dodający odporność jest mały, ale jego tryb awarii ujawnia się dopiero podczas incydentu. Przeglądaj dowody, nie diff:

  1. Test kontraktu: czerwony na starym kodzie, zielony na nowym. Obejmuje otwarcie obwodu, filtr 4xx, timeout, próbę half-open, klucz idempotencji i wywołanie następujące po timeoucie.
  2. Test liczby ponowień: sprawdza dokładną liczbę wywołań dla 503, 404 i otwartego obwodu. Polityka ponowień bez asercji na liczbę wywołań psuje się po cichu: przechodzi każdy inny test, a mnoży obciążenie.
  3. Grepy z pustym wynikiem: git grep -nE "class [A-Za-z]*(CircuitBreaker|Retry|Backoff)" src/ nic nie zwraca, więc obok biblioteki nie wślizgnęła się ręcznie napisana maszyna stanów.
  4. Metryka i alert: circuit_breaker_state_change istnieje, a alert odpala, gdy breaker pozostaje otwarty. Wklej do opisu pull requesta jedną prawdziwą linię metryki z lokalnego uruchomienia.
  5. Jedna zainscenizowana awaria: przed pierwszym wdrożeniem na produkcję wstrzyknij awarię na stagingu przez proxy, na przykład Toxiproxy, i obserwuj otwarcie breakera na dashboardzie. Metoda jest opisana w testowaniu przez wstrzykiwanie błędów.

Autor zatwierdza kroki 1–4 w pull requeście. Właściciel usługi lub lider dyżuru zatwierdza krok 5 i progi, bo to on odpowiada za czas odtworzenia, który te progi kodują. Żeby sprawdzić gotowy diff według tej listy, uruchom prompt review w nowej sesji:

  • Breaker otwiera się przy chwilowym zakłóceniu. volumeThreshold w opossum domyślnie wynosi 0, więc przy errorThresholdPercentage: 50 jeden błąd na jedno żądanie wystarczy, żeby otworzyć obwód. Wyjście: ustaw volumeThreshold na liczbę żądań, którą przekracza nawet spokojna minuta ruchu, a resetTimeout opieraj na zaobserwowanym czasie powrotu zależności (prompt z Sentry powyżej), nie na okrągłej liczbie.
  • Ponowienia zamieniają spowolnienie w awarię. Ponowienie wewnątrz breakera albo brak breakera pozwala każdemu klientowi dalej zasypywać zależność żądaniami. Wyjście: wrap(retry, breaker) z ponowieniem na zewnątrz, backoff z jitterem i test sprawdzający łączną liczbę wywołań.
  • Ponowienie nigdy nie następuje. Fallback zarejestrowany na breakerze zamienia każdy błąd w sukces, więc otaczające go ponowienie nie ma czego ponawiać. Wyjście: przenieś fallback na zewnątrz ponowienia i breakera i zachowaj test liczby ponowień.
  • Fallback maskuje prawdziwy błąd. Fallback łapiący wszystko zamienia 400, błędy autoryzacji i brakujące rekordy w ciche, błędne odpowiedzi. Wyjście: errorFilter w opossum dla 4xx, jawny predykat isRetryable dla cockatiel i test, że 404 jest propagowane.
  • Timeout obciąża klienta dwa razy. Breaker przekracza timeout, fallback kolejkuje zamówienie, a pierwotne żądanie i tak kończy się u dostawcy. Wyjście: daj każdemu wywołaniu własny AbortSignal.timeout(), nieco krótszy niż timeout w opossum, zamiast wspólnego sygnału breakera z getSignal(), oraz wysyłaj ten sam klucz idempotencji w pierwotnym wywołaniu i w zakolejkowanym.
  • Otwarty breaker ukrywa awarię. Gdy fallback działa, odsetek błędów widocznych dla użytkowników wygląda dobrze i nikt nie dostaje powiadomienia, choć zależność leży całkowicie. Wyjście: alertuj na metrykę stanu breakera i na licznik fallbacków, a nie tylko na odsetek 5xx. Kierowanie takiego alertu opisują wzorce monitoringu i logowania.
  • Agent zgaduje API biblioteki. Nazwy opcji różnią się między opossum, cockatiel i starszymi forkami, a typy mogą nie nadążać za pakietem. Wyjście: rozstrzyga test kontraktu; podłącz serwer MCP z dokumentacją, na przykład Context7, żeby agent czytał dokumentację zainstalowanej wersji.

Czy ten artykuł Ci pomógł?

Jedno kliknięcie, bez logowania.

wcale bardzo