Strona główna / Artykuły

Narzędzia, pętle, graf: trzy warstwy leżące u podstaw większości awarii agentów w środowisku produkcyjnym.

Awarie agentów produkcyjnych rzadko wynikają z samego modelu. Należy oddzielić zestaw narzędzi, warunek zatrzymania pętli oraz topologię grafu LangGraph – każdy z tych elementów ma swojego odpowiedzialnego.

Wydawca: REACTAPP.TOP24 min czytania5526 słów

Źródła: The Missing Layer in Production AI Agent Stacks

Agent umiera podczas wykonywania zadań, a zespół obwinia model. Zastępują go inteligentniejszym, ale agent znów umiera. Zmienia się instrukcja, a on nadal umiera. Wprowadza się logikę ponownych prób, ale nic się nie zmienia. Sprawa trafia do dostawcy LLM, który informuje, że model zachowuje się zgodnie z projektem. Wszyscy są zdezorientowani.

Ten wzorzec się powtarza. To, co wygląda na wadę modelu, jest w rzeczywistości błędem w mechanizmach go otaczających, a te mechanizmy składają się z trzech warstw, które zespoły rzadko trzymają oddzielnie: narzędzi dostępnych dla agenta („ Harness”), cyklu decydującego o zakończeniu pracy („ Loop”) oraz mapy miejsc, do których może udać się agent („ Graph”). Gdy warstwy się zacierają, błąd w cyklu lub w narzędziach przypisuje się modelowi, a wysiłki skupiają się na widocznej warstwie, podczas gdy ta uszkodzona pozostaje w ukryciu.

Dwie warstwy zamiast trzech

Większość stosów agentów produkcyjnych w 2026 roku działa na dwóch warstwach. Składają się one na graf utworzony za pomocą LangGraph, CrewAI lub OpenAI Agents SDK oraz na warstwę odpowiadającą za trwałość, taką jak Temporal, albo w ogóle na żadną taką warstwę. Pomija się natomiast warstwę sterującą – tę, która określa, jakie narzędzia może używać agent, do jakich adresów URL może się łączyć, co może zwrócić oraz kiedy musi przestać działać.

Dostawcy podążają tym samym kierunkiem. Claude Code otrzymał ustawienie sandbox.network.deniedDomains. GitHub wprowadził kontrole firewall na poziomie organizacji dla swojego agenta Copilot, a VS Code otrzymał klucze polityki grupowej, które filtrowają to, do czego agent może uzyskać dostęp w sieci. Publiczny katalog wzorców agentów wymienia politykę wyjścia jako odrębny wzorzec. Badania wskazują w tym samym kierunku: badanie z arXiv z 2026 roku utrzymywało model niezmieniony przy każdym uruchomieniu, zastępując jedynie narzędzie obsługi u sześciu modeli nowoczesnych i zmierzyło 41% niższe koszty na zadanie oraz 44% mniejszą opóźnioność. Niewiele zespołów zainwestowało w tę warstwę.

Żadna z tych elementów nie istnieje w izolacji. Wokół trzech podstawowych warstw znajduje się brama, która kieruje żądania i kontroluje budżety, pamięć służąca do pobierania danych oraz cache semantyczny, śledzenie każdego kroku, mechanizmy zarządzania stosujące kwoty i limity szybkości, a także proces oceny umożliwiający wdrożenie na idealnym zbiorze danych. Każdy z tych elementów ma swoje znaczenie. Jednak to nie one zazwyczaj powodują awarię agenta. To zamieszanie w granicach struktur, pętli i grafów powoduje takie problemy, ponieważ wynikająca z tego awaria łatwo może zostać błędnie zinterpretowana jako usterka modelu.

Jak odkryto te trzy warstwy

Powodem była usługa pozyskiwania danych, która nie mogła przestać wykonywać powtórek wywołania funkcji query_suppliers. Sztuczna inteligencja ciągle informowała o potrzebie dodatkowych danych, pętla się powtarzała, a rachunek za tokeny stale rosł. Zamiast czytać poradniki, najbardziej przydatne okazały się analizy po incydentach, dokumenty architektoniczne oraz raporty z incydentów, a w różnych, niespowiązanych projektach stale pojawiały się te same trzy słowa: harness, loop, graph.

Często cytowany model opisany w literaturze na temat architektury agentów przedstawia to rozdzielenie w następujący sposób. Harness opisuje relację jednego agenta z jego otoczeniem. Loop opisuje, w jaki sposób wykonywanie zadań przez pojedynczego agenta jest kontrolowane w czasie. Graph opisuje, ile agentów, narzędzi i punktów kontrolnych łączy się w ramach całego aplikacji. Każda warstwa opiera się na tej poniżej niej, a każda pojawia się dopiero wtedy, gdy warstwa poniżej jest wystarczająco stabilna.

Gdy mieliśmy już te nazwy, oryginalny błąd stał się oczywisty: warunek zatrzymania znajdował się wewnątrz narzędzia, a nie w pętli. Harness i pętla zostały pomieszałe. Model nie był uszkodzony, lecz granice warstw tak. Inni zgłaszają ten sam wzorzec. Ktoś ujął to wprost: agent określany jako niewiarygodny z powodu błędnego modelu zazwyczaj ma winą niewłaściwą warstwę. W jednym biuletynie opisano założyciela, który usunął jeden model, zastąpił go rzekomo lepszym, ale otrzymał z powrotem ten sam uszkodzony agent, ponieważ problem leżał w mechanizmach wokół niego.

Te trzy warstwy nie zostały tu wymyślone. Nazwali je ludzie, którzy zarządzają agentami na dużą skalę, a informacje te pojawiają się również w niezależnych źródłach. Poniżej opisano to, co dzieje się, gdy te nazwy spotykają się z rzeczywistym kodem.

