Engineering the Agentic Stack · Część 1

Pętle rozumowania agentów AI: ReAct, ReWOO, Plan-and-Execute

Tłumaczenie automatyczne Ten artykuł został automatycznie przetłumaczony z angielskiego oryginału.

Aktualizacja artykułu

Pierwotnie opublikowano 31 stycznia 2026 r. Artykuł zaktualizowano i ponownie zrecenzowano 6 września 2026 r. Aktualizacja koncentruje się na nowszych możliwościach modeli i reasoning APIs oraz zawiera zmienione przykłady i linki do źródeł.

Pętla rozumowania agenta to przepływ sterowania, który decyduje, kiedy model planuje, wywołuje narzędzie, odczytuje wynik i kończy działanie. Dla inżyniera budującego agenta ten wybór jest również budżetem: określa, jak często uruchamiany jest model, ile historii niesie każde wywołanie oraz czy zaskakujący wynik może zmienić kolejne działanie.

W tym artykule porównuję ReAct, ReWOO oraz Plan-and-Execute na przykładzie agenta analityka rynku zbudowanego w LangGraph. Poznasz regułę routingu i schematy implementacji, które można dostosować — zamiast trzech nazw do dodania na diagramie.

Pętla jest najbardziej wewnętrzną warstwą tej serii. Otaczają ją pamięć, narzędzia, bezpieczeństwo, runtime oraz kontrole wykonywane przed uznaniem zadania za zakończone; nie zastępują one pętli.

Krótkie porównanie frameworków znajdziesz w Najlepsze frameworki agentów AI w 2026 roku.

Pętla rozumowania decyduje, co zrobić dalej. Nie przechowuje stanu, nie wykonuje narzędzi ani nie autoryzuje efektów ubocznych.

Harness to program sterujący między modelem a maszyną. Składa prompty ze stanu przechowywanego w pamięci (Część 2), definiuje działania, które model może nazwać (Część 3), autoryzuje wywołania (Część 4) oraz sprawdza dowody przed uznaniem zadania za zakończone (Część 6). Są to odrębne problemy inżynieryjne, ale pojedyncza tura przechodzi przez wszystkie cztery.

Runtime (Część 5) dostarcza log sesji, sandbox, magazyn checkpointów oraz trace’y, które przetrwają dłużej niż pojedynczy proces workera.

Gdzie znajduje się każda część serii Engineering the Agentic StackGdzie znajduje się każda część serii Engineering the Agentic Stack

Każdy artykuł stanowi samodzielną całość. Razem prowadzą od pętli na zewnątrz.


Zacznij od granicy awarii

Dobry prompt nie rozstrzyga kwestii przepływu sterowania. Wzorce różnią się tym, ile pracy jest ustalone przed pierwszym wywołaniem narzędzia. To określa liczbę wywołań modelu, moment ujawnienia błędnego planu oraz to, czy nieoczekiwany wynik narzędzia może przekierować wykonanie.

Trzy wzorce rozumowania agentów AI

Porównanie ReAct, ReWOO i Plan-and-Execute według momentu, w którym dowody mogą zmienić planPorównanie ReAct, ReWOO i Plan-and-Execute według momentu, w którym dowody mogą zmienić plan

ReAct: podejmowanie decyzji po każdej obserwacji

ReAct (Yao et al., 2022), skrót od Reason + Act, utrzymuje kolejną decyzję blisko najnowszej obserwacji:

Oryginalny artykuł wykorzystuje jawny tekst typu Thought, Action i Observation. Współczesne natywne API narzędzi udostępniają proponowane wywołania i wyniki narzędzi; widoczna myśl nie jest wymagana. Myślenie przeplatane jest odrębną funkcją modelu/API. W historycznym schemacie promptowania:

  1. Thought: agent generuje „myśl”, aby rozłożyć cel na części i zaplanować kolejny krok.
  2. Action: na podstawie tej myśli wywołuje narzędzie.
  3. Observation: agent odczytuje wynik, który aktualizuje jego rozumienie problemu przed sformułowaniem kolejnej myśli.

ReAct odczytuje każdy wynik narzędzia przed wyborem kolejnej akcjiReAct odczytuje każdy wynik narzędzia przed wyborem kolejnej akcji

Dzięki temu ReAct ma użyteczne właściwości:

  • W ręcznym przykładzie PaLM-540B dla HotpotQA z artykułu obserwacje z Wikipedii prowadziły do mniejszej liczby halucynowanych faktów niż promptowanie chain-of-thought.
  • Agent może na bieżąco zmieniać strategię na podstawie tego, co właśnie zobaczył.
  • Historia wywołań narzędzi i obserwacji daje konkretny ślad wykonania.

Ta sama pętla ma jednak koszty:

  • W naiwnej implementacji obejmującej całą historię, bez cachowania, każda tura ponownie przetwarza narastającą historię. Prompt caching zmienia koszt wejścia i czas przetwarzania, ale nie zmienia zajętości okna kontekstowego ani nieaktualnych obserwacji. Mierz osobno tokeny z cache i bez cache; podsumowywanie oraz obcinanie odrzucają kontekst.
  • Jest nieefektywna, gdy wywołania narzędzi można było zaplanować z góry — właśnie tę niszę wypełnia ReWOO.
  • Bez warunku zatrzymania lub limitu kroków pętla może działać w nieskończoność.

Używaj jej do zadań eksploracyjnych, debugowania oraz pracy, w której nie da się przewidzieć kolejnej akcji.

ReWOO: najpierw skompiluj graf narzędzi

