Agent zrobił to samo dwa razy

przez Łukasz | sie 24, 2026

Objawy

Ten syndrom różni się od pozostałych jedną rzeczą: objaw widzi klient, nie ty.

Trzy identyczne wiadomości w skrzynce. Dwa zamówienia zamiast jednego. Podwójne obciążenie. Duplikaty rekordów, które ktoś zauważa tydzień później.

W twoim dzienniku wszystko wygląda poprawnie — jedno wywołanie, jeden wynik. Bywa nawet gorzej: log mówi, że operacja się nie udała, a w systemie docelowym jest wykonana.

Ten rozjazd między tym, co widzisz, a tym, co się wydarzyło, jest głównym sygnałem rozpoznawczym.

Mechanizm w trzech zdaniach

W pętli agenta powtórzenie jest regułą, nie wyjątkiem: ponowienia po błędach, zapętlenie modelu, restart procesu, równoległe wywołania, ponowne uruchomienie zadania. W klasycznej aplikacji ścieżki wykonania są przewidywalne — tutaj nie są.

Operacja, która nie jest odporna na powtórzenie, prędzej czy później zostanie powtórzona. Pojęcie porządkujące to idempotencja, a warstwę wykonawczą opisuje artykuł o awariach narzędzi.

Najpierw ustal, gdzie powstaje duplikat

Trzy miejsca, każde z inną naprawą.

Na wejściu — to samo zadanie trafiło do agenta dwa razy. W pętli — agent wywołał narzędzie więcej niż raz. W transporcie — wywołanie było jedno, dotarło dwa razy albo dotarło mimo zgłoszonego błędu.

Rozstrzyga to porównanie trzech liczb za ten sam okres: ile zadań weszło, ile wywołań narzędzia jest w twoim logu, ile operacji ma system docelowy.

Diagnostyka różnicowa

1. Ponowienie po przekroczeniu czasu po stronie klienta

Jak rozpoznać: log mówi „timeout”, system docelowy ma rekord. Liczba operacji w systemie docelowym jest wyższa niż liczba udanych wywołań w twoim logu.

Dlaczego to najgroźniejsze: operacja się wykonała, tylko odpowiedź nie wróciła. Z perspektywy pętli to nieudane wywołanie, więc ponawia. Nie ma sposobu, żeby odróżnić „nie zadziałało” od „zadziałało, ale nie wiem” bez pytania systemu docelowego.

Test: dla kilku spraw z timeoutem sprawdź ręcznie, czy operacja istnieje po drugiej stronie.

2. Ciche ponowienie na poziomie biblioteki

Jak rozpoznać: liczba wywołań w twoim kodzie i w logu dostawcy się różni, a ty nigdzie nie napisałeś pętli ponowień.

Dlaczego się dzieje: SDK, klient HTTP albo framework agentowy ponawiają domyślnie. Zwykle rozsądnie — dopóki operacja nie ma skutków ubocznych.

Test: wypisz faktyczną konfigurację ponowień w używanych bibliotekach, zamiast zakładać, że jest wyłączona.

3. Zapętlenie modelu

Jak rozpoznać: w kontekście widać kilka wywołań tego samego narzędzia z tymi samymi argumentami — świadomych, nie technicznych.

Dlaczego się dzieje: to pierwszy syndrom z konsekwencjami w świecie. Jeśli narzędzie ma skutki uboczne, każde powtórzenie jest osobną operacją.

Test: policz wystąpienia narzędzia w śladzie pojedynczego przebiegu.

4. Restart bez trwałego rejestru

Jak rozpoznać: duplikaty pojawiają się seriami, skorelowanymi z wdrożeniami albo restartami procesu. Dotyczy agentów działających w tle.

Dlaczego się dzieje: agent długo żyjący po restarcie zaczyna od zera. Wszystko, czego nie zapisał trwale, jest dla niego niewykonane.

Test: sprawdź, czy rejestr wykonanych operacji przeżywa restart i czy zapis następuje przed operacją, nie po.

5. Równoległe wywołania z jednej odpowiedzi

Jak rozpoznać: duplikaty mają identyczny znacznik czasu co do sekundy.

Dlaczego się dzieje: model poprosił o kilka wywołań naraz, wśród nich dwa takie same albo różniące się nieistotnie. Pętla wykonała je współbieżnie.

Test: sprawdź, czy zrównoleglasz wywołania i czy obejmuje to narzędzia zapisujące.

6. Brak deduplikacji na wejściu

Jak rozpoznać: liczba zadań, które weszły, jest wyższa niż liczba zgłoszeń od użytkowników.

Dlaczego się dzieje: użytkownik kliknął dwa razy, kolejka dostarczyła wiadomość ponownie, webhook przyszedł podwójnie. Agent zadziałał poprawnie — dwa razy, bo dwa razy go poproszono.

Test: porównaj liczby na wejściu z liczbą realnych zgłoszeń.

7. Wiele instancji tego samego agenta

Jak rozpoznać: duplikaty pojawiły się po skalowaniu poziomym albo po dodaniu drugiego środowiska.

Dlaczego się dzieje: dwie instancje pobrały to samo zadanie z kolejki, bo nie ma blokady ani znacznika przetwarzania.

Test: sprawdź, czy zadanie jest oznaczane jako podjęte, zanim zacznie być wykonywane.

Postępowanie

Natychmiast: wyłącz automatyczne ponawianie dla operacji ze skutkami ubocznymi. Lepiej zgłosić błąd i poprosić o decyzję, niż wykonać coś drugi raz. To zatrzymuje krwawienie w kilkanaście minut.