Harness: to, czego agent może dotknąć

„Harness” stanowi granicę możliwości. Określa, jakie narzędzia ma agent, jakie są ich schematy wejściowe i wyjściowe, jakie uprawnienia posiada, jaką pamięć może odczytać, do których adresów URL może się odwołać oraz jakie dane może zobaczyć.

Traktuj agenta jak wykonawcę. „Harness” to odznaka wisząca na pasku: decyduje, które drzwi się otwierają, które systemy są widoczne i co jest dozwolone po wejściu do środka. Nie mówi nic o inteligencji. Określa obszar działania, a bezpieczeństwo również się w nim znajduje. Bez niego agent to po prostu wywołanie funkcji z kluczem API, które może swobodnie wywoływać, czytać i zwracać cokolwiek. Z nim agent może robić tylko to, na co pozwala „Harness”.

Większość projektów opartych na agentach nigdy nie tworzy tej warstwy. Tutorials kończą się w momencie, gdy model wywoła narzędzie i otrzyma wynik. Pominęto trzy sytuacje: narzędzie zwracające 10MB plik JSON, który przekracza rozmiar okna kontekstowego, narzędzie łączące się z dowolną adresem URL zgodnie z poleceniem wprowadzonego tekstu, oraz ponowne uruchomienie efektu ubocznego z powodu braku klucza idempotencji. Wszystkie te przypadki to problemy związane z frameworkiem, a ich rozwiązanie przekształca demonstrację w system produkcyjny.

Katalog wzorców określa zasady dotyczące wyjścia w sieci. Domeny, z którymi mogą się łączyć narzędzia agentów, powinny być ograniczone listami dozwolonych i zabronionych adresów, których przestrzega framework, co chroni model przed zagrożeniami z sieci. Gdy model decyduje o nawiązaniu połączenia, możliwe jest obejście tych kontroli poprzez iniekcję poleceń. Sprawdzenie na poziomie frameworka sprawia, że izolacja staje się właściwością strukturalną, a nie probabilistyczną.

To właśnie jest istota różnicy. Modele są probabilistyczne i można je oszukać, wprowadzić fałszywe dane lub wprowadzić w błąd. Narzędzie typu harness ma charakter strukturalny i egzekwuje zasady w kodzie, a nie w instrukcjach. Uprzejme poproszenie modelu, by nie odwiedzał dowolnych adresów URL, to prośba. Unieruchomienie go całkowicie to gwarancja.

Pętla: kiedy agent się zatrzymuje

Pętla jest silnikiem działania agenta. Agent obserwuje świat, wybiera działanie, je wykonuje, sprawdza wynik, a następnie albo kontynuuje, albo się zatrzymuje.

Cykl obserwacja-działanie-weryfikacja to część łatwa. Trudna jest natomiast warunek zatrzymania, ponieważ bez niego agent bez nadzoru traci tokeny w nieskończoność, nie dając wyraźnego sygnału awarii.

Agent ds. pozyskiwania dostawców nie miał żadnych. Wywoływał funkcję query_suppliers, sprawdzał wyniki, stwierdzał, że potrzebuje więcej danych, i wywoływał ją ponownie, w kółko. Model językowy ciągle podawał, że potrzebuje więcej informacji, ponieważ modele językowe skłaniają się ku dodatkowym wywołaniom narzędzi: są pomocne, próbują i nie chcą się poddać. Ta cecha jest atutem przy zadaniami kreatywnych, ale wadą przy zadaniami produkcyjnych.

Katalog wzorców określa to jako pętlę nieograniczoną. Implementacja polega na „iterowaniu, dopóki model nie zgłosi zakończenia pracy”, bez żadnego licznika, zegara ani limitu czasowego lub kosztowego, który mógłby to przerwać. Nieujawnionym założeniem jest to, że model sam poinformuje o zakończeniu. Przy trudnych zadaniach rzadko tak się dzieje: model zapuszcza się w powiązane pytania, powtarza nieudane działania i krąży w błędach, nie dostrzegając tej pętli.

To nie jest hipoteza:

  • W lipcu 2025 roku niekontrolowana rekurencja w Claude Code pochłonęła od 16 000 do 50 000 dolarów w ciągu pięciu godzin. Nie doszło do awarii ani błędu. Agenci po prostu wykonywali polecenia bez żadnego punktu końcowego.
  • Publiczne raporty o błędach opisują sytuacje, gdy podagenci tworzą potomków na głębokości ponad 50 poziomów. W jednej sesji zużyto 4 miliony tokenów w ciągu pięciu minut. Inny użytkownik zapłacił 27,60 dolarów ponad limit planu, gdy kredyty na korzystanie zostały aktywowane w trakcie sesji, bez możliwości zatrzymania jej. W pierwszym przypadku przydatne badania zakończyły się po trzech lub czterech agentach, a pozostałe 44 nie wniosły nic nowego. W drugim przypadku podagenci potraktowali polecenie dotyczące obsługi kilku niezależnych celów jako zgodę na stworzenie własnych potomków.
  • W artykule z lipca 2026 roku problem ten nazwano „Nieskończonymi pętlami agentowymi”. Narzędzie do analizy statycznej IAL-SCAN przeanalizowało 6 549 repozytoriów agentów LLM i potwierdziło 68 awarii w 47 projektach. Sześćdziesiąt osiem agentów, które nigdy się nie zakończą, to raczej bomba czasowa pod względem kosztów niż zwykły błąd.

Przewodnik projektowy dotyczący ograniczonych pętli agentowych stawia tę samą wymogę: ograniczenie musi być częścią projektu już na etapie wdrażania pętli, a nie optymalizacją dodawaną dopiero wtedy, gdy pojawią się problemy. W przeciwnym razie jedyną zasadą zatrzymania jest decyzja modelu o zakończeniu pracy, co jest raczej nadzieją niż mechanizmem kontroli.

