Strona główna / Artykuły

Tworzenie agenta ReAct w czystym Pythonie: Myślenie, działanie, obserwacja

Napisz pętlę agenta ReAct od zera w Pythonie: kontrakt Thought, Action, Observation, pętlę while sterowaną przez PAUSE, informacje zwrotne o błędach oraz dwa tryby awarii produkcji.

Wydawca: REACTAPP.TOP8 min czytania1785 słów

Źródła: Building a ReAct AI Agent from Scratch in Pure Python

Agent typu planuj-i-wykonuj ma sprytny trik: model najpierw tworzy numerowaną listę kroków, a zwykły Python je wykonywa. To działa dopóki rzeczywistość nie jest zgodna z planem – punkt końcowy się przesuwa, wyszukiwanie zwraca błąd 404 lub model po prostu źle ocenił drugi krok. Agent mimo to kontynuuje wykonywanie kroków od trzeciego do dziesiątego, ponieważ jego konstrukcja nie pozwala mu na sprawdzanie informacji.

ReAct naprawia to, zastępując pełny plan pętlą. Poniżej ta pętla jest budowana w czystym Pythonie, bez żadnego frameworku, zanim przejdziemy do dwóch sposobów, w jakie może się psuć, gdy zaczynają z niej korzystać prawdziwi użytkownicy.

Od ustalonego planu do pętli zwrotnej

Zastanów się nad dwoma sposobami przechodzenia przez nieznany las. Jeden podróżnik zapisuje „1 km na północ, potem 500 m na wschód” i postępuje zgodnie z tym planem, co oznacza, że będzie iść dalej, nawet gdy po 200 metrach pojawi się klif. Drugi zatrzymuje się co jakiś czas i mówi na głos: droga na północ jest zablokowana klifem, więc spróbujmy skręcić na zachód – tam rzeczywiście jest bezpieczna ścieżka.

Pierwszy podróżnik to agent typu planuj-i-wykonuj. Jest tani w użyciu i dobrze zorganizowany, ale kruchy: jeśli drugi krok się nie powiedzie, kroki od 3 do 10 stają się bezużyteczne. Drugi to agent typu ReAct (rozumowanie plus działanie). Wykonuje jeden krok po drugim i ponownie sprawdza otoczenie przed podjęciem kolejnego. To wymaga więcej zasobów, ale znacznie lepiej radzi sobie w trudnych sytuacjach.

Komentarz na bieżąco prowadzony przez drugiego podróżnika stanowi wewnętrzny monolog i jest to mechanizm, który sprawia, że agent wydaje się adaptacyjny, a nie sterowany z góry. Aby jednak był przydatny, ten monolog musi być w stanie uruchamiać rzeczywiste działania.

Wezwanie narzędzia w jednym akapicie

Model językowy może wydawać jedynie tekst. Aby dać mu możliwość działania, używa się zwykłego słownika, który łączy nazwy z funkcjami w Pythonie, na przykład {"get_weather": get_weather_api}. Gdy wyjście modelu zawiera ciąg znaków get_weather, kod wykrywa to i wywołuje odpowiadającą funkcję. Narzędzia pozostają dokładnie takie same w agencie ReAct. Zmienia się tylko sposób, w jaki model decyduje się je używać.

Oto nieco dziwna część całego projektu – aby model wyglądał, jakby myślał samodzielnie, trzeba go silnie ograniczyć.

Umowa leżąca u podstaw monologu

Zamiast prosić o odpowiedź, zmuszasz model do przestrzegania ścisłego formatu wyjściowego i nie pozwalamy mu korzystać z żadnego narzędzia, dopóki nie postępuje zgodnie z tym formatem. Pętla składa się z trzech kroków:

  1. Rozważanie. Model analizuje pytanie, wszystko, co zostało dotychczas zebrane, oraz wcześniejsze niepowodzenia, a następnie zapisuje, co należy zrobić dalej i dlaczego.
  2. Działanie. Na podstawie tych rozważań wybiera jedno narzędzie z listy i wpisuje dokładne dane wejściowe dla niego.
  3. Obserwacja. To jest kluczowy moment przeniesienia kontroli. Model musi się zatrzymać. Twój kod w Pythonie przejmuje kontrolę, odczytuje informacje dotyczące działania, uruchamia odpowiednią funkcję (np. żądanie pogody w czasie rzeczywistym, zapytanie do bazy danych itp.) i zwraca surowy wynik modelowi.

