Главная / Статьи

Создание агента ReAct на чистом Python: мышление, действие, наблюдение

Напишите цикл агента ReAct с нуля на Python: контракт «Мысль, Действие, Наблюдение», цикл while с использованием команды PAUSE, обратная связь при ошибках и два способа сбоя в производственной среде.

Издатель: REACTAPP.TOP8 мин чтения1785 слов

Источники: 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 остаются прежними; меняется лишь способ, которым модель решает их использовать.

Вот что делает всю эту конструкцию несколько странной: чтобы модель казалась думающей самостоятельно, необходимо сильно ограничивать её действия.

Контракт, лежащий в основе монолога

Вместо того чтобы просить о ответе, вы принуждаете модель соблюдать строгий формат вывода и не позволяете ей использовать никакие инструменты, пока она не будет следовать этому формату. Цикл состоит из трех этапов:

  1. Мышление. Модель анализирует вопрос, всю собранную ранее информацию и предыдущие неудачи, затем записывает, что делать дальше и почему.
  2. Действие. Исходя из этих размышлений, она выбирает один инструмент из списка и формирует для него точный входной параметр.
  3. Наблюдение. Это ключевой момент передачи контроля. Модель должна остановиться. Ваш код на 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."

Что дает владение этим циклом

Поскольку цикл — это ваш собственный код, вы решаете, что произойдет при сбое. Пример из работы инструмента в стиле Weather показывает, почему это важно.

В одном из случаев модель правильно определяет, что ей нужен инструмент с именем 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, который оперативно разбирается с деталями. Координация нескольких таких агентов, созданных с нуля без использования каких-либо фреймворков, является следующим логичным шагом после применения этого цикла.