ReWOO (Reasoning WithOut Observation) oddziela planowanie od wykonania. Planner zapisuje kompletną sekwencję narzędzi w jednym przebiegu, używając placeholderów dla wartości, które pojawią się dopiero po wykonaniu.

  1. Plan: jedno wywołanie LLM zapisuje kompletny plan wywołań narzędzi, używając placeholderów zmiennych (#E1, #E2) dla wyników, które jeszcze nie istnieją.
  2. Worker: executor niebędący LLM uruchamia zaplanowane narzędzia i uzupełnia placeholdery. Worker w artykule wykonuje plan; implementacja przedstawiona dalej dodaje zależne od zależności równoległe batch’e dla gotowych kroków.
  3. Solver: końcowe wywołanie LLM otrzymuje zebrane obserwacje i generuje odpowiedź.

ReWOO planuje graf zależności przed uruchomieniem narzędziReWOO planuje graf zależności przed uruchomieniem narzędzi

Taki podział zapewnia:

  • Mniejszą liczbę powtarzanych wywołań modelu niż w ReAct, gdy początkowy plan pozostaje poprawny.
  • Mniej powtarzanej historii promptu niż w przeplatanej pętli obejmującej całą historię. Opóźnienie narzędzi nadal zależy od sposobu, w jaki worker planuje wywołania.
  • Możliwość niezależnego dostrajania plannera, bez dostępu do działającego środowiska.

Tworzy on jednak twardą granicę. W teście obciążeniowym HotpotQA z artykułu każde narzędzie zwracało No evidence found; ReWOO tracił mniej dokładności niż ReAct, ponieważ nieudane obserwacje nie wprowadzały jego plannera w kolejną pętlę. Jest to względna odporność, a nie polityka odzyskiwania po błędzie wykonania. Implementacja nadal musi zdecydować, czy błąd narzędzia stanie się dowodem dla solvera, wywoła ponowienie próby, czy przerwie wykonanie. ReWOO pasuje do przewidywalnych workflowów; samodzielnie nie planuje ponownie działań wokół niepoprawnego grafu początkowego.

Używaj go do szybkich snapshotów, sprawdzania statusu oraz dashboardów, w których zachowanie narzędzi jest przewidywalne.

Plan-and-Execute: dekomponuj zadanie, a następnie reaguj lokalnie

Promptowanie Plan-and-Solve opisuje metodę promptowania, która najpierw tworzy plan, a następnie rozwiązuje podzadania. Powiązany wzorzec orkiestracji narzędzi jest powszechnie nazywany Plan-and-Execute. Przewodnik LangChain po Plan-and-Execute opisuje ten wzorzec:

  1. Faza planowania: agent najpierw generuje plan, który dzieli zadanie na mniejsze podzadania.
  2. Faza wykonania: agent następnie realizuje te podzadania jedno po drugim. Gdy wykorzystywane są narzędzia, każde podzadanie zwykle działa jako własna mała pętla ReAct, dzięki czemu executor nadal może reagować na wynik zwrócony przez narzędzie, mimo że ogólny plan pozostaje stały.

Oryginalny artykuł koncentrował się na promptowaniu zero-shot. W implementacji korzystającej z narzędzi wzorzec orkiestracji może wykonywać zaplanowane kroki sekwencyjnie i używać różnych modeli do planowania i wykonania. Taki podział modeli jest decyzją implementacyjną, a nie wynikiem ustalonym w artykule na temat Plan-and-Solve.

Opcjonalny wariant Plan-and-Execute z ponownym planowaniem: każdy krok wykorzystuje feedback, a replanner może zmodyfikować ogólny planOpcjonalny wariant Plan-and-Execute z ponownym planowaniem: każdy krok wykorzystuje feedback, a replanner może zmodyfikować ogólny plan

Rysunek przedstawia opcjonalną gałąź ponownego planowania. Graf dydaktyczny opisany w dalszej części tego artykułu jej nie wykorzystuje: feedback może zmieniać pracę w ramach jednego kroku, ale nie pozostałą część planu.

Ten wzorzec jest użyteczny, ponieważ zapewnia:

  • Hierarchiczne rozumowanie odzwierciedlające sposób, w jaki ludzki ekspert dzieli projekt na mniejsze części.
  • Jawna krawędź ponownego planowania umożliwia wstrzymanie pracy i ponowną ocenę sytuacji po nieoczekiwanym wyniku kroku.
  • Specjalizację modeli. Planner może być kosztowny, a executor — tani.
  • Po skonfigurowaniu checkpointera każdy ukończony krok może stać się punktem wznowienia.

Wady tego podejścia:

  • Więcej wywołań modelu niż w ReWOO, gdy każdy krok zawiera własną pętlę ReAct.
  • Więcej stanu do zarządzania.
  • Przerost formy nad treścią w przypadku zapytań one-shot.

Używaj go do złożonych analiz i prac badawczych wymagających końcowej syntezy.

Wybieraj na podstawie miejsca, w którym plan może zawieść

CechaReAct (2022)Plan-and-Execute (2023)ReWOO (2023)
Główna filozofiaImprowizator: na podstawie ostatniego wyniku decyduje o kolejnym kroku, wykonując jedno wywołanie naraz.Architekt: buduje pełny plan, wykonuje go, a następnie dokonuje przeglądu.Optymalizator: kompiluje graf zależności, a następnie grupuje wywołania, które są gotowe do wykonania.
Przepływ pracyPętla iteracyjna: Thought → Action → Observation.Dwuetapowy: faza 1 (planowanie), faza 2 (wykonanie).Rozdzielony: Planner zapisuje graf wywołań narzędzi; Worker je uruchamia; Solver składa odpowiedź.
AdaptacyjnośćNajwyższa: może zmienić kierunek po każdym pojedynczym wywołaniu narzędzia.Każdy krok może reagować na swój wynik. Opcjonalny replanner może zmienić późniejsze kroki.Najniższa: skrypt plannera jest wykonywany do końca; w trakcie działania nie następuje ponowne planowanie.
WydajnośćNaiwna pętla z pełną historią powtarza większą liczbę tokenów wejściowych; zarządzanie kontekstem może ograniczyć ten wzrost.Pętla ReAct każdego kroku może zaczynać się od krótkiego kontekstu zamiast pełnej historii wykonania; po włączeniu replanner dodaje wywołania planowania.Mniejsza liczba wywołań modelu; Worker z tego artykułu dodatkowo grupuje narzędzia, których zależności są gotowe.
Najlepsze zastosowanieEksploracja bez z góry określonego celu lub zadania, w których wyniki są nieprzewidywalne.Zadania o długim horyzoncie, wymagające stałego celu (np. napisanie artykułu naukowego).Ustrukturyzowane, powtarzalne przepływy pracy (np. sprawdzanie pogody w 5 miastach).

Tabela służy do wyboru strategii, a nie do porównywania wyników benchmarków. Używaj ReAct, gdy wynik narzędzia może zmienić następne działanie. Używaj Plan-and-Execute, gdy zadanie można podzielić na kroki, ale każdy krok nadal wymaga informacji zwrotnej. Dodaj replannera tylko wtedy, gdy jeden wynik musi zmienić późniejsze kroki; poniższy graf dydaktyczny go nie zawiera. Używaj ReWOO, gdy wszystkie zależności narzędzi są znane przed wykonaniem. Jego graf zależności pozwala dydaktycznemu Workerowi uruchamiać równolegle gotowe wywołania i wykrywać graf, który nie może posunąć się naprzód. Za pomocą pomocnika execute_tool z companiona przechwycone błędy narzędzi stają się ciągami przekazywanymi do Solvera. Wyjątek, który wydostanie się poza pomocnika, przerywa działanie Workera. Żadna z tych ścieżek nie zapewnia ponawiania prób ani ponownego planowania. Przed optymalizacją pod kątem liczby wywołań zmierz wszystkie trzy strategie z użyciem swojego modelu, opóźnienia narzędzi, zestawu zadań i zasad ponawiania prób.

Co się zmienia w przypadku obecnych modeli i harnessów

Te wzorce opisują decyzje podejmowane poza modelem. Model, który rozumuje między wywołaniami narzędzi, nadal potrzebuje programu uruchamiającego te wywołania, zatrzymującego pętlę i autoryzującego efekty uboczne. W przypadku Claude Sonnet 5 adaptive thinking jest domyślnie włączone, a jego tekst jest domyślnie pomijany. Pusty blok thinking nie oznacza, że model pominął rozumowanie. Przy zwracaniu wyników narzędzi zachowuj kompletne, podpisane bloki thinking; odtworzenie konwersacji na podstawie widocznego tekstu powoduje utratę tej ciągłości. Porównuj effort związany z rozumowaniem i rozliczaną liczbę tokenów wyjściowych, a także liczbę wywołań modelu.

W nowej implementacji LangChain zacznij od create_agent. Ten samowystarczalny przykład konfiguracji korzysta z bieżącego identyfikatora Sonnet i lokalnego narzędzia fixture. Przed jego uruchomieniem ustaw ANTHROPIC_API_KEY oraz zainstaluj langchain i langchain-anthropic; wywołanie to wykonuje płatne żądania do modelu.

from langchain.agents import create_agent
from langchain_anthropic import ChatAnthropic

def lookup_fixture_company(ticker: str) -> str:
    """Look up a company name in a tiny local fixture, not live market data."""
    return {"NVDA": "NVIDIA", "AMD": "AMD"}.get(
        ticker.strip().upper(), "No company in the fixture"
    )

agent = create_agent(
    model=ChatAnthropic(model="claude-sonnet-5", max_tokens=4096),
    tools=[lookup_fixture_company],
    system_prompt="Use the fixture for company names. Do not invent market data.",
)
result = agent.invoke({
    "messages": [{"role": "user", "content": "Which company is NVDA in the fixture?"}]
})
print(result["messages"][-1].content)

Jest to pętla narzędziowa sterowana obserwacjami. W przypadku tego pojedynczego wyszukiwania nie jest potrzebny osobny planner. W przykładzie pomijam nadpisywanie parametrów próbkowania: aktualizacja modelu wymaga również sprawdzenia akceptowanych parametrów, bloków odpowiedzi i zachowania ustrukturyzowanych danych wyjściowych. Przykład sprawdzono offline pod kątem importów i konstrukcji, ale nie ewaluowano go przy użyciu działającej inferencji.

Jeśli zadanie wymaga odizolowanych kontekstów subagentów, plików i automatycznego zarządzania kontekstem, Deep Agents pakuje te możliwości wokół tej samej pętli narzędziowej. To wybór harnessu, a nie czwarty algorytm rozumowania. Porównuj pojedynczego agenta z delegowaniem na zadaniach, które faktycznie dzielą się na niezależne prace; uwzględnij w wyniku koszt koordynacji i utratę kontekstu.

Przykład: agent Market Analyst

Market Analyst Agent konkretyzuje tę różnicę. Jedna baza kodu wykorzystuje wszystkie trzy wzorce do badań rynku, a router wybiera między ścieżką deep research a ścieżką flash briefing. Poniższe fragmenty to skrócone warianty dydaktyczne commita b4e769a: poniższy worker dodaje równoległe batch’e gotowe do wykonania po spełnieniu zależności i zgłasza błąd, gdy graf nie może kontynuować działania. Jego execute_tool to pomocnicza funkcja towarzysząca, która przechwytuje wyjątki narzędzi i zwraca solverowi ciągi błędów. Tylko wyjątki, które wydostaną się z tej funkcji pomocniczej, przerywają future. Ta adaptacja dydaktyczna nie dodaje retry ani mechanizmów odzyskiwania.

Do orkiestracji wykorzystuje LangGraph. Węzeł to funkcja Python zwracająca pola do aktualizacji we współdzielonym stanie. Krawędź deklaruje kolejny węzeł i może wywoływać funkcję routingu. LangGraph scala aktualizacje i tworzy checkpointy na granicach superkroków: pojedynczego węzła lub batcha równoległych węzłów. Oczekujące zapisy zachowują pomyślne wyniki węzłów równoległych, gdy inny węzeł zakończy się błędem. Wszystkie trzy wzorce współdzielą jeden obiekt stanu, więc routing nie wymaga trzech osobnych schematów:

Market Analyst Agent kieruje jedno żądanie do dwóch pętli rozumowania ze współdzielonym stanemMarket Analyst Agent kieruje jedno żądanie do dwóch pętli rozumowania ze współdzielonym stanem

Diagram wyodrębnia routing i tworzenie draftu. Pomija współdzielony moduł ewaluatora oraz akceptację człowieka przed publikacją, pokazane później, dzięki czemu obie pętle rozumowania pozostają czytelne.

Definicja stanu

Poniższe bloki Pythona to fragmenty integracji, a nie samodzielne skrypty: współdzielą typy stanu, helpery węzłów i importy frameworka z materiału towarzyszącego. Izolowany runner przykładów w repozytorium je pomija; testy kontraktowe offline obejmują stan, proponowane wywołania, identyfikatory wiadomości oraz zachowanie po wznowieniu.

Schemat stanu przenosi pola potrzebne w obu trybach:

from typing import Literal

class PlanStep(BaseModel):
    """A single step in the research plan."""
    step_number: int
    description: str
    tool_hint: str | None = None
    completed: bool = False
    result: str | None = None

class UserProfile(BaseModel):
    """Structured user context loaded from long-term memory."""
    risk_tolerance: str | None = None
    investment_horizon: str | None = None

class AgentState(BaseModel):
    """Main state for the Market Analyst Agent graph."""

    # Identity and profile context for memory-backed personalization
    user_id: str
    user_profile: UserProfile = Field(default_factory=UserProfile)

    # Message history with LangGraph's add_messages reducer
    messages: Annotated[list, add_messages] = Field(default_factory=list)

    # Execution mode (set by router)
    execution_mode: ExecutionMode | None = None

    # Plan-and-Execute state
    plan: list[PlanStep] = Field(default_factory=list)
    current_step_index: int = 0

    # ReWOO state
    rewoo_plan: list[ReWOOPlanStep] = Field(default_factory=list)

    # Research results
    research_data: ResearchData | None = None

    # Final report. Both paths write this field, then the graph pauses before
    # publishing (see interrupt_before below), so a human signs off on a draft
    # a fresh-context evaluator has already voted on.
    draft_report: DraftReport | None = None
    report_approved: bool = False
    evaluator_verdict: Literal["pass", "fail", "needs_human"] | None = None
    evaluator_reasons: list[str] = Field(default_factory=list)

Wzorzec 1: implementacja Plan-and-Execute

Plan-and-Execute dobrze sprawdza się w syntezie wieloetapowej. Planner zapisuje kroki wysokiego poziomu, a następnie pętla ReAct wykonuje każdy krok i reaguje na wyniki narzędzi.

Poniższe historyczne fragmenty materiału towarzyszącego zachowują claude-sonnet-4-5-20250929 oraz starsze API create_react_agent, aby pozostać porównywalne z podlinkowanym commitem. Aktualnym punktem wyjścia jest powyższy przykład create_agent. Migracja całego grafu wymaga wspólnego przetestowania schematów plannera, obsługi wewnętrznych wiadomości, routingu i zachowania po wznowieniu; sama zmiana nazwy modelu nie jest taką migracją.

Implementacja wyraźnie oddziela cztery granice:

  1. Jedna faza planowania na początku. Pojedyncze wywołanie LLM generuje cały plan jako listę opisów kroków.
  2. Dane wyjściowe sterowane schematem, który waliduje strukturę odpowiedzi. Wykonanie wymaga osobnej kontroli planu.
  3. Brak wykonywania narzędzi na tym etapie. Planner decyduje tylko, co zrobić, a nie jak.
  4. Kroki czytelne dla człowieka. Każdy krok jest tekstem, który zinterpretuje executor.
# System prompt guides the LLM to think like a research analyst
# creating a strategic plan, not immediate tool calls
PLANNER_SYSTEM_PROMPT = """You are a senior investment research analyst.
Break down stock analysis requests into 4-6 research steps covering:
1. Current price and basic metrics
2. Recent news and announcements
3. Competitor analysis (if relevant)
4. Financial health assessment
5. Risk factors
6. Investment thesis synthesis

Output as JSON with step_number, description, and tool_hint."""

# Schema-Guided Reasoning: Enforce structure with Pydantic
class PlanOutput(BaseModel):
    """Structured output for the planner."""

    steps: list[PlanStep] = Field(description="Research steps to execute")
    ticker: str = Field(description="The stock ticker being analyzed")

def planner_node(state: AgentState) -> dict:
    """Generate a research plan from the user's request.

    This is Phase 1 of Plan-and-Execute: creating the high-level strategy.
    """

    # Use a powerful model for strategic planning
    llm = ChatAnthropic(model="claude-sonnet-4-5-20250929", temperature=0)

    # Ask for a typed plan and validate it before execution.
    # The API can still fail, so production code also handles that exception.
    structured_llm = llm.with_structured_output(PlanOutput)

    # Pull the request out of the message history
    human = [m for m in state.messages if isinstance(m, HumanMessage)]
    last_user_message = human[-1].content if human else "Analyze the market"

    # Context from long-term memory personalizes the plan
    profile_context = f"""
User Profile:
- Risk Tolerance: {state.user_profile.risk_tolerance}
- Investment Horizon: {state.user_profile.investment_horizon}
"""

    # Single LLM call creates the complete plan
    result: PlanOutput = structured_llm.invoke([
        SystemMessage(content=PLANNER_SYSTEM_PROMPT + profile_context),
        HumanMessage(content=f"Create a research plan for: {last_user_message}"),
    ])

    # State update: Store the plan and initialize tracking
    return {
        "plan": result.steps,           # The sequential steps to execute
        "current_step_index": 0,        # Start at step 0
        "research_data": ResearchData(ticker=result.ticker),  # Initialize data container
    }

Wiersz llm.with_structured_output(PlanOutput) implementuje Schema-Guided Reasoning (SGR), które omawiałem w poprzednim wpisie. Schemat odrzuca niepoprawne pola. Przed wykonaniem należy również wymagać niepustej i ograniczonej liczebnie listy kroków, unikalnych numerów kroków oraz użytecznych opisów; ten skrócony schemat nie wymusza tych warunków. Materiał towarzyszący nadal może zaakceptować pusty plan i zakończyć działanie błędem, gdy executor spróbuje pobrać pierwszy krok.

Wzorzec 2: wykonanie ReAct

Gdy plan jest gotowy, executor wykonuje każdy krok jako osobną pętlę ReAct. To faza 2: każdy krok jest wystarczająco mały, aby cykl Thought–Action–Observation pozostał skoncentrowany, a agent mógł reagować na dowolny wynik zwrócony przez narzędzie.

Część ReAct działa następująco:

  1. Iteracyjne wykonanie. Jeden krok naraz, z informacją zwrotną w postaci obserwacji.
  2. Pętla model–wynik narzędzia działa wewnątrz create_react_agent; fabryka nie gwarantuje udostępnienia transkryptu rozumowania.
  3. Wyniki poprzednich kroków są przekazywane jako kontekst bieżącego rozumowania.
  4. Agent wybiera narzędzia na podstawie opisu kroku.
  5. Może zmienić podejście w trakcie kroku na podstawie tego, co zwróci narzędzie.
# The five market-data tools the ReAct agent chooses from here. The repo's
# TOOLS list carries four more — a skill loader, two CLI wrappers, and a
# restricted in-process Python evaluator — covering three of the five tool
# modalities Part 3 compares. MCP is the fourth, and it lives in a sidecar
# rather than in this list.
TOOLS = [
    get_stock_snapshot,
    get_price_history,
    search_news,
    search_competitors,
    get_financials,
]

def executor_node(state: AgentState) -> dict:
    """Execute the current step using a ReAct agent.

    This is Phase 2 of Plan-and-Execute: adaptive execution of each planned step.
    Each step runs as a mini ReAct loop until completion.
    """

    # Get the current step from the plan
    current_step = state.plan[state.current_step_index]

    # Build context from what we've learned so far
    # This matters: each step builds on previous observations
    previous_context = ""
    for step in state.plan[:state.current_step_index]:
        if step.result:
            previous_context += f"\nStep {step.step_number}: {step.result}\n"

    # Create a ReAct agent for this step
    # This companion example pins LangGraph's deprecated create_react_agent API.
    # Current LangChain guidance recommends create_agent instead:
    # https://reference.langchain.com/python/langgraph.prebuilt/chat_agent_executor/create_react_agent
    # The observable loop is a proposed call, its result, and the next decision.
    # A visible reasoning block depends on the model and API configuration.
    react_agent = create_react_agent(
        model=ChatAnthropic(model="claude-sonnet-4-5-20250929"),
        tools=TOOLS,
    )

    # Invoke the ReAct loop for this single step
    # The agent will loop internally until it completes the step
    result = react_agent.invoke({
        "messages": [
            SystemMessage(content=EXECUTOR_SYSTEM_PROMPT),
            HumanMessage(content=f"""Execute Step {current_step.step_number}:
{current_step.description}

Ticker: {state.research_data.ticker}
Previous findings: {previous_context}"""),
        ]
    })

    # Extract the final answer from the ReAct agent's message history
    # The last message contains the synthesis after all tool calls
    updated_plan = list(state.plan)
    updated_plan[state.current_step_index] = PlanStep(
        step_number=current_step.step_number,
        description=current_step.description,
        completed=True,
        result=result["messages"][-1].content,  # Final synthesized answer
    )

    # State update: Mark step complete and advance to next
    return {
        "plan": updated_plan,
        "current_step_index": state.current_step_index + 1,
    }

Wzorzec 3: ReWOO do szybkich snapshotów

W przypadku krótkiego briefingu ReWOO usuwa wywołania modelu z fazy wykonania. Niezależne narzędzia działają równolegle, a narzędzia zależne czekają na swoje zależności. Planner emituje graf narzędzi z wyprzedzeniem, a worker wykonuje go bez pytania modelu o kolejny krok.

Schemat wygląda następująco:

  1. Trzy fazy (Planner → Worker → Solver). Worker nie pyta modelu, co zrobić dalej, ani nie tworzy planu ponownie.
  2. Wywołania narzędzi odwołują się do placeholderów #E1, #E2 reprezentujących wyniki, które jeszcze nie istnieją.
  3. Podczas wykonywania nie jest używany LLM. Worker po prostu uruchamia narzędzia.
  4. Niezależne narzędzia są uruchamiane równolegle.
  5. Na końcu wykonywane jest jedno wywołanie syntezy obejmujące wszystkie dane naraz.

Faza 1: planner ReWOO (z góry zapisuje typowany plan proponowanych wywołań)

class ReWOOPlanStep(BaseModel):
    """A step in the ReWOO plan with variable placeholders.

    Key difference from Plan-and-Execute's PlanStep:
    - Contains actual tool_name and tool_args (not just description)
    - Uses variable references (#E1) for dependencies
    """
    step_id: str  # e.g., "#E1" - becomes a variable
    description: str
    tool_name: str     # Requested tool name; validate it against a registry in production
    tool_args: dict    # Proposed arguments; may contain refs like {"price": "#E1"}
    depends_on: list[str] = []  # Declared ordering; cross-check it against #E placeholders
    result: str | None = None

class ReWOOPlanOutput(BaseModel):
    """Structured output for ReWOO planner."""
    steps: list[ReWOOPlanStep] = Field(description="Planned tool calls with variables")

def rewoo_planner_node(state: AgentState) -> dict:
    """Generate a complete plan of tool calls upfront.

    This is the key difference from Plan-and-Execute: instead of creating
    human-readable step descriptions, it creates typed proposed tool calls.
    Production code validates them before execution; this teaching worker does not.
    """

    llm = ChatAnthropic(model="claude-sonnet-4-5-20250929", temperature=0)

    # Schema-Guided Reasoning validates the planner response shape.
    # It does not validate a tool name, its arguments, or its dependencies.
    structured_llm = llm.with_structured_output(ReWOOPlanOutput)

    ticker = state.research_data.ticker if state.research_data else "UNKNOWN"
    human = [m for m in state.messages if isinstance(m, HumanMessage)]
    query = human[-1].content if human else f"Analyze {ticker}"

    # Single LLM call to plan ALL tool executions
    result: ReWOOPlanOutput = structured_llm.invoke([
        SystemMessage(content=REWOO_PLANNER_PROMPT),
        HumanMessage(content=f"""Create a ReWOO plan for: {query}

Ticker: {ticker}

Output tool calls with:
- step_id: Variable name (#E1, #E2, etc.)
- description: What this accomplishes
- tool_name: Exact tool from the list
- tool_args: Dictionary of arguments
- depends_on: List of step_ids this depends on"""),
    ])

    # Store the typed proposed-call plan.
    # This teaching worker sends ready steps directly to execute_tool.
    return {"rewoo_plan": result.steps}

Faza 2: worker ReWOO (wykonuje narzędzia bez rozumowania LLM)

def rewoo_worker_node(state: AgentState) -> dict:
    """Execute dependency-ready tools in parallel batches (no LLM calls).

    Independent tools share a batch. Dependent tools wait until their
    prerequisites complete. The worker follows the dependency graph and
    does not add LLM calls.
    """

    results = {}        # Results keyed by step_id (e.g., "#E1": "$150.23")
    updated_steps = []  # Plan steps with their result field filled in
    pending = {step.step_id: step for step in state.rewoo_plan}

    # Keep scheduling dependency-ready batches until the graph is complete.
    # This handles chains even when the planner does not list them topologically.
    with ThreadPoolExecutor(max_workers=5) as executor:
        while pending:
            ready = [
                step for step in pending.values()
                if all(dep in results for dep in step.depends_on)
            ]
            if not ready:
                unresolved = ", ".join(pending)
                raise ValueError(f"Unresolvable ReWOO dependencies: {unresolved}")

            futures = {
                executor.submit(execute_tool, step, results): step
                for step in ready
            }
            for future in as_completed(futures):
                step = futures[future]
                results[step.step_id] = future.result()
                updated_steps.append(step.model_copy(update={"result": results[step.step_id]}))
                del pending[step.step_id]

    # State update: restore the planner's order (sorting on step_id would put
    # "#E10" before "#E2") and hand the filled-in plan to the Solver
    plan_order = {s.step_id: i for i, s in enumerate(state.rewoo_plan)}
    return {"rewoo_plan": sorted(updated_steps, key=lambda s: plan_order[s.step_id])}

Faza 3: solver ReWOO (syntetyzuje wszystkie wyniki w jednym wywołaniu LLM)

def rewoo_solver_node(state: AgentState) -> dict:
    """Synthesize all tool results into a flash briefing.

    This is the second efficiency gain: Instead of interleaving
    LLM calls with tool execution (like ReAct), we make ONE
    final synthesis call with all gathered data.
    """

    # Build context from ALL tool results at once
    tool_results = []
    for step in state.rewoo_plan:
        if step.result:
            tool_results.append(f"### {step.description}\n{step.result}")

    context = "\n\n".join(tool_results)

    # Single LLM call to synthesize everything
    llm = ChatAnthropic(model="claude-sonnet-4-5-20250929", temperature=0)
    structured_llm = llm.with_structured_output(FlashBriefingOutput)
    result = structured_llm.invoke([
        SystemMessage(content=REWOO_SOLVER_PROMPT),
        HumanMessage(content=f"Create a flash briefing from this data:\n\n{context}"),
    ])

    # FlashBriefingOutput and DraftReport carry the same fields; the state
    # schema expects DraftReport, so convert before returning.
    return {"draft_report": DraftReport(**result.model_dump())}

Planner zapisuje proponowane wywołania, worker uzupełnia ich placeholdery, a solver otrzymuje wyniki. Schemat sprawdza wyłącznie strukturę odpowiedzi. Przed wysłaniem odrzuć puste lub zbyt duże plany, zduplikowane identyfikatory, brakujące zależności i cykle. Zduplikowane identyfikatory są po cichu scalane w słowniku pending powyżej. Sprawdzaj nazwy narzędzi i argumenty względem rejestru, rekurencyjnie analizuj placeholdery wewnątrz zagnieżdżonych list i obiektów, porównuj je z depends_on i odrzucaj nierozwiązane odwołania przed wywołaniem, którego dotyczą. Ten demonstracyjny worker pomija te kontrole i wysyła gotowe kroki do execute_tool companiona, wraz z jego mechanizmem traktowania błędów jako dowodów. Solver musi traktować tekst błędu jako brak dowodu, a nie jako pomyślne wyszukiwanie. Jeśli jego graf nie może wykonać kolejnego kroku, zatrzymuje się przed solverem; nie ma ścieżki ponowienia ani ponownego planowania. Ponowienie tego samego stanu tylko powtarza ten sam plan, dlatego ponowne planowanie wymaga wyniku błędu, warunkowej krawędzi i limitu prób.

Gdzie każdy wzorzec wywołuje model

WzorzecWywołania LLM podczas wykonywaniaAktualizacje stanuKluczowy wzorzec kodu
Plan-and-Execute1 podczas planowania + pętla ReAct dla każdego kroku (po kilka wywołań) + 1 dla raportuSekwencyjne ukończenie krokówplanner_node() → pętla: executor_node()reporter_node()
ReAct (w każdym kroku)Wiele na krok (cykle thought-action)Tylko wewnętrzny transcript; zewnętrzny graf rejestruje wyniki ukończonych krokówCompanion przypięty do użycia przestarzałego create_react_agent()
ReWOO1 podczas planowania + 0 podczas wykonywania + 1 podczas syntezyBatches narzędzi zależne od zależnościrewoo_planner_node()rewoo_worker_node()rewoo_solver_node()

Kluczowa różnica dotyczy danych wyjściowych plannera. Określają one zakres swobody pozostawiony executorowi:

  1. Plan-and-Execute tworzy czytelne dla człowieka opisy kroków:

    # Planner output (list of PlanStep objects)
    plan = [
        PlanStep(
            step_number=1,
            description="Get current price and key financial metrics",
            tool_hint="get_stock_snapshot"
        ),
        PlanStep(
            step_number=2,
            description="Search for recent news and earnings",
            tool_hint="search_news"
        ),
        # ... more steps
    ]

    Executor odczytuje każdy opis i decyduje, które narzędzia wywołać. To elastyczne rozwiązanie, ale każdy krok jest osobną pętlą ReAct, więc koszt kroku to kilka wywołań modelu, a nie jedno.

  2. ReAct nie ma planu tworzonego z wyprzedzeniem. Korzysta z iteracyjnego rozumowania:

    # A standalone full-history ReAct loop carries messages from earlier turns.
    messages = [
        HumanMessage(content="Execute Step 1: Get current price"),
        AIMessage(content="", tool_calls=[{
            "id": "1",
            "name": "get_stock_snapshot",
            "args": {"ticker": "NVDA"},
            "type": "tool_call",
        }]),
        ToolMessage(tool_call_id="1", content="$132.45"),
        AIMessage(content="Now I need metrics..."),
        # ... agent continues until step complete
    ]

    W samodzielnej pętli ReAct z pełną historią każde wywołanie modelu przenosi historię, która rośnie przez całe zadanie. Trafienia w cache mogą ograniczyć ponowne obliczenia i opłaty za dane wejściowe; pętla produkcyjna może również streszczać lub skracać historię. Przykład Plan-and-Execute rozpoczyna każde wywołanie ReAct od bieżącego kroku i skrótu wcześniejszych ustaleń. Ukończony wynik przechowuje w plan, a nie w transkrypcie wewnętrznych wywołań narzędzi.

  3. ReWOO tworzy jawne, typowane proponowane wywołania narzędzi:

    # Planner output (list of ReWOOPlanStep objects)
    rewoo_plan = [
        ReWOOPlanStep(
            step_id="#E1",
            description="Read the current price and valuation snapshot",
            tool_name="get_stock_snapshot",
            tool_args={"ticker": "NVDA"}
        ),
        ReWOOPlanStep(
            step_id="#E2",
            description="Find recent NVDA earnings news",
            tool_name="search_news",
            tool_args={"query": "NVDA earnings", "max_results": 5}
        ),
        # ... all tool calls planned upfront
    ]

    Worker działa bez dostępu do kontekstu i bez udziału LLM. Wszystkie wywołania modelu znajdują się w plannerze i solverze, dzięki czemu liczba wywołań modelu jest przewidywalna.

Przepływ pamięci i stanu:

  • Plan-and-Execute: stan przepływa przez plancurrent_step_indexresearch_data.
  • ReAct w ramach tego grafu Plan-and-Execute: wewnętrzny agent tworzy transkrypt jednego kroku. Zewnętrzny graf zachowuje plan, current_step_index oraz wyniki ukończonych kroków.
  • ReWOO: stan przepływa przez rewoo_plan, a worker uzupełnia pola result.

Połącz obie ścieżki w jeden graf

Graf udostępnia użytkownikom dwie ścieżki oparte na jednym AgentState: szczegółowy research korzysta z Plan-and-Execute z pętlą ReAct wewnątrz każdego kroku, natomiast szybki briefing korzysta z ReWOO. ReAct jest tutaj prymitywem wykonawczym, a nie trzecią ścieżką.

Ta implementacja nie ma replannera: wykonuje początkowy plan do końca. Dodanie replanningu wymagałoby krawędzi z executor z powrotem do planner oraz reguły określającej, kiedy zaskakujący wynik uzasadnia kolejne wywołanie modelu.

LangGraph utrzymuje deklaratywny sposób definiowania połączeń:

def create_graph(checkpointer=None):
    builder = StateGraph(AgentState)

    # Add nodes
    builder.add_node("router", router_node)
    builder.add_node("planner", planner_node)
    builder.add_node("executor", executor_node)
    builder.add_node("reporter", reporter_node)
    builder.add_node("rewoo_planner", rewoo_planner_node)
    builder.add_node("rewoo_worker", rewoo_worker_node)
    builder.add_node("rewoo_solver", rewoo_solver_node)
    builder.add_node("evaluator", evaluator_node)
    builder.add_node("publish", publish_node)

    # Define edges
    builder.add_edge(START, "router")
    builder.add_conditional_edges("router", route_after_router, {
        "planner": "planner",
        "rewoo_planner": "rewoo_planner",
    })

    # Deep Research path
    builder.add_edge("planner", "executor")
    builder.add_conditional_edges("executor", route_after_executor, {
        "executor": "executor",  # Loop back for more steps
        "reporter": "reporter",  # Done with plan
    })
    builder.add_edge("reporter", "evaluator")

    # Flash Briefing path (ReWOO)
    builder.add_edge("rewoo_planner", "rewoo_worker")
    builder.add_edge("rewoo_worker", "rewoo_solver")
    builder.add_edge("rewoo_solver", "evaluator")

    # Both paths run the same evaluator before the human approval step.
    builder.add_edge("evaluator", "publish")
    builder.add_edge("publish", END)

    return builder.compile(
        checkpointer=checkpointer,
        # Human-in-the-loop pause: the reporter (or ReWOO solver) writes a
        # draft. A separate model session reads that draft and records its
        # assessment. The graph then stops before publishing, whichever
        # assessment it produced, so a human can review both the draft and
        # assessment. Part 6 explains how the program decides a run is complete.
        interrupt_before=["publish"],
    )

Automatyczny wybór wzorca za pomocą routera

Router mapuje charakter żądania na odpowiednią ścieżkę. Schema-Guided Reasoning ogranicza dane wyjściowe klasyfikatora:

class ExecutionMode(str, Enum):
    """Execution mode for the agent."""

    DEEP_RESEARCH = "deep_research"  # Plan-and-Execute + ReAct (thorough)
    FLASH_BRIEFING = "flash_briefing"  # ReWOO (fast, token-efficient)

class RouterOutput(BaseModel):
    """Structured output for the router."""

    mode: ExecutionMode  # DEEP_RESEARCH or FLASH_BRIEFING
    ticker: str
    reasoning: str

ROUTER_SYSTEM_PROMPT = """Classify the user's request:

1. **deep_research**: Complex analysis requiring synthesis
   - Examples: "Analyze strategic risks", "investment thesis"

2. **flash_briefing**: Quick snapshots, simple data retrieval
   - Examples: "quick snapshot", "current price"

Default to deep_research if unclear."""

llm = ChatAnthropic(model="claude-sonnet-4-5-20250929", temperature=0)
structured_llm = llm.with_structured_output(RouterOutput)

Dzięki temu routerowi „current price” trafia do ReWOO, a „investment thesis” do Plan-and-Execute. W przypadku niejednoznacznego żądania domyślnie wybierany jest szczegółowy research. Zanim udostępnisz router użytkownikom, porównaj obie ścieżki w ramach stałego workflow, używając tych samych zadań, narzędzi, dowodów i budżetu. Powtarzaj próby stochastyczne i zliczaj każdą rozpoczętą próbę. Śledź skuteczność zadań, błędną akceptację, tokeny z cache i spoza cache, czas ścienny oraz odzyskiwanie po błędach narzędzi lub mylących obserwacjach. Są to decyzje dotyczące przepływu sterowania, a nie ranking sposobów wdrożenia; relacja inżynierska firmy Anthropic również zaleca rozpoczynanie od prostych, kompozycyjnych workflowów.

Pełna implementacja towarzysząca, obejmująca router i współdzielony stan, znajduje się w przypiętym commicie Market Analyst Agent.

Kolejna warstwa to pamięć

Część 2, Architektura pamięci agentów AI, rozdziela wznawialne checkpointy od wiedzy między sesjami i dokumentów projektowych. Bez tej warstwy stanu opisane wyżej router i executor działają tylko tak długo, jak długo aktywne pozostają jeden proces i jedno okno kontekstu.

Referencje