Każdy obieg w tym cyklu dostarcza modelowi nowych dowodów, na podstawie których może rozumować. Cykl hipotezowania, wykonywania działań i weryfikacji powtarza się, aż myśl w końcu doprowadzi do wniosku, który użytkownik może zaakceptować. Krótko mówiąc, model rozumuje i podejmuje decyzje, podczas gdy zwykły kod pełni rolę sędziego – przerywając każde wezwanie narzędzia i informując o tym, co naprawdę się stało.

Na papierze wygląda to schludnie. Pytanie praktyczne brzmi, jak sprawić, by predyktor tekstu zatrzymał się w trakcie pracy, poczekał na skrypt, a następnie wznowił swój tok rozumowania.

Ręczne pisanie pętli

Frameworki takie jak LangChain, CrewAI i AutoGen ukrywają to wszystko za jednym wywołaniem .run(). Jest to przyjemne, dopóki agent nie zaczyna krążyć w kółko lub nie wymyśla narzędzia, którego w rzeczywistości nie istnieje. Naprawianie błędów w „czarnej skrzynce” jest uciążliwe, dlatego warto znać to, co się w niej znajduje.

Zacznij od instrukcji systemowych, które określają zasady współpracy i definiują umowę:

system_prompt = """
You are a dynamic AI Agent. You work in a continuous
loop of
- Thought,
- Action,
- Observation.
- PAUSE

Your available tools are:
- get_weather(location: str): Returns the current weather.
- calculate(expression: str): Evaluates a math expression.

Use the following format exactly:
Question: the input question you must answer
Thought: you should always think about what to do
Action: the action to take, should be one of [get_weather, calculate]
Action Input: the input to the action
PAUSE
(You will then receive an Observation from the environment)
Thought: I now know the answer
Final Answer: the final answer to the original input question
"""

Linia PAUSE pełni kluczową rolę, a polecenie określa również dokładne nazwy pól (Question, Thought, Action, Action Input, Final Answer), dzięki czemu wyrażenie regularne może je pewnie znaleźć w tekście o dowolnej strukturze. Polecenie to każe modelowi przestać generować, co zwraca kontrolę do Pythona, aby narzędzie mogło działać, a jego wynik mógł zostać zapisany do kontekstu jako Observation.

Modele nie zawsze doskonale przestrzegają instrukcji dotyczących formatu, co możesz zobaczyć poniżej. Prompt jest jednak jedynie książką zasad – coś musi je egzekwować: wykrywać PAUSE, uruchamiać funkcję i zapewniać ciągłość działania. Tym „czymś” jest pętla while, serce agenta. Za każdym razem pyta model, analizuje odpowiedź, uruchamia żądane narzędzia i dodaje całą wymianę informacji do historii, zatrzymując się w momencie, gdy model poda ostateczną odpowiedź lub wyczerpie się limit ruchów.

import re

def react_agent(self, user_question, max_turns=5):
    # Initialize our conversational memory with the system prompt
    messages = [
        {"role": "system", "content": self.system_prompt},
        {"role": "user", "content": f"Question: {user_question}"}
    ]

    turn_count = 0

    while turn_count < max_turns:
        print(f"\n--- Turn {turn_count + 1} ---")

        # 1. Get the LLM's response (The Thought + Action)
        response = my_react_agent.call_llm(messages)
        print(response)

        # Add the LLM's generation to the memory
        messages.append({"role": "assistant", "content": response})

        # 2. Check if the agent has reached a conclusion
        if "Final Answer:" in response:
            print("\n✅ Task Complete.")
            #print(json.dumps(messages, indent=2))
            return response.split("Final Answer:")[-1].strip()

        # 3. Parse the Action and Action Input using Regex
        action_match = re.search(r"Action: (.*)", response)
        input_match = re.search(r"Action Input: (.*)", response)

        if action_match and input_match:
            action = action_match.group(1).strip()
            action_input = input_match.group(1).strip()

            # 4. The Observation Phase (Python takes control)
            print(f"⚙️ System Executing: {action}({action_input})")
            try:
                action_input = json.loads(action_input)
                observation_result = my_react_agent.execute_tool(action, action_input)
            except (TypeError, ValueError, json.JSONDecodeError) as error_messgae:
                observation_result = error_messgae
            # Format the observation and feed it back to the agent
            observation_text = f"Observation: {observation_result}"
            messages.append({"role": "user", "content": observation_text})
            print(observation_text)

        else:
            # If the LLM breaks the API contract, gently correct it
            messages.append({"role": "user", "content": "Error: Invalid format. Please provide an Action and Action Input, or a Final Answer."})

        turn_count += 1
    return "❌ Agent timed out before reaching a final answer."

Co daje posiadanie tego pętli

