Створення агента ReAct на чистому Python: мислення, дія, спостереження
Напишіть цикл агента ReAct з нуля на Python: контракт «Мислення», «Дія», «Спостереження», цикл while, керований PAUSE, зворотний зв’язок про помилки та два режими збою в роботі.
Джерела: Building a ReAct AI Agent from Scratch in Pure Python
Агент, який планує та виконує дії, має цікавий трюк: модель спочатку складає пронумерований список кроків, а звичайний Python їх виконує. Це працює доти, доки реальний світ не суперечить плану. Кінцева точка зміщується, пошук повертає код 404, або модель просто неправильно вгадала другий крок. Агент все одно продовжує виконувати кроки з третього по десятий, тому що його конструкція не дозволяє йому звертатися за інформацією.
ReAct вирішує цю проблему, замінюючи повний план на цикл. Нижче цей цикл створюється на чистому Python без використання фреймворків, а потім розглядаються два способи, якими він може зламатися під час роботи з реальними користувачами.
Від фіксованого маршруту до циклу зворотного зв’язку
Уявіть два способи пройти незнайомим лісом. Один мандрівник записує «1 км на північ, потім 500 м на схід» та слідує цим плану до кінця, що означає, що він продовжує йти навіть тоді, коли на відстані 200 м з’являється скеля. Інший зупиняється кожні кілька кроків та вголос аналізує ситуацію: шлях на північ заблокований скелею, тож варто спробувати йти на захід як об’їзд, і справді там є безпечний шлях.
Перший мандрівник — це агент типу «планування та виконання». Цей підхід економний у використанні ресурсів та добре організований, але крихкий: якщо другий крок зазнає невдачі, кроки 3–10 втрачають значення. Другий мандрівник — це агент типу ReAct (міркування плюс дія). Він виконує по одному кроку за раз та знову перевіряє оточення перед наступним кроком. Це вимагає більше ресурсів, але дозволяє краще впоратися з труднощами, якщо ситуація ускладнюється.
Прямий коментар, який веде другий мандрівник, є внутрішнім монологом, і саме цей механізм змушує агента виглядати адаптивним, а не за скриптом. Однак щоб він був корисним, цей монолог має здатність ініціювати реальні дії.
Виклик інструменту в одному абзаці
Модель мови може генерувати лише текст. Щоб надати їй можливості дії, використовується звичайний словник, який відносить імена до функцій Python, щось на кшталт {"get_weather": get_weather_api}. Коли результат роботи моделі містить рядок get_weather, ваш код це помічає та викликає відповідну функцію. Інструменти залишаються абсолютно такими ж у агенті ReAct. Змінюється лише те, як модель вирішує їх використовувати.
Ось трохи дивна частина всієї конструкції. Щоб модель виглядала так, ніби думає самостійно, її потрібно суворо обмежувати.
Контракт, що лежить в основі монологу
Замість того, щоб просити відповідь, ви примушуєте модель дотримуватися суворого формату вихідних даних та не дозволяєте їй використовувати жодних інструментів, поки вона не дотримається цього формату. Цей цикл складається з трьох етапів:
- Мислення. Модель аналізує запитання, усю зібрану раніше інформацію та попередні невдачі, потім записує, що робити далі та чому.
- Дія. На основі цього мислення вона обирає один інструмент зі списку та формулює точний вхідні дані для нього.
- Спостереження. Це ключовий етап передачі обов’язків. Модель мусить зупинитися. Ваш код на Python бере керування на себе, читає інструкцію щодо дії, виконує реальну функцію (запит до погодних даних, запит до бази даних тощо) та повертає необроблені результати моделі.
Кожен цикл надає моделі нові дані для міркувань. Цикл гіпотезування, виконання та перевірки повторюється, поки думка нарешті не прийде до висновку, який може зрозуміти користувач. Коротко кажучи, модель міркує та приймає рішення, тоді як звичайний код виступає у ролі арбітра, перехоплюючи кожен виклик інструменту та повідомляючи про те, що насправді сталось.
На папері це здається простим. Практичне питання полягає у тому, як змусити прогнозувальник тексту зупинитися на півдорозі, почекати на скрипт та потім продовжити свою логіку.
Написання циклу вручну
Фреймворки на кшталт LangChain, CrewAI та AutoGen ховають усе це за одним викликом .run(). Це зручно, поки агент не починає ходити колами чи не вигадує інструмент, якого насправді немає. Дебагування „чорної скриньки“ є дуже складним, тому варто знати, що всередині неї знаходиться.
Почніть з системного запиту, який встановлює правила взаємодії та визначає умови контракту:
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
"""
Рядок PAUSE виконує основну роботу, а інструкція також вказує точні назви полів (Question, Thought, Action, Action Input, Final Answer), щоб регулярний вираз міг надійно їх знайти у вихідних даних неструктурованого формату. Вона просить модель припинити генерацію, що повертає керування до Python, щоб інструмент міг запрацювати, а його результат можна було записати в контекст як Observation.
Моделі не завжди ідеально дотримуються вказівок щодо формату, і це можна побачити нижче. Проте запит є лише посібником з правил. Щось мусить їх виконувати: фіксувати момент PAUSE, запускати функцію та забезпечувати продовження роботи. Цим „щось“ є цикл while, серцебиття агента. З кожним проходом він ставить запит до моделі, аналізує відповідь, запускає необхідні інструменти та додає весь обмін повідомленнями до історії, зупиняючись тоді, коли модель надає остаточну відповідь або закінчується ліміт кроків.
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."
Що дає власність на цей цикл
Оскільки цикл — це ваш власний код, ви самі вирішуєте, що робити у разі помилки. Лог з виконання за допомогою інструменту у стилі метеорологічних даних показує, чому це важливо.
Наприклад, модель правильно визначає, що їй потрібен інструмент під назвою get_planet_mass, але записує вхідні дані у одинарні лапки, {'planet': 'Earth'}. Це не є коректним JSON, тому парсер у Python видає стандартну помилку: він очікував назви властивостей у подвійних лапках.
Звичайний скрипт зупинився б саме тут. Цей цикл не кидає помилку. Він бере необроблений текст помилки та додає його до розмови як наступну Observation.
На другому кроці модель читає це повідомлення та у своїх думках усвідомлює, що формат вхідних даних був неправильним, і що необхідно дотримуватися правил JSON. Вона знову надсилає запит із {"planet": "Earth"}, маршрутизатор запускає відповідний інструмент, і завдання завершується.
Зауважте, що для цього випадку не було написано нічого особливого. Не існує механізму повторної спроби обробки пошкодженого JSON — лише звичка повертати помилки у вигляді спостережень, а решту виконує сама здатність моделі до розуміння тексту. Ось як виглядає внутрішній монолог на практиці. Статичний планувальник би зламався вже на першому кроці та потребував би людини для його перезапуску. Цикл ReAct дозволив моделі діагностувати та виправити власну синтаксис під час виконання. Вона не просто дотримувалася плану, а реагувала на те, що насправді відбувалося.
Де ReAct не спрацьовує в продакшені
Код вище здається майже чарівним під час першої спроби його виконання. Перш ніж розіслати його, ви повинні знати два моменти, де він може зламатися.
Спіраль галюцинацій. Іноді модель уперто переконується, що несправний інструмент ось-ось почне працювати. Вона викликає функцію get_weather, бачить помилку, знову викликає її, бачить ту саму помилку, і продовжує це робити, поки не вичерпаються кредити вашого API. Захистом є обмеження max_turns=5 у циклі. Такий жорсткий механізм переривання є обов’язковим, і жоден агент ReAct не повинен мати можливості працювати без обмежень.
Надмірний обсяг контексту. Кожен цикл додає свої думки, дії та спостереження до розмови. Якщо інструмент повертає JSON-дані обсягом 10 000 рядків, усе це потрапляє у вікно контексту. До наступного кроку модель потопає у даних, якість її міркувань погіршується, і вона може втратити зв’язок із початковим запитанням. Ніколи не вводьте безпосередньо у поле спостережень сирі відповіді API чи повні результати веб-скрапінгу. Навіть приблизний ліміт, наприклад, кілька тисяч символів разом із приміткою про те, що решта була відкинута, кращий, ніж перевантаження моделі. Використайте невелику функцію проміжного рівня, яка спочатку узагальнює чи скорочує результати, щоб монолог залишався коротким та зрозумілим.
Існує ще один обмеження, яке жоден механізм захисту не може усунути. Коли одному агенту доводиться одночасно використовувати занадто багато великих інструментів та обробляти велику кількість накопиченого контексту, його мислення стає хаотичним, і він відхиляється від поставленої задачі. Агент типу ReAct — це вправний тактик, але поганий керівник.
Поєднання обох стилів
Ці два підходи вирішують різні проблеми. Підхід «планування та виконання» забезпечує структуру, а підхід ReAct — стійкість. Багато продакшн-систем у кінцевому підсумку використовують обидва: планувальник складає загальні кроки, а потім передає кожен складний підзавдання окремому агенту типу ReAct, який оперативно вирішує деталі. Координація кількох таких агентів, створених з нуля без використання фреймворків, є природним наступним кроком після цього циклу.