Dlatego warunek zatrzymania musi być mechaniczny, a nie semantyczny. „LLM uważa, że praca jest zakończona” to kryterium semantyczne. „Przeprowadzono trzy próby” lub „Wydano 50 centów” to kryteria mechaniczne. Warunki mechaniczne są deterministyczne, poddawalne testom i stabilne przy ponownym uruchomieniu. Nie zależą one od oceny modelu, która jest probabilistyczna i może się różnić przy ponownej próbie.

Agent czuwający w firmie analitycznej pokazuje, ile to kosztuje. Miał on za zadanie zlokalizować przyczynę problemu z opóźnieniem w bramce płatności w ciągu niecałych dwóch minut. Przeszukał logi, otrzymał 4 200 wierszy, skompresował je, doszedł do wniosku, że potrzebne są dodatkowe informacje, i przeszukał je ponownie. Po dwunastu próbach ticket wciąż nie miał streszczenia, podczas gdy proces wykorzystał 380 000 tokenów i 47 dolarów na usługi API. Model nie odmówił kontynuacji, a system nie nakazał przerwy. Gdy firma wprowadziła ograniczenia liczby iteracji, wskaźniki stagnacji (wykrywające powtarzające się niemal identyczne wyniki) oraz mechanizm kontrolujący budżet tokenów, udział niekontrolowanych pętli spadł o 91%, a średni czas rozwiązania ticketa zmniejszył się z 4,1 do 1,6 minuty. Pracę wykonała ograniczona pętla, a nie silniejszy model.

Co zawiera ograniczona pętla

Przykład analityki wskazuje na elementy pętli, która nie może wymknąć się spod kontroli. Każdy z tych elementów jest sprawdzany przez kod znajdujący się poza modelem:

  • Limit iteracji. Dokładna liczba przeprowadzonych prób, dzięki czemu zadanie osiągające ten limit kończy się wyraźną porażką, a nie dalszym, bezskutecznym działaniem.
  • Sprawdzenie stagnacji. Analiza ostatnich wyników, dzięki której kilka niemal identycznych wyników z rzędu jest traktowanych jako brak postępu, nawet jeśli liczba kroków jest nadal niska.
  • Limit wydatków. Budżet w postaci tokenów lub pieniędzy, którego wykonywanie nie może przekroczyć, przy czym mechanizm kontroli znajduje się w czasie wykonywania, a nie w instrukcji wejściowej.
  • Czas timeoutu. Limit czasu trwania, który wykrywa powolne pętle, które nie uruchamiają pozostałych mechanizmów kontroli.
  • Limity rekurencji dla podagentów. Limit głębokości oraz limit łącznej liczby stworzonych agentów, dzięki czemu zdelegowana zadanie nie może tworzyć potomnych, którzy z kolei tworzą własne agenty.

Żaden z tych mechanizmów nie wymaga współpracy modelu, a każdy z nich można przetestować indywidualnie za pomocą fałszywego modelu, który nigdy nie zgłasza ukończenia zadania. Nieudana próba trwająca długo po trzech próbach kosztuje kilka centów. Ta sama próba, pozostawiona pod decyzję modelu, kosztuje tyle, ile pozwala licznik.

Graph: dokąd może iść agent

Graph to topologia: istniejące węzły, łączące je krawędzie, punkty, w których ścieżka się rozgałęzia, miejsca, gdzie osoba udziela zatwierdzenia, oraz stan przekazywany z węzła na węzeł. Jeśli graph to mapa, to pętla to silnik poruszający się po niej, a ograniczenia określają, co może się wydarzyć na każdym przystanku.

Głównym wyborem projektowym jest jawność. Pętla ReAct (myśl, działaj, obserwuj, powtórz) to niejawny graf, w którym jeden węzeł zaopatruje sam siebie, a model językowy pełni rolę funkcji łączącej. W przypadku prostych zadań to wystarcza. Gdy jednak struktura staje się istotna, taki model przestaje działać. Porównanie ReAct z LangGraph dobrze to ilustruje: w pierwszym przypadku model wybiera kolejny krok na podstawie wprowadzonego promptu i historii, natomiast w drugim deweloper dokonuje tego wyboru za pomocą kodu oraz jasno określonych ścieżek.

Różnica staje się odczuwalna, gdy konieczny jest krok zatwierdzenia przez człowieka. W grafie implicytnym nie istnieje pojęcie „między”: agent przechodzi od jednego kroku do drugiego dzięki własnemu rozumowaniu. Dodanie punktu kontrolnego oznacza przerwanie pętli, wprowadzenie mechanizmu pauzy, zewnętrznizację stanu oraz wznowienie działania na podstawie sygnału. To w zasadzie przymusowe dostosowanie grafu do pętli, a pętla się opiera, ponieważ została stworzona do samodzielnego funkcjonowania. W grafie eksplicytnym wystarczy nowy węzeł z wywołaniem interrupt(). Graf się zmienia, podczas gdy pętla i jej elementy pozostają bez zmian.

Jeden z programistów przetestował tę samą politykę, model deterministyczny oraz trzy scenariusze za pomocą zwykłego pętla wywołań narzędzi oraz wyraźnego grafu stanów LangGraph. Zachowanie było identyczne, jednak wersja oparta na grafie wymagała 97 linii kodu w porównaniu z 50, co stanowi około 94% więcej kodu; dlatego po trzech krokach zwykła pętla wygrywa we wszystkich pomiarach. Następnie programista wskazał na to, czego takie porównanie nie może uchwycić. W pętli zachowanie wynika z kolejności instrukcji i musi być pozyskane poprzez przeczytanie jej treści. W grafie krawędzie są deklarowane jako dane, co umożliwia inspekcję topologii, jej rysowanie oraz ponowne odtworzenie bez otwierania żadnego węzła. Wartość wynosi nil po trzech krokach, dużą wartość po trzydziestu, a jeszcze większą, gdy przejmuje kontrolę inna osoba.

To jest argument dotyczący produkcji. Dema składają się z trzech kroków i należą do osoby, która je napisała. Produkcja ma trzydzieści kroków i jest kontrolowana przez osobę na dyżurze o 3 rano. W takim skali wyraźna topologia to warunek przetrwania, a nie dodatkowy obciążenie.

Praktycznym testem przy wyborze między tymi dwoma formami jest zastanowienie się, kto będzie odpowiedzialny za agenta za sześć miesięcy. Krótki agent o jednym celu, utrzymywany przez jego autora, może pozostać zwykłym pętlą. Agent wymagający zatwierdzeń ludzi, posiadający kilka gałęzi lub innego właściciela na dyżurze wymaga wyraźnego grafu, ponieważ topologia staje się danymi, które można narysować, przeanalizować i ponownie odtworzyć.

Jak wygląda pomieszanie warstw

Poniższe przypadki awarii pochodzą z procesu budowania, który jest bezpiecznym miejscem na mieszanie warstw. To te same problemy, które występują w produkcji.

Harness jako właściciel pętli

W tej niejasnej wersji sprawdzenie „czy to wystarczająco dobre?” znajduje się wewnątrz narzędzia, więc to narzędzie decyduje, kiedy pętla ma się zatrzymać. Narzędzie powinno stanowić określoną funkcjonalność, na przykład „może wysyłać zapytania do dostawców”, a nie być podmiotem podejmującym decyzje typu „wystarczy, przestań”. Umieszczenie warunku zatrzymania wewnątrz narzędzia sprawia, że pętla staje się niepodatna na testowanie samodzielnie, uniemożliwia wymianę narzędzia bez ponownej implementacji logiki zatrzymania i nie pozostawia miejsca na ograniczenie liczby iteracji, ponieważ narzędzie nic nie wie o próbach.

Poprawka ma charakter strukturalny. Należy wyjąć warunek zatrzymania z narzędzia i umieścić go w węźle grafu: narzędzie zwraca dane, węzeł decyduje, czy wymagana jest kolejna iteracja, mechanizm dostarcza odpowiednią funkcjonalność, a pętla znajduje się w grafie. Niejasna wersja wygląda w kodzie w ten sposób:

# CONFUSED LAYER — Harness owns Loop logic
async def query_suppliers(item: str, budget: float, quantity: int, attempts: int) -> dict:
    """Tool decides when the loop stops. Capability + control flow in one function."""
    results = query_mock_marketplace(item, budget, quantity)

    # The tool decides when to stop — couples execution logic with capability
    if len(results) > 5 or attempts >= 3:
        return {"status": "STOP", "suppliers": results}
    return {"status": "CONTINUE", "suppliers": results}


async def research_node(state: AgentState) -> dict:
    # The node just calls the tool and forwards whatever the tool decided.
    # It has no idea why the loop stopped — that logic is hidden inside the tool.
    result = await query_suppliers(
        state["brief"].item, state["brief"].budget, state["brief"].quantity,
        state.get("attempts", 0) + 1,
    )
    return {"suppliers": result["suppliers"], "status": result["status"].lower()}

# ✅ CLEAN SEPARATION — Harness is pure capability, Loop lives in the Node
MAX_RESEARCH_ATTEMPTS = 3

async def query_suppliers_impl(item: str, budget: float, quantity: int) -> dict:
    """Pure Harness capability: schema validation, circuit breaker, marketplace call.

    The tool has no idea a loop exists. It returns data. It does not decide
    when to stop. Circuit breaker + fault injection live here because they are
    capability concerns (is the downstream reachable?), not control-flow concerns.
    """
    breaker = get_circuit_breaker("query_suppliers")
    if not breaker.allow_call():
        raise CircuitBreakerOpenError("query_suppliers", breaker.state)

    maybe_inject("query_suppliers")  # fault injection for chaos tests
    try:
        results = query_mock_marketplace(item, budget, quantity)
        breaker.record_success()
        return {"suppliers": results}
    except Exception as e:
        breaker.record_failure()
        raise


async def research_node(state: AgentState) -> dict:
    """Graph Node / Loop: mechanical stop conditions and routing decisions.

    The node reads state, calls the tool through the registry, and decides
    whether to stop — based on counters in state, NOT on the LLM's judgment
    and NOT on the tool's opinion.
    """
    brief = state["brief"]
    attempts = state.get("attempts", 0) + 1

    # Harness call — schema validation, timeout, output cap all happen inside registry.call
    query_result = await registry.call("query_suppliers", {
        "item": brief.item,
        "budget": brief.budget,
        "quantity": brief.quantity,
    })
    suppliers = query_result.get("suppliers", [])

    # Mechanical stop condition — enforced OUTSIDE the tool, in code, on state.
    # Deterministic. Survives replay. Testable without the tool being live.
    if not suppliers and attempts >= MAX_RESEARCH_ATTEMPTS:
        logger.info(
            "STOP_CONDITION_FIRED path=no_matches item=%s attempts=%d max=%d",
            brief.item, attempts, MAX_RESEARCH_ATTEMPTS,
        )
        return {"suppliers": [], "attempts": attempts, "status": "no_matches"}

    if not suppliers:
        return {"suppliers": [], "attempts": attempts, "status": "running"}

    # Enrich, then mark completed
    enriched = []
    for s in suppliers:
        quote = await registry.call("get_price_quote", {
            "supplier_name": s["name"], "item": brief.item, "quantity": brief.quantity,
        })
        rating = await registry.call("check_seller_rating", {"supplier_name": s["name"]})
        enriched.append({**s, "quote": quote, "rating_info": rating})

    return {"suppliers": enriched, "attempts": attempts, "status": "completed"}