Ponieważ pętla to twój własny kod, to ty decydujesz, co się stanie w przypadku błędu. Ślad z wykonywania z narzędziem typu weather pokazuje, dlaczego to ma znaczenie.

Na przykład model poprawnie określa, że potrzebuje narzędzia o nazwie get_planet_mass, ale zapisuje dane wejściowe w pojedynczych cudzysłowach, {'planet': 'Earth'}. To nie jest prawidłowy JSON, więc parser w Pythonie wysyła standardowe ostrzeżenie: oczekiwał nazw właściwości w podwójnych cudzysłowach.

Zwykły skrypt zakończyłby działanie właśnie w tym momencie. Ta pętla jednak nie rzuca błędem – bierze surowy tekst błędu i umieszcza go w rozmowie jako kolejną Observation.

W drugim kroku model odczytuje tę wiadomość i w swoich myślach stwierdza, że format wejściowego danych był błędny i musi przestrzegać zasad JSON. Ponownie wysyła żądanie z {"planet": "Earth"}, router uruchamia narzędzie i zadanie zostaje zakończone.

Zauważ, że nie napisano nic specjalnego dla tego przypadku. Nie ma mechanizmu ponownej próby obsługi błędnie sformatowanego JSON – istnieje jedynie zwyczaj zwracania błędów jako obserwacji, a resztę robi sama zdolność modelu do rozumienia tekstu. Tak właśnie wygląda w praktyce wewnętrzny monolog. Statyczny planer zatrzymałby się już w pierwszym kroku i wymagałby interwencji człowieka, aby go ponownie uruchomić. Pętla ReAct pozwala modelowi zdiagnozować i naprawić własną składnię w trakcie działania. Nie tylko realizował plan, ale reagował na to, co faktycznie się wydarzyło.

Gdzie ReAct zawodzi w produkcji

Kod powyżej wydaje się niemal magiczny po raz pierwszy, gdy jest uruchamiany. Zanim go wyślesz, powinieneś znać dwa miejsca, w których może napotkać problemy.

Spirala halucynacji. Czasami model uparcie jest przekonany, że uszkodzony narzędzie zaraz będzie działać. Wywołuje funkcję get_weather, widzi błąd, wywołuje ją ponownie, widzi ten sam błąd i kontynuuje, podczas gdy twoje kredyty API się wyczerpują. Obroną jest ograniczenie max_turns=5 w pętli. Taki twardy mechanizm przerwania jest niezbędny i żaden agent ReAct nie powinien mieć możliwości działania bez ograniczeń.

Rozprzestrzenianie się kontekstu. Każdy cykl dodaje swoje myśli, działania i obserwacje do rozmowy. Jeśli narzędzie zwraca dane w formacie JSON liczące 10 000 linii, wszystko to trafia do okna kontekstowego. W następnym etapie model tonie w danych, jakość jego rozumowania spada i może stracić poczucie oryginalnego pytania. Nigdy nie wprowadzaj bezpośrednio surowych odpowiedzi API ani pełnych wyników skrapowania internetu jako obserwacji. Nawet przybliżony limit, np. pierwsze kilka tysięcy znaków wraz z adnotacją, że reszta została obcięta, jest lepszy niż zalewanie modelu danymi. Umieść pomiędzy nimi małą funkcję middleware, która najpierw podsumuje lub skróci wyniki, dzięki czemu monolog pozostanie krótki i zwięzły.

Istnieje również ograniczenie, którego żaden mechanizm ochronny nie może naprawić. Gdy jeden agent musi zarządzać zbyt wieloma dużymi narzędziami oraz zbyt dużą ilością zgromadzonego kontekstu, jego rozumowanie staje się chaotyczne i odchyla się od zadania. Agent ReAct to sprytny taktyk, ale kiepski menedżer.

Łączenie obu stylów

Te dwa podejścia rozwiązują różne problemy. Planowanie i wykonywanie zapewniają strukturę, natomiast ReAct daje odporność. Wiele systemów produkcyjnych ostatecznie wykorzystuje oba: planer opracowuje ogólne kroki, a następnie przekazuje każde skomplikowane podzadanie osobnemu agentowi ReAct, który natychmiast uporządkowuje szczegóły. Koordynacja kilku takich agentów, stworzonych od zera bez żadnych frameworki, jest naturalnym następnym krokiem po tym cyklu.

Literatura pokrewna

  • Componenty React muszą być czyste: idea stojąca za StrictMode i kompilatorem — dlaczego React oczekuje czystych componentów, jak podwójne renderowanie w StrictMode ujawnia nieczystość, gdzie powinny znajdować się efekty uboczne oraz dlaczego kompilator React polega na czystości tych componentów.