Основной цикл ReAct для тестирования
Updated Jul 2026
От скриптов к агентам: фундаментальный сдвиг
Традиционная автоматизация тестирования является императивной: вы пишете пошаговые скрипты, которые выполняются детерминированно. Агентное тестирование принципиально отличается. Агент наблюдает состояние системы, рассуждает о следующем действии, действует и оценивает результат -- всё в рамках цикла, который адаптируется к неожиданным ситуациям.
ReAct (Reason + Act) -- это фундаментальный паттерн для агентных систем. Первоначально описанный для LLM-агентов общего назначения, он напрямую отображается на рабочие процессы тестирования.
Четыре фазы
+---------------------------------------------------+
| |
| OBSERVE --> THINK --> ACT --> EVALUATE --> LOOP |
| | | | | |
| Read page Decide Execute Check if |
| state, what to the test assertion |
| logs, test action passed or |
| errors next failed |
| |
+---------------------------------------------------+
Фаза 1: НАБЛЮДЕНИЕ (OBSERVE)
Агент собирает информацию о текущем состоянии тестируемой системы. В браузерном тестировании это означает чтение содержимого страницы, URL, ошибок консоли и сетевой активности. В API-тестировании -- чтение кодов статуса ответов, заголовков и тел ответов.
class Observation:
url: str # Current page URL
text: str # Visible text content (truncated)
errors: list[str] # Console errors
network_requests: list[dict] # Recent HTTP requests
screenshot_path: str | None # Path to current screenshot
page_title: str # Document title
Ключевой инсайт: Качество наблюдения определяет качество рассуждений агента. Богатое наблюдение (текст + ошибки + сеть) даёт лучшие решения, чем скудное (только URL).
Фаза 2: РАЗМЫШЛЕНИЕ (THINK)
Агент анализирует наблюдение и решает, что делать дальше. Именно здесь способность LLM к рассуждению незаменима. Он учитывает:
- Цель теста (что мы пытаемся проверить?)
- Текущее состояние (где мы сейчас?)
- Историю (что мы уже пробовали?)
- Ограничения (сколько шагов осталось? какие действия разрешены?)
prompt = f"""
Objective: {test_objective}
Current URL: {observation.url}
Page content (truncated): {observation.text[:2000]}
Console errors: {observation.errors}
History: {self.history[-5:]} # Last 5 actions
What should I do next? Choose one:
- NAVIGATE <url>
- CLICK <selector>
- TYPE <selector> <text>
- ASSERT <condition>
- DONE <pass|fail> <reason>
"""
decision = self.llm.generate(prompt)
Фаза 3: ДЕЙСТВИЕ (ACT)
Агент выполняет принятое решение на тестируемой системе. Это единственная фаза, которая изменяет состояние.
def execute(self, decision: str) -> ActionResult:
if decision.startswith("NAVIGATE"):
url = decision.split(" ", 1)[1]
self.browser.navigate(url)
elif decision.startswith("CLICK"):
selector = decision.split(" ", 1)[1]
self.browser.click(selector)
elif decision.startswith("TYPE"):
parts = decision.split(" ", 2)
self.browser.type(parts[1], parts[2])
elif decision.startswith("ASSERT"):
condition = decision.split(" ", 1)[1]
return self.evaluate_assertion(condition)
elif decision.startswith("DONE"):
return self.parse_final_result(decision)
Фаза 4: ОЦЕНКА (EVALUATE)
После действия агент оценивает, удалось ли действие и достигнута ли цель теста. Если цель ещё не достигнута, цикл продолжается.
if result.is_terminal:
return TestResult(
status=result.status,
reason=result.reason,
steps_taken=self.step_count,
history=self.history
)
# Otherwise, loop back to OBSERVE
Полная реализация агента ReAct
class ReActTestAgent:
def __init__(self, llm, browser, max_steps=20):
self.llm = llm
self.browser = browser
self.max_steps = max_steps
self.history = []
def run(self, test_objective: str) -> TestResult:
"""Execute a test using the ReAct loop."""
for step in range(self.max_steps):
# OBSERVE
observation = self.browser.get_state()
# THINK
prompt = f"""
Objective: {test_objective}
Current URL: {observation.url}
Page content (truncated): {observation.text[:2000]}
Console errors: {observation.errors}
History: {self.history[-5:]}
What should I do next? Choose one:
- NAVIGATE <url>
- CLICK <selector>
- TYPE <selector> <text>
- ASSERT <condition>
- DONE <pass|fail> <reason>
"""
decision = self.llm.generate(prompt)
# ACT
action_result = self.execute(decision)
self.history.append({
"step": step,
"observation": observation.summary,
"decision": decision,
"result": action_result
})
# EVALUATE
if decision.startswith("DONE"):
return self.parse_result(decision)
return TestResult(
status="TIMEOUT",
reason=f"Exceeded {self.max_steps} steps"
)
Почему ReAct важен для тестирования
Ключевой инсайт: агент не следует фиксированному скрипту. Он адаптирует следующее действие на основе того, что наблюдает. Это создаёт возможности, недоступные детерминированным скриптам:
| Возможность | Скриптовое тестирование | Агентное тестирование ReAct |
|---|---|---|
| Отсутствующий элемент | Тест сразу падает | Агент ищет альтернативный селектор |
| Неожиданное всплывающее окно | Тест падает или блокируется | Агент закрывает окно и продолжает |
| Медленная загрузка страницы | Таймаут (фиксированное ожидание) | Агент наблюдает состояние загрузки, ждёт интеллектуально |
| Изменённый макет | Тест падает (сломанный селектор) | Агент рассуждает о новом макете |
| Состояние ошибки | Тест идёт только по happy path | Агент исследует ошибку |
| Новая функциональность | Тест игнорирует её | Агент замечает и может исследовать |
Пример: адаптивное исследование ошибки
Step 1: OBSERVE — Login page loaded, email and password fields visible
Step 2: THINK — "I should fill in valid credentials and click login"
Step 3: ACT — Type email, type password, click submit
Step 4: OBSERVE — Error banner appeared: "Account locked after 5 failed attempts"
Step 5: THINK — "Unexpected error. This is not a test failure, it is test
environment state. The account was locked by a previous test run.
I should report this as an environment issue."
Step 6: ACT — DONE fail "Account locked - environment state issue, not a bug"
Детерминированный скрипт просто упал бы с ошибкой «Ожидался URL дашборда, получен URL логина». Агент предоставляет диагностическую информацию.
Буфер истории
Буфер истории критически важен для предотвращения бесконечных циклов и обеспечения обучения в рамках одного тестового прогона.
# Keep the last N steps to fit in context window
self.history = self.history[-5:]
# Each history entry includes:
{
"step": 3,
"observation": "Page shows login form with email/password fields",
"decision": "TYPE input[name=email] test@example.com",
"result": "success"
}
Без истории: Агент может кликать одну и ту же кнопку многократно, не помня, что уже пробовал это.
С историей: Агент рассуждает: «Я уже пробовал нажать кнопку отправки, и это не привело к навигации. Дайте-ка проверю, не пропустил ли я ошибку валидации.»
Ограничения ReAct для тестирования
Недетерминированность. Одна и та же цель теста может привести к разным путям в разных запусках. Это преимущество для исследования, но проблема для регрессионного тестирования.
Дороговизна. Каждый шаг требует вызова LLM. 20-шаговый тест стоит в 20 раз больше токенов, чем скрипт.
Медлительность. Задержка LLM (100-500 мс на вызов) накапливается. 20-шаговому агенту требуется 2-10 секунд только на рассуждения, плюс время выполнения действий.
Риск галлюцинаций. Агент может «видеть» несуществующие элементы или неверно интерпретировать содержимое страницы.
Сложность отладки. Когда агент ReAct падает, приходится прослеживать всю историю его рассуждений, чтобы понять причину. Это сложнее, чем отладка линейного скрипта.
Когда использовать ReAct, а когда скрипты
| Используйте ReAct | Используйте скрипты |
|---|---|
| Исследовательское тестирование | Регрессионное тестирование |
| Обнаружение новых функций | Проверка известных потоков |
| Самовосстанавливающиеся наборы тестов | Тесты-шлюзы CI/CD |
| Сложные многошаговые рабочие процессы | Простые CRUD-операции |
| Окружения с частыми изменениями UI | Стабильные окружения |
Ключевой вывод
Цикл ReAct -- это строительный блок всего агентного тестирования. Он превращает тестирование из «следуй этим точным шагам» в «достигни этой цели, адаптируясь к тому, что наблюдаешь». Освойте этот паттерн, и вы поймёте основу каждого инструмента агентного тестирования на рынке.