# The Graph owns WHERE the agent can go — conditional edges, not the node body.
def _should_retry_or_end(state: AgentState) -> str:
    """After Research: loop back if still running, else Score or END."""
    if state["status"] == "running":
        return "research"          # self-loop — the retry
    if state["status"] == "no_matches":
        return END                 # mechanical stop → graph terminates
    return "score"                 # success → next node


# Wiring (in build_sourcing_graph):
#   graph.add_conditional_edges("research", _should_retry_or_end,
#       {"research": "research", "score": "score", END: END})

Pętla zarządzająca grafem

Wiele frameworków dostarcza pętlę ReAct, w której topologia jest ukryta: agent decyduje o następnym kroku na podstawie wyników modelu, a nie istnieje żaden wyraźny zapis tej struktury. Tak jest do momentu, gdy konieczne staje się dodanie kroku zatwierdzenia przez człowieka pomiędzy podjęciem decyzji a jej potwierdzeniem – wtedy rozpoczyna się modyfikacja, przy czym pętla stara się temu przeciwstawić.

Rozwiązaniem jest projektowanie grafu w sposób wyraźny od samego początku, przy czym węzły stanowią funkcje, a krawędzie przejścia – niektóre z nich są warunkowe, inne pełnią rolę przerwań. Pętla nadal pomaga agentowi poruszać się dalej, ale to graf określa dostępne miejsca, a nie pętla. Monolityczna forma ReAct, w której pętla jest grafem, a LLM funkcją krawędzi, wygląda w ten sposób:

# ❌ CONFUSED LAYER — Implicit topology inside a monolithic ReAct loop
# The loop IS the graph. The LLM is the edge function. There is no "between."
while not agent.is_done():
    action = llm.decide_next_step(history)
    if action == "query":
        data = query_suppliers()
    elif action == "approve":
        # Retrofitting human approval into an implicit loop requires messy pauses.
        # You have to break the loop, externalize state, park the worker, and
        # resume on a webhook — the loop fights you because it was designed to
        # be self-driving.
        pause_execution_and_wait_for_webhook()
    elif action == "stop":
        break


# ✅ CLEAN SEPARATION — Declarative graph with explicit nodes and human interrupt
from langgraph.graph import StateGraph, START, END
from langgraph.types import interrupt


def approve_node(state: AgentState) -> dict:
    """Human checkpoint via interrupt() — a STRUCTURAL pause, not a prompt.

    The graph genuinely suspends here. interrupt() pauses execution and waits
    for a human to call Command(resume=approval_data). The worker is freed.
    If the worker crashes while waiting, Temporal replays the activity, the
    graph re-runs from the last checkpoint, and interrupt() fires again —
    the human re-approves. No data loss.
    """
    selected = state.get("selected_supplier", {})
    brief = state.get("brief", {})

    approval_request = {
        "item": brief.item,
        "quantity": brief.quantity,
        "supplier": selected.get("name", ""),
        "price": selected.get("price", 0),
        "total_cost": selected.get("price", 0) * brief.quantity,
    }

    # interrupt() pauses the graph HERE. The human must call /approve
    # with a resume value to continue.
    approval = interrupt(approval_request)

    approved = approval.get("approved", False)
    comment = approval.get("comment", "")

    return {
        "approval_status": "approved" if approved else "rejected",
        "approval_comment": comment,
        "rejection_count": state.get("rejection_count", 0) + (0 if approved else 1),
    }


def _after_approve(state: AgentState) -> str:
    """After Approve: go to Confirm if approved, retry Research if rejected."""
    if state.get("approval_status") == "approved":
        return "confirm"
    if state.get("rejection_count", 0) + 1 >= 3:
        return END
    return "research"


def build_sourcing_graph(checkpointer=None):
    """Topology is DECLARED DATA, not statement order in a loop body.

        START → Research → (retry) → Score → Decide → Approve → Confirm → END
                                              ↓ (rejected, < 3)
                                             Research
                                              ↓ (rejected, = 3)
                                             END
    """
    graph = StateGraph(AgentState)

    graph.add_node("research", research_node)
    graph.add_node("score", score_node)
    graph.add_node("decide", decide_node)
    graph.add_node("approve", approve_node)      # explicit checkpoint node
    graph.add_node("confirm", confirm_node)

    graph.add_edge(START, "research")
    graph.add_conditional_edges("research", _should_retry_or_end,
        {"research": "research", "score": "score", END: END})
    graph.add_edge("score", "decide")
    graph.add_edge("decide", "approve")
    graph.add_conditional_edges("approve", _after_approve,
        {"confirm": "confirm", "research": "research", END: END})
    graph.add_edge("confirm", END)

    return graph.compile(checkpointer=checkpointer)

Graph zarządzający narzędziem

Taki przypadek jest rzadszy, choć pojawia się podczas przeglądów kodu. Węzeł grafu bezpośrednio wywołuje zewnętrzną API, na przykład żądanie HTTP GET do punktu końcowego dostawcy wewnątrz funkcji węzła, dzięki czemu to graf kontroluje to wywołanie narzędzia.

Jest to błędne, ponieważ graf stanowi topologię i nie powinien nic wiedzieć o protokole HTTP, autoryzacji, ponawianiu prób ani mechanizmach ochronnych typu circuit breaker. Gdy wywołanie znajduje się wewnątrz węzła, dodanie takiego mechanizmu oznacza edycję grafu, zmiana implementacji również wymaga edycji grafu, a testowanie grafu wymaga dostępu do rzeczywistego narzędzia.