Potem: klucz idempotencji. Wywołanie niesie identyfikator, po którym system docelowy rozpoznaje powtórzenie i zwraca wynik pierwszego wykonania zamiast wykonywać ponownie. Standard w API płatniczych i najlepsze dostępne rozwiązanie przyczyn pierwszej, drugiej, trzeciej i piątej naraz. Jako klucza użyj identyfikatora przebiegu połączonego z numerem kroku — masz go już w śladzie przebiegu.

Gdy systemu docelowego nie da się zmienić: sprawdzenie stanu przed działaniem. Zanim wyślesz, zapytaj, czy taka operacja już istnieje. Słabsze niż klucz idempotencji, bo między sprawdzeniem a zapisem jest szczelina, ale zwykle wystarczające.

Dla operacji nieodwracalnych: rozdzielenie przygotowania od zatwierdzenia. Agent tworzy operację w stanie oczekującym, zatwierdza ją osobny krok — najlepiej z udziałem człowieka.

Zapisuj ślad przed operacją, nie po. Ta kolejność ma większe znaczenie niż cokolwiek innego w tym mechanizmie: rejestr zapisany po wykonaniu nie chroni przed niczym, bo awaria zdarza się dokładnie pomiędzy.

Profilaktyka

Reguła, od której warto zacząć projektowanie narzędzi: każde narzędzie ze skutkiem ubocznym jest albo idempotentne, albo objęte potwierdzeniem przed wykonaniem. Trzeciej możliwości nie ma.

Oznaczaj narzędzia jawnie w kodzie, żeby pętla mogła automatycznie decydować o ponawianiu i zrównoleglaniu. Nie zrównoleglaj operacji zapisujących. Deduplikuj na wejściu.

Jak zdiagnozować u siebie

Symulacja przyczyny pierwszej — operacja wykonana, odpowiedź zgubiona — i porównanie zachowania bez klucza idempotencji i z nim.

python
import uuid

SYSTEM_DOCELOWY = []          # "baza" po drugiej stronie
WIDZIANE_KLUCZE = {}

class Timeout(Exception): pass

def wyslij_mail(adres, tresc, klucz=None, gubie_odpowiedz=False):
    """Narzędzie po drugiej stronie sieci."""
    if klucz and klucz in WIDZIANE_KLUCZE:
        return WIDZIANE_KLUCZE[klucz]          # rozpoznane powtórzenie

    SYSTEM_DOCELOWY.append({"adres": adres, "tresc": tresc})
    wynik = {"status": "wyslano", "id": len(SYSTEM_DOCELOWY)}
    if klucz:
        WIDZIANE_KLUCZE[klucz] = wynik

    if gubie_odpowiedz:                        # wykonane, ale nie wróciło
        raise Timeout("brak odpowiedzi")
    return wynik

def z_ponowieniami(klucz=None, prob=3):
    for i in range(prob):
        try:
            return wyslij_mail("klient@firma.pl", "Potwierdzenie",
                               klucz=klucz, gubie_odpowiedz=(i < 2))
        except Timeout:
            print(f"  próba {i+1}: timeout — ponawiam")
    return None

print("BEZ klucza idempotencji:")
SYSTEM_DOCELOWY.clear(); WIDZIANE_KLUCZE.clear()
z_ponowieniami()
print(f"  maili w systemie docelowym: {len(SYSTEM_DOCELOWY)}\n")

print("Z kluczem idempotencji:")
SYSTEM_DOCELOWY.clear(); WIDZIANE_KLUCZE.clear()
klucz = f"{uuid.uuid4().hex[:8]}-krok3"
z_ponowieniami(klucz=klucz)
print(f"  maili w systemie docelowym: {len(SYSTEM_DOCELOWY)}")

Pierwszy przebieg wysyła trzy maile, mimo że pętla widzi wyłącznie nieudane próby. Drugi wysyła jednego — bo system docelowy rozpoznaje powtórzenie po kluczu, choć z perspektywy pętli sytuacja jest identyczna.

To jest cała różnica między agentem, który po awarii sieci zawiadamia klienta raz, a takim, który robi to trzy razy. Kod pętli w obu przypadkach ten sam.

Ćwiczenie: policz w swoim systemie trzy liczby za ostatni miesiąc — zadania na wejściu, wywołania narzędzi ze skutkami ubocznymi w logu, operacje w systemie docelowym. Jeśli trzecia jest wyższa od drugiej, masz przyczynę pierwszą albo drugą i wiesz, gdzie szukać.

Powiązane

Klasyfikacja awarii, ponowienia i skutki uboczne: Maszyneria agenta.

Następny syndrom: Agent zrobił coś, o co nikt nie prosił.

Model nie pamięta — pamięta produkt

Model nie pamięta — pamięta produkt

Model nie pamięta ani jednego słowa z waszej poprzedniej wymiany. Wagi są zamrożone, między turami nie zapisuje się nic, a wrażenie ciągłości powstaje przez mechanizm tak prosty, że po jego poznaniu przestaje dziwić połowa rzeczy z tej serii — łącznie z rachunkiem za...

Czego nie widzisz w oknie czatu

Czego nie widzisz w oknie czatu

Okno czatu wygląda jak najkrótsza możliwa droga do modelu, a jest najdłuższą. Między twoim zdaniem a wagami stoi więcej warstw niż w wywołaniu API — i żadnej z nich nie widzisz, nie ustawiasz i nie możesz wyłączyć. W poprzednim torze rozkładaliśmy to, co dzieje się na...