Kurs Python · Moduł 10: AI i modele językowe
ReAct Agents - Reasoning and Acting
W tej lekcji7
Witaj! Darwin tutaj. Zapytaj zwykły model: "Jaka jest pogoda w Serengeti i jakie zwierzęta tam żyją?". Odpowie gładko, ale pogodę zmyśli, bo nie ma dostępu do żadnej stacji meteo. W poprzednich lekcjach daliśmy modelowi narzędzia. Teraz nauczymy go z nich korzystać krok po kroku, jak tropiciel, który najpierw myśli, potem idzie po śladach, a na końcu ocenia, co znalazł.
ReAct (Reasoning and Acting) to wzorzec projektowy dla agentów AI, który łączy rozumowanie z podejmowaniem akcji. Agent na przemian "myśli" (reasoning) i "działa" (acting), co prowadzi do lepszych rezultatów! Wzorzec opisali badacze z Princeton i Google w pracy "ReAct: Synergizing Reasoning and Acting in Language Models" z 2022 roku.
Czym jest ReAct?
ReAct to paradygmat, w którym agent AI:
- Thought - analizuje sytuację, planuje
- Action - wykonuje akcję (np. wywołuje narzędzie)
- Observation - obserwuje wynik akcji
- Repeat - powtarza cykl aż do rozwiązania
Pętla kręci się, aż agent uzna, że ma dość informacji. Na dole schematu widać zapis jednej wyprawy:
1┌─────────────────────────────────────────────────────────┐
2│ ReAct Loop │
3├─────────────────────────────────────────────────────────┤
4│ │
5│ ┌──────────┐ ┌──────────┐ ┌─────────────┐ │
6│ │ Thought │───▶│ Action │───▶│ Observation │ │
7│ │ (Myśl) │ │ (Działaj)│ │ (Obserwuj) │ │
8│ └──────────┘ └──────────┘ └──────┬──────┘ │
9│ ▲ │ │
10│ └──────────────────────────────────┘ │
11│ (Repeat) │
12│ │
13│ Przykład: │
14│ Thought: Muszę sprawdzić pogodę w Serengeti │
15│ Action: get_weather("Serengeti") │
16│ Observation: Temperatura 28°C, słonecznie │
17│ Thought: Teraz mogę odpowiedzieć użytkownikowi │
18│ Action: final_answer("Pogoda w Serengeti...") │
19│ │
20└─────────────────────────────────────────────────────────┘Kluczowa jest Observation: model nie zgaduje wyniku akcji, tylko dostaje prawdziwą odpowiedź narzędzia.
Podstawowa implementacja ReAct
Klasa ReActAgent przechowuje słownik narzędzi, limit iteracji i historię kroków. System prompt wstawia opisy narzędzi, biorąc je z docstringów funkcji (func.__doc__), i uczy model formatu Thought/Action/Answer:
1from openai import OpenAI
2from typing import Callable
3import json
4import re
5
6client = OpenAI()
7
8class ReActAgent:
9 """Agent implementujący wzorzec ReAct."""
10
11 def __init__(self, tools: dict[str, Callable], max_iterations: int = 10):
12 self.tools = tools
13 self.max_iterations = max_iterations
14 self.history: list[str] = []
15
16 def _create_system_prompt(self) -> str:
17 """Tworzy system prompt z opisem narzędzi."""
18 tool_descriptions = "\n".join([
19 f"- {name}: {func.__doc__}"
20 for name, func in self.tools.items()
21 ])
22
23 return f"""Jesteś pomocnym asystentem Safari, który rozwiązuje problemy krok po kroku.
24
25Dostępne narzędzia:
26{tool_descriptions}
27
28Używaj formatu:
29Thought: [twoje rozumowanie o tym, co robić dalej]
30Action: [nazwa_narzędzia(argumenty)]
31
32Gdy masz wystarczające informacje, aby odpowiedzieć:
33Thought: [rozumowanie końcowe]
34Answer: [twoja odpowiedź dla użytkownika]
35
36Zawsze myśl przed działaniem. Analizuj obserwacje zanim podejmiesz kolejny krok."""Limit max_iterations to bezpiecznik: bez niego agent, który się zapętli, zużywałby tokeny bez końca.
Parser wyciąga trzy fragmenty wyrażeniami regularnymi z modułu re. Flaga re.DOTALL pozwala kropce obejmować także znaki nowej linii:
1 def _parse_response(self, response: str) -> tuple[str, str, str]:
2 """Parsuje odpowiedź na thought, action i answer."""
3 thought_match = re.search(r'Thought:\s*(.+?)(?=Action:|Answer:|$)', response, re.DOTALL)
4 action_match = re.search(r'Action:\s*(.+?)(?=Thought:|Observation:|Answer:|$)', response, re.DOTALL)
5 answer_match = re.search(r'Answer:\s*(.+)', response, re.DOTALL)
6
7 thought = thought_match.group(1).strip() if thought_match else ""
8 action = action_match.group(1).strip() if action_match else ""
9 answer = answer_match.group(1).strip() if answer_match else ""
10
11 return thought, action, answerBrak dopasowania nie rzuca wyjątku, tylko zwraca pusty napis.
Wykonanie akcji zamienia tekst w stylu get_weather("Serengeti") na wywołanie prawdziwej funkcji:
1 def _execute_action(self, action_str: str) -> str:
2 """Wykonuje akcję i zwraca obserwację."""
3 # Parsuj nazwę funkcji i argumenty
4 match = re.match(r'(\w+)\((.*)\)', action_str)
5 if not match:
6 return f"Błąd: Nieprawidłowy format akcji: {action_str}"
7
8 func_name = match.group(1)
9 args_str = match.group(2)
10
11 if func_name not in self.tools:
12 return f"Błąd: Nieznane narzędzie: {func_name}"
13
14 try:
15 # Proste parsowanie argumentów
16 if args_str:
17 args = [arg.strip().strip('"').strip("'") for arg in args_str.split(',')]
18 result = self.tools[func_name](*args)
19 else:
20 result = self.tools[func_name]()
21 return str(result)
22 except Exception as e:
23 return f"Błąd wykonania: {str(e)}"Każdy błąd wraca do modelu jako tekst obserwacji, zamiast przerywać program. Parsowanie argumentów przez split(',') jest celowo proste i zepsuje się na przecinku wewnątrz napisu - w produkcji lepiej użyć natywnego function calling, który pokażę niżej.
Metoda run spina wszystko w pętlę:
1 def run(self, query: str) -> str:
2 """Uruchamia agenta ReAct."""
3 self.history = [f"User: {query}"]
4
5 for i in range(self.max_iterations):
6 # Buduj kontekst
7 context = "\n".join(self.history)
8
9 # Wywołaj LLM
10 response = client.chat.completions.create(
11 model="gpt-4.1-mini",
12 messages=[
13 {"role": "system", "content": self._create_system_prompt()},
14 {"role": "user", "content": context}
15 ],
16 temperature=0
17 )
18
19 llm_response = response.choices[0].message.content
20 thought, action, answer = self._parse_response(llm_response)
21
22 # Zapisz thought
23 if thought:
24 self.history.append(f"Thought: {thought}")
25 print(f"Thought: {thought}")
26
27 # Jeśli jest odpowiedź końcowa
28 if answer:
29 print(f"Answer: {answer}")
30 return answer
31
32 # Wykonaj akcję
33 if action:
34 self.history.append(f"Action: {action}")
35 print(f"Action: {action}")
36
37 observation = self._execute_action(action)
38 self.history.append(f"Observation: {observation}")
39 print(f"Observation: {observation}")
40
41 return "Przekroczono limit iteracji bez znalezienia odpowiedzi."Każdy obrót dopisuje myśl, akcję i obserwację do history, a przy następnym wywołaniu model dostaje całą historię. temperature=0 daje możliwie powtarzalne decyzje, dlatego wybrałem gpt-4.1-mini - modele rozumujące, takie jak gpt-5-mini, przyjmują tylko domyślną temperaturę.
Przykład użycia ReAct
Narzędzia to zwykłe funkcje z docstringami. Tu zwracają dane na sztywno, w prawdziwej aplikacji wywołałyby API pogodowe czy bazę gatunków:
1# Definicja narzędzi
2def get_weather(location: str) -> str:
3 """Pobiera aktualną pogodę dla danej lokalizacji."""
4 weather_data = {
5 "Serengeti": "28°C, słonecznie, wilgotność 45%",
6 "Kilimandżaro": "5°C, pochmurnie, śnieg na szczycie",
7 "Nairobi": "22°C, częściowe zachmurzenie"
8 }
9 return weather_data.get(location, f"Brak danych pogodowych dla {location}")
10
11def search_animals(query: str) -> str:
12 """Wyszukuje informacje o zwierzętach Safari."""
13 animals = {
14 "lew": "Panthera leo - największy kot Afryki, żyje w stadach",
15 "słoń": "Loxodonta africana - największe zwierzę lądowe, żyje 60-70 lat",
16 "żyrafa": "Giraffa camelopardalis - najwyższe zwierzę świata, do 5.5m wysokości"
17 }
18 for key, value in animals.items():
19 if key in query.lower():
20 return value
21 return f"Nie znaleziono informacji o: {query}"
22
23def calculate_distance(from_loc: str, to_loc: str) -> str:
24 """Oblicza odległość między lokalizacjami Safari."""
25 distances = {
26 ("Nairobi", "Serengeti"): 350,
27 ("Serengeti", "Kilimandżaro"): 280,
28 ("Nairobi", "Kilimandżaro"): 200
29 }
30 key = (from_loc, to_loc)
31 reverse_key = (to_loc, from_loc)
32
33 if key in distances:
34 return f"{distances[key]} km"
35 elif reverse_key in distances:
36 return f"{distances[reverse_key]} km"
37 return f"Nieznana odległość między {from_loc} a {to_loc}"Docstring nie jest ozdobą: trafia do system promptu i to na jego podstawie model decyduje, którego narzędzia użyć.
Teraz rejestrujemy narzędzia i wysyłamy agenta w teren:
1# Utworzenie agenta
2tools = {
3 "get_weather": get_weather,
4 "search_animals": search_animals,
5 "calculate_distance": calculate_distance
6}
7
8agent = ReActAgent(tools=tools)
9
10# Uruchomienie
11result = agent.run("Jaka jest pogoda w Serengeti i jakie zwierzęta tam żyją?")
12print(f"\nKońcowa odpowiedź: {result}")Na to pytanie agent zwykle wykona dwie akcje - get_weather i search_animals - zanim zwróci Answer.
ReAct z LangChain
LangChain ma gotowego agenta ReAct, więc pętli nie piszesz sam: dekorator @tool zamienia funkcję w narzędzie, a opis bierze z docstringu:
1from langchain.agents import create_react_agent, AgentExecutor
2from langchain_openai import ChatOpenAI
3from langchain.tools import tool
4from langchain import hub
5
6# Narzędzia jako tools LangChain
7@tool
8def safari_weather(location: str) -> str:
9 """Pobiera pogodę dla lokalizacji na Safari."""
10 return f"Pogoda w {location}: 26°C, słonecznie"
11
12@tool
13def animal_info(animal_name: str) -> str:
14 """Wyszukuje informacje o zwierzęciu Safari."""
15 return f"Informacje o {animal_name}: Wspaniałe zwierzę Afryki!"
16
17@tool
18def safari_distance(from_location: str, to_location: str) -> str:
19 """Oblicza odległość między punktami Safari."""
20 return f"Odległość z {from_location} do {to_location}: 150 km"Teraz łączymy model, gotowy prompt ReAct z LangChain Hub i narzędzia, a AgentExecutor prowadzi całą pętlę:
1# LLM i prompt
2llm = ChatOpenAI(model="gpt-4.1-mini", temperature=0)
3prompt = hub.pull("hwchase17/react")
4
5# Agent ReAct
6tools = [safari_weather, animal_info, safari_distance]
7agent = create_react_agent(llm, tools, prompt)
8
9# Executor
10agent_executor = AgentExecutor(
11 agent=agent,
12 tools=tools,
13 verbose=True,
14 handle_parsing_errors=True,
15 max_iterations=10
16)
17
18# Uruchomienie
19result = agent_executor.invoke({
20 "input": "Sprawdź pogodę w Serengeti i powiedz mi o lwach"
21})
22print(result["output"])handle_parsing_errors=True odsyła modelowi błąd formatu, zamiast przerywać działanie, a verbose=True wypisuje każdy krok. Uwaga na wersje: ten kod działa w LangChain 0.3. W LangChain 1.0 create_react_agent, AgentExecutor i hub przeniesiono do pakietu langchain-classic (importujesz je z langchain_classic), a zalecanym następcą jest create_agent z langchain.agents.
ReAct z OpenAI Function Calling
Nowoczesne modele mają wbudowane wywoływanie narzędzi, więc nie trzeba parsować tekstu. Narzędzie opisujemy schematem JSON, a funkcja wykonawcza dostaje gotowe argumenty:
1from openai import OpenAI
2import json
3
4client = OpenAI()
5
6# Definicja tools dla OpenAI
7tools = [
8 {
9 "type": "function",
10 "function": {
11 "name": "get_safari_info",
12 "description": "Pobiera informacje o Safari",
13 "parameters": {
14 "type": "object",
15 "properties": {
16 "topic": {
17 "type": "string",
18 "description": "Temat: weather, animals, locations"
19 },
20 "query": {
21 "type": "string",
22 "description": "Szczegółowe zapytanie"
23 }
24 },
25 "required": ["topic", "query"]
26 }
27 }
28 }
29]
30
31def execute_safari_tool(topic: str, query: str) -> str:
32 """Wykonuje narzędzie Safari."""
33 if topic == "weather":
34 return f"Pogoda dla {query}: 28°C, słonecznie"
35 elif topic == "animals":
36 return f"Informacje o {query}: Fascynujące zwierzę Safari!"
37 elif topic == "locations":
38 return f"Lokalizacja {query}: Popularne miejsce na Safari"
39 return "Nieznany temat"Pole description pełni tę samą rolę co docstring, a required mówi, których argumentów model nie może pominąć.
Pętla wygląda znajomo, tylko zamiast wyrażeń regularnych czytamy message.tool_calls:
1def react_with_functions(query: str) -> str:
2 """ReAct używając OpenAI function calling."""
3 messages = [
4 {
5 "role": "system",
6 "content": "Jesteś ekspertem Safari. Odpowiadaj krok po kroku, używając dostępnych narzędzi."
7 },
8 {"role": "user", "content": query}
9 ]
10
11 for _ in range(5): # Max iterations
12 response = client.chat.completions.create(
13 model="gpt-5-mini",
14 messages=messages,
15 tools=tools,
16 tool_choice="auto"
17 )
18
19 message = response.choices[0].message
20 messages.append(message)
21
22 # Sprawdź czy są tool calls
23 if message.tool_calls:
24 for tool_call in message.tool_calls:
25 args = json.loads(tool_call.function.arguments)
26 result = execute_safari_tool(**args)
27
28 messages.append({
29 "role": "tool",
30 "tool_call_id": tool_call.id,
31 "content": result
32 })
33 else:
34 # Brak tool calls = finalna odpowiedź
35 return message.content
36
37 return "Przekroczono limit iteracji"
38
39# Użycie
40answer = react_with_functions("Jaka pogoda panuje w Serengeti?")
41print(answer)Odpowiedź modelu z tool_calls trzeba dopisać do messages przed wynikami, a każdy wynik łączy się z wywołaniem przez tool_call_id. tool_choice="auto" pozwala modelowi zdecydować, czy potrzebuje narzędzia. Myśl nie jest tu wypisywana jako Thought, ale cykl akcja i obserwacja pozostaje ten sam. Do nowych projektów polecam właśnie tę wersję, bo jest najodporniejsza na błędy formatu.
Zaawansowany ReAct z pamięcią
Agent może też oceniać własne postępy. Najpierw struktura na jeden krok - @dataclass generuje konstruktor, a field(default_factory=datetime.now) wstawia czas utworzenia:
1from dataclasses import dataclass, field
2from typing import Optional
3from datetime import datetime
4
5@dataclass
6class ThoughtStep:
7 """Krok rozumowania agenta."""
8 thought: str
9 action: Optional[str] = None
10 observation: Optional[str] = None
11 timestamp: datetime = field(default_factory=datetime.now)Optional[str] oznacza, że akcja i obserwacja mogą być puste, na przykład przy ostatniej myśli.
Agent z refleksją co kilka kroków prosi model o ocenę dotychczasowej trasy:
1class AdvancedReActAgent:
2 """Zaawansowany agent ReAct z pamięcią i refleksją."""
3
4 def __init__(self, tools: dict, model: str = "gpt-5-mini"):
5 self.tools = tools
6 self.model = model
7 self.client = OpenAI()
8 self.thought_history: list[ThoughtStep] = []
9 self.long_term_memory: list[str] = []
10
11 def reflect(self) -> str:
12 """Refleksja nad dotychczasowymi krokami."""
13 if len(self.thought_history) < 2:
14 return ""
15
16 steps_summary = "\n".join([
17 f"- Thought: {s.thought}, Action: {s.action}, Result: {s.observation}"
18 for s in self.thought_history[-3:]
19 ])
20
21 response = self.client.chat.completions.create(
22 model=self.model,
23 messages=[
24 {
25 "role": "system",
26 "content": "Przeanalizuj dotychczasowe kroki i zasugeruj ulepszenia."
27 },
28 {
29 "role": "user",
30 "content": f"Dotychczasowe kroki:\n{steps_summary}"
31 }
32 ],
33 max_completion_tokens=1000
34 )
35
36 return response.choices[0].message.content
37
38 def should_reflect(self) -> bool:
39 """Decyduje czy agent powinien przeprowadzić refleksję."""
40 # Refleksja co 3 kroki lub gdy ostatnia akcja nie powiodła się
41 if len(self.thought_history) % 3 == 0 and len(self.thought_history) > 0:
42 return True
43 if self.thought_history and "błąd" in (self.thought_history[-1].observation or "").lower():
44 return True
45 return False
46
47 def run(self, query: str) -> str:
48 """Uruchamia agenta z refleksją."""
49 # ... implementacja podobna do podstawowej wersji
50 # ale z dodaną refleksją
51
52 for i in range(10):
53 if self.should_reflect():
54 reflection = self.reflect()
55 print(f"Refleksja: {reflection}")
56
57 # ... reszta logiki ReAct
58
59 return "Odpowiedź"Metoda run to szkielet do uzupełnienia w ćwiczeniu. W reflect użyłem max_completion_tokens: modele rozumujące nie przyjmują starszego max_tokens, a w nowym limicie mieszczą się też ukryte tokeny rozumowania, więc nie ustawiaj go zbyt nisko.
Wzorce ReAct
Podstawową pętlę rozbudowuje się na kilka sposobów. Docstring na górze to mapa wariantów, a klasa pod nim szkicuje wariant z planowaniem:
1"""
2Popularne wzorce używane w agentach ReAct:
3
41. BASIC ReAct
5 Thought -> Action -> Observation -> Repeat
6
72. ReAct with Reflection
8 Thought -> Action -> Observation -> Reflect -> Repeat
9
103. ReAct with Planning
11 Plan -> Thought -> Action -> Observation -> Replan -> Repeat
12
134. ReAct with Self-Consistency
14 Multiple ReAct traces -> Vote on best answer
15
165. ReAct with Retrieval (RAG-ReAct)
17 Thought -> Retrieve -> Augment -> Action -> Observation
18"""
19
20# Przykład ReAct with Planning
21class PlanningReActAgent:
22 def __init__(self, tools: dict):
23 self.tools = tools
24 self.plan: list[str] = []
25 self.current_step = 0
26
27 def create_plan(self, query: str) -> list[str]:
28 """Tworzy plan działania przed rozpoczęciem."""
29 # LLM tworzy plan kroków
30 response = client.chat.completions.create(
31 model="gpt-5-mini",
32 messages=[
33 {
34 "role": "system",
35 "content": "Stwórz plan kroków do rozwiązania zadania. Każdy krok w nowej linii."
36 },
37 {"role": "user", "content": query}
38 ]
39 )
40 plan = response.choices[0].message.content.split("\n")
41 return [step.strip() for step in plan if step.strip()]
42
43 def replan(self, remaining_steps: list[str], new_info: str) -> list[str]:
44 """Aktualizuje plan na podstawie nowych informacji."""
45 # LLM może zmodyfikować pozostałe kroki
46 return remaining_steps # lub zmodyfikowane krokiSelf-consistency uruchamia kilka niezależnych przebiegów i wybiera odpowiedź, na którą wskazuje większość. Moja rada: zaczynaj od podstawowego ReAct i dokładaj warianty dopiero wtedy, gdy widzisz konkretny problem.
ReAct to potężny wzorzec dla agentów AI. Łączy rozumowanie z działaniem, co prowadzi do bardziej przemyślanych i dokładnych odpowiedzi. W następnym module poznasz jeszcze bardziej zaawansowane techniki - RAG i systemy Multi-Agent!
Pamiętaj: dobry agent jak dobry tropiciel - najpierw myśli, potem sprawdza ślady, a odpowiada dopiero wtedy, gdy zobaczył je na własne oczy.
Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Czym jest wzorzec ReAct w kontekście agentów AI?
Zadania praktyczne w grze
- Układanie w pionie
Ułóż kroki pętli ReAct w prawidłowej kolejności:
- Układanie w pionie
Ułóż kroki pętli ReAct w prawidłowej kolejności:
- Edytor kodu
Stwórz klasę ReActAgent z metodami run() i execute_action().
- Klikanie w kolejności
Kliknij w przyciski, aby stworzyć agenta ReAct z LangChain:
- Układanie w pionie
Ułóż kroki ReAct z refleksją:
- Edytor kodu
Zaimplementuj agenta z narzędziami: get_weather, search_animals, calculate_distance.