Rozwiązanie polega na kierowaniu wywołania przez narzędzie pomocnicze. Węzeł żąda call_tool("query_suppliers", {...}) i otrzymuje zweryfikowany, skategoryzowany wynik. Narzędzie pomocnicze zajmuje się żądaniem HTTP, ponawianiem prób, mechanizmami ochronnymi oraz weryfikacją struktury danych. Graf nie wie o istnieniu protokołu HTTP, a narzędzie pomocnicze nie wie o istnieniu grafu:

# ❌ CONFUSED LAYER — Graph node owns the tool call (Harness concern)
import httpx


def supplier_node(state: AgentState) -> dict:
    """The graph node reaches directly into HTTP, auth, retries, and parsing.

    Why that's wrong: the graph is topology. It should not know about HTTP,
    authentication, retries, circuit breakers, or schema validation. Those are
    harness concerns. When the graph owns the tool call:
      - You can't add a circuit breaker without modifying the graph.
      - You can't swap the tool implementation without modifying the graph.
      - You can't test the graph without the tool being live.
      - You can't enforce an egress allowlist — the URL is hardcoded here.
    """
    resp = httpx.get(
        f"https://supplier-api.example.com/suppliers",
        params={"item": state["brief"].item},
        headers={"Authorization": f"Bearer {SUPPLIER_API_KEY}"},
        timeout=10.0,
    )
    resp.raise_for_status()
    suppliers = resp.json()["suppliers"]  # no schema validation, no output cap
    return {"suppliers": suppliers}


# ✅ CLEAN SEPARATION — Graph node calls through the harness; harness owns the call
async def supplier_node(state: AgentState) -> dict:
    """The graph node says 'call this tool with these args' and gets back a
    validated, typed result. The graph doesn't know there's HTTP involved.
    The harness doesn't know there's a graph involved.
    """
    query_result = await registry.call("query_suppliers", {
        "item": state["brief"].item,
        "budget": state["brief"].budget,
        "quantity": state["brief"].quantity,
    })
    suppliers = query_result.get("suppliers", [])

    # Mechanical stop condition — the node's job, not the tool's
    if state.get("attempts", 0) + 1 >= MAX_RESEARCH_ATTEMPTS and not suppliers:
        return {"suppliers": [], "attempts": state.get("attempts", 0) + 1, "status": "no_matches"}

    return {"suppliers": suppliers, "attempts": state.get("attempts", 0) + 1, "status": "completed"}


# What the harness does — the graph never sees this:
#
#   registry.call("query_suppliers", {...})
#     │
#     ├─ 1. validate_input(QuerySuppliersInput, args)        # Pydantic v2
#     ├─ 2. EgressFilter(spec.allowed_egress)                # URL allowlist
#     ├─ 3. SandboxedHTTPClient(egress_filter)               # blocked domains = structural
#     ├─ 4. run_with_timeout(fn, kwargs, spec.timeout_seconds)
#     ├─ 5. enforce_output_cap(result, spec.max_output_bytes)  # 64KB cap
#     └─ 6. validate_output(QuerySuppliersOutput, result)    # garbage caught here
#
# The graph node gets back a validated dict. It never sees the HTTP call,
# the auth header, the retry, the circuit breaker, or the egress filter.
# Swap httpx for aiohttp, swap the supplier API for a new one, add a circuit
# breaker — the graph doesn't change. The harness doesn't know a graph exists.

Kto posiada który warstwę

Każda warstwa wymaga odpowiedniej infrastruktury, która będzie nią zarządzać. Gdy prawa własności nie są jasne, warstwy wpadają w konflikt.

Temporal odpowiada za bezpieczeństwo przy awariach pętli

Gdy silnik przestaje działać w trakcie cyklu, Temporal zajmuje się przywróceniem normalnego funkcjonowania. Zapisuje każdy krok w historii zdarzeń. Po awarii procesora odtwarza proces pracy, nie uruchamiając ponownie poszczególnych czynności, lecz wprowadzając zapisane wyniki, dzięki czemu pętla restartuje się od ostatniego ukończonego kroku, a nie od zera.

To jest powód, dla którego warunek zatrzymania musi być deterministyczny. Warunek taki jak „model ocenia wykonaną pracę” jest niedeterministyczny, więc ponowne uruchomienie może doprowadzić do innego wyniku. Licznik kroków, górna granica wydatków lub próg jakości wyznaczony na podstawie wyników narzędzi sprawdzają się przy odtwarzaniu procesu. Model może mieć wpływ na decyzję, ale sprawdzenie if attempts >= 3: stop powinno znajdować się w kodzie.

LangGraph posiada topologię grafu

W LangGraph węzły to zwykłe funkcje, krawędzie to przejścia, a krawędzie warunkowe to funkcje, które zwracają nazwę następnego węzła. LangGraph kompiluje to wszystko w działający graf. Ponieważ graf jest jawny, można go przeglądać, każdy węzeł można sprawdzić osobno, a nowy węzeł można dodać bez zakłócania reszty. Punkt zatwierdzenia przez człowieka to po prostu węzeł wywołujący interrupt(), który zmienia graf, ale nie wpływa na pętle i struktury połączeń.

LangGraph kontroluje również kształt stanu, który jest reprezentowany przez TypedDict przekazywany między węzłami. Każdy węzeł odczytuje to, czego potrzebuje, i zapisuje to, co wytwarza, dzięki czemu stan pełni rolę umowy między nimi. Węzeł, który nigdy nie zapisuje wartości suppliers, uniemożliwia następnemu węzłowi jej odczyt, a sprawdzacz typów zwraca na to uwagę przed uruchomieniem.

Ręcznie budowana platforma narzędzi obejmuje wszystko inne

To jest warstwa, której ani Temporal, ani LangGraph nie dostarczają, więc musi zostać stworzona od zera. Obejmuje ona:

  • Rejestrację. Każde narzędzie określa nazwę, schemat wejściowy w formacie Pydantic, schemat wyjściowy, czas timeout oraz to, czy wymaga właściwości idempotencji.
  • Walidację. Dane wejściowe są weryfikowane przed uruchomieniem narzędzia, a dane wyjściowe po nim, dzięki czemu błędne odpowiedzi są wykrywane, zanim agent je zobaczy.
  • Filtrację wyjść. Narzędzia wykonujące żądania HTTP przechodzą przez zabezpieczony klient z listą dozwolonych adresów URL, więc żadne narzędzie nie może skontaktować się z dowolnym adresem. Decyzję podejmuje platforma, a nie model.
  • Ograniczenie rozmiaru wyjść. Wyniki są obcinalane po osiągnięciu 64KB przed dotarciem do okna kontekstowego. Plik o rozmiarze 10MB przychodzi skrócony z oznaczeniem _truncated: true.
  • Wyłączniki obwodowe. Każde narzędzie ma jeden takiego wyłącznik, który jest wspólny dla całego procesu pracy agenta. Pięć nieudanych prób z rzędu powoduje wyłączenie obwodu, a od tego momentu agenci szybko zawodzą zamiast nadal próbować komunikować się z niesprawną API.
  • Granice równoczesności narzędzi. Limity równoczesności dla poszczególnych narzędzi zapobiegają temu, by jedno wolne narzędzie zajmowało wszystkie dostępne sloty dla zadań.
  • Klucze idempotencji. W przypadku narzędzi wywołujących efekty uboczne system tworzy klucz na podstawie identyfikatora procesu i nazwy kroku. Ponownie uruchomione zadanie otrzymuje istniejący wynik, a nie powstaje żaden duplikat.

Żadna z tych funkcji nie pochodzi z Temporal ani LangGraph. To warstwa, która zapewnia bezpieczne wywoływanie narzędzi, a której najczęściej pomijają projekty agentów.

Weryfikacja: „agent twierdzi, że to zadziałało” versus „to rzeczywiście zadziałało”

Trzecim krokiem pętli jest weryfikacja, która musi być przeprowadzana na zewnątrz. Nie może chodzić o to, że agent sprawdza własny wynik, ani o to, że LLM ocenia własne zadanie domowe.

Katalog wzorów nazywa to rozwiązanie rejestracją weryfikacji: samodzielnie zgłaszane informacje, takie jak „budowa zakończona pomyślnie”, ustępują miejsca uporządkowanym zapisom, w których każda weryfikacja to operacja INSERT do bazy danych, a każdy zestaw dowodów – operacja SELECT. Jeśli agent zostanie sam, może wyobrazić sobie, że weryfikacje przebiegły pomyślnie, pominąć poszczególne kroki bez podania powodu lub twierdzić, że uzyskał wyniki z narzędzia, którego nigdy nie uruchomił.

Zespół Honk w Spotify napotkał dokładnie taki problem. Ich agenci czasami zgłaszali, że „testy przebiegły pomyślnie, nie znaleziono problemów”, choć mieli rację. Wprowadzili do pętli deterministyczne narzędzia weryfikujące format, budowę i testy oraz zablokowali tworzenie pull-requests, gdy tylko jakiekolwiek z tych narzędzi zawiodło. Prosty opis działania agenta przestał być sygnałem sukcesu – zastąpił go uporządkowany wynik weryfikacji.

Inne projekty idą dalej. Jeden z narzędzi otwartego oprogramowania albo zawiera potwierdzenie „zakończone, wszystkie testy przeszły”, albo oznacza twierdzenie jako niezweryfikowane, a sam fakt pomyślnego wykonania polecenia nigdy nie powoduje aktualizacji semantycznej treści twierdzenia. Artykuł badawczy sformułowuje tę zasadę w ten sposób: należy ponownie wyprowadzić twierdzenie i nie ufać mu, ponieważ liczba słów agenta nie ma żadnego znaczenia jako dane wejściowe.

W opisanym tu zestawie komponentów weryfikacja jest odrębną czynnością Temporal, taką jak verify_po_exists lub verify_payment_initiated, która bezpośrednio wysyła zapytania do bazy danych. Agent twierdzi, że utworzył zamówienie zakupowe. Czynność ta sprawdza bazę danych z zamówieniami, a jeśli zamówienie tam nie występuje, proces przepada, niezależnie od tego, co zgłosił agent.

Samoodczuwanie to sygnał, który najczęściej może być błędny w sposób najtrudniejszy do wykrycia, dlatego weryfikacja musi być zewnętrzna. Agent o nazwie create_po przetworzył odpowiedź i stwierdził, że wszystko działa. Jednak odpowiedź mogła być wynikiem żądania 200 OK zawierającym błędy, zamówienie mogło zostać zapisane, a następnie cofnięte z powodu przekroczenia czasu transakcji, albo agent mógł odczytać niewłaściwe pole i podać ID, którego nigdy nie istniało. Agent wie tylko to, co zwróciło narzędzie, a nic o stanie bazy danych. Dlatego zwraca się do bazy danych.

Dowody z praktyki produkcyjnej potwierdzają to. Artykuł z lipca 2026 roku analizował Leni, analityka biznesowego opartego na AI, którego pętle weryfikacyjne wykonują zadania, obserwują je, porównują i korygują przy użyciu małych modeli specjalizowanych pod konkretne zadania. Cały system przewyższył swój bazowy model o 11 punktów procentowych w teście SpreadsheetBench – 91,25% w porównaniu z 80,25%. Sama weryfikacja przyniosła niewielki wzrost o 1,5 punktu, ale te punkty skupiły się na najwyższych wartościach w skali ocen, gdzie umożliwiły ukończenie zadań, które w przeciwnym razie by się nie udały. Największy wzrost wyników pochodził od mechanizmów wspierających, kierowania procesem oraz modeli specjalistycznych. Weryfikacja nie ratuje wszystkich przypadków – ratuje te na granicy, gdy agent twierdzi, że zadanie zostało ukończone, podczas gdy rzeczywistość jest inna.

Zasada separacji

Podstawowe zasady: każda warstwa powinna być testowalna, zastępowalna i zrozumiała bez pozostałych warstw.

  • Graf można sprawdzić za pomocą symulowanych wywołań narzędzi, co potwierdza, że odpowiednie węzły są uruchamiane we właściwej kolejności.
  • Harness można przetestować poprzez bezpośrednie wywoływanie narzędzi oraz sprawdzenie walidacji schematu, mechanizmów ochronnych i właściwości idempotencji.
  • Pętlę można wypróbować za pomocą prostej aktywności w ramach procesu Temporal, co potwierdza, że przetrwa ona awarię.

Zmiana grafu nie wpływa na harness, zamiana implementacji narzędzia również nie zmienia grafu, a modyfikacja polityki ponawiania prób pętli nie dotyka żadnego z nich.

Korzyści są praktyczne. Graf agenta ds. pozyskiwania zasobów został zmieniony trzy razy: dodano węzeł oceny, pętlę ponawiania prób z uwzględnieniem odrzuceń oraz punkt kontrolny zatwierdzenia przez człowieka, przy czym nie musiano edytować kodu harness. Dostawca LLM przesunął się od bezpośredniego połączenia z OpenAI do warstwy routingu, nie zmieniając przy tym kodu grafu. Polityka ponawiania prób zmieniła się z agresywnej na ściśle niedozwalającą ponawiania prób w przypadku operacji wywołujących efekty uboczne, bez konieczności modyfikacji grafu czy harness. Oddzielenie komponentów zapewnia większą elastyczność niż czystość: wolność zmiany jednej rzeczy bez uszkodzenia trzech innych.

Ta sama dyscyplina przynosi korzyści, gdy coś idzie nie tak. Nieudaną próbę można przeanalizować poziom po poziomie, zadając trzy precyzyjne pytania. Czy narzędzie zwróciło coś, co nigdy nie powinno mieć prawa zwrócić, lub dotarło w miejsce, do którego nigdy nie powinno mieć prawa dotrzeć? To wskazuje na system połączeń. Czy agent przekroczył jakikolwiek rozsądny limit? To wskazuje na pętlę. Czy agent dotarł do węzła, do którego nie powinien mieć dostępu, lub pominął punkt kontrolny? To wskazuje na graf. Zmiana modelu nie rozwiązuje żadnego z tych trzech problemów.

Trzy pytania przy projektowaniu każdego agenta

  1. Co może dotknąć? (System połączeń) Jakie narzędzia istnieją, z jakimi schematami, jaka ilość danych może zostać wyświetlona, gdzie znajdują się progi wyłączników bezpieczeństwa i jakie klucze idempotencji są stosowane.
  2. Kiedy to się zatrzymuje? (Pętla) Górna granica kroków, górna granica kosztów, czas wygaśnięcia oraz warunek mechanicznego zatrzymania, który nie zależy od oceny modelu LLM.
  3. Dokąd może iść? (Graf) Węzły, krawędzie, gałęzie warunkowe, punkty kontrolne realizowane przez człowieka oraz kształt stanu przepływający między węzłami.

Gdy jakakolwiek odpowiedź brzmi „model decyduje”, istnieje problem. Model LLM doradza, kod podejmuje decyzję, narzędzia ją egzekwują, graf kieruje przepływem, a pętla określa jej granice.

Krótko mówiąc, istnieją trzy warstwy, trzej odpowiedzialni i trzy pytania. Jeśli je pomieszać, błędy wyglądają jak problemy modelu, choć takimi nie są. Jeśli je trzymać oddzielnie, niestabilność modelu pozostaje w ryzach, narzędzia można bezpiecznie używać, a agent zatrzymuje się w odpowiednim momencie.

Literatura pokrewna

  • Tworzenie pętli ulepszeń agenta za pomocą LangGraph i LangSmith Evals — Krok po kroku pętla LangGraph i LangSmith: zbieranie śladów, trzy warstwy oceny, grupowanie błędów, zestawy danych do regresji offline oraz testy przed wdrożeniem.
  • Zanim agent AI otrzyma uprawnienia do produkcji: umowy, grafy roszczeń, testy w cieniu i canary — Proces wypuszczania agentów AI oparty na dowodach: umowy określone w ramach konkretnych zadań, scenariusze z zachowaniem stanu, iniekcja błędów, granice niepewności, stopniowe testy canary oraz automatyczne promowanie po spełnieniu warunków.
  • Dodawanie agentów AI do mikrosług Spring Boot za pomocą Spring AI — Praktyczny przewodnik po pakowaniu narzędzi w Spring AI, dodawaniu mechanizmów ochronnych i metryk tokenowych oraz uruchamianiu pętli agentów asynchronicznie w istniejących usługach Spring Boot.
  • Agenci bezpieczne pod kątem powtórek: unikanie wywołań LLM w procesie pracy za pomocą Temporal — Dlaczego agent, który ulega awarii w trakcie działania, nie powinien ponownie wywoływać swojego LLM: aktywności jako granica niedeterminizmu, mechanizmy powtórek, sygnały stanu, próby ponowne i grafy bezstanowe.
  • Wdrażanie agentów AI do produkcji: wybór między chmurą, samodzielnym hostowaniem a rozwiązaniem hybrydowym — Porównanie wdrożeń agentów w chmurze, w modelu samodzielnego hostowania oraz hybrydowym pod kątem ekspozycji danych, kosztów, ograniczeń, routingu i możliwości monitorowania, wraz z czterema pytaniami pomocnymi w wyborze oraz błędami, których należy unikać.