Искусственный интеллект
Агенты с искусственным интеллектом объяснили, что такое цикл реагирования и как он работает.
Как агенты рассуждают, действуют и наблюдают за тем, как шаг за шагом приходят к окончательному ответу.
Мария Мушутци
3 июля 2026 г.
Читать 12 мин.
Делиться
Изображение создано автором с помощью ChatGPT Images 2.0
В моём последнем посте мы говорили о вызове инструмента. Вызов инструмента — это механизм, который позволяет модели искусственного интеллекта решать, какую функцию необходимо использовать и с какими аргументами, вместо того чтобы просто генерировать текст в качестве выходных данных. К концу этого поста у нас была настройка, которая могла выбирать getcurrentweather или convert_currency, или выполнять оба сразу — вызывая их параллельно, — или ни то ни другое, а просто генерировать текст. Другими словами, модель решает, что ей нужно делать дальше, мы (остальной код) выполняем это решение, передаём обратно результат модели, и в итоге модель предоставляет пользователю информированный ответ в текстовом формате.
Более продвинутая версия этого цикла не останавливается после одного раунда выбора модели — выполнения кода — передачи обратно результата — ответа модели. Вместо того чтобы генерировать ответ в конце, модель может использовать результат одного вызова инструмента, чтобы решить, следует ли вызывать следующий инструмент и какой именно. Как уже упоминалось в конце статьи о вызове инструмента, это цикл ReAct (Reason + Act), который позволяет агентам обрабатывать задачи, которые невозможно решить за один вызов.
Но что это будет за задача? В примере с параллельным звонком в предыдущем посте мы спрашивали: какая погода в Афинах и сколько стоит 100 долларов в евро? Это две разные вещи, требующие использования двух разных инструментов для получения ответа, но они также независимы друг от друга. Другими словами, мы можем ответить на эти два вопроса независимо, одновременно, не нуждаясь в какой‑либо информации из первого вопроса, чтобы ответить на второй.
Но что, если мы спросим что‑то вроде: «Я поспорил со своим другом на 100 евро, что сегодня в Афинах будет дождь. Если я выиграю, сколько это будет в долларах?» В данном случае модель не сможет решить, нужно ли ей вызывать convertcurrency, пока сначала не вызовет getcurrent_weather и не выяснит, действительно ли шёл дождь. Проще говоря, ответ на второй вопрос полностью зависит от результата первого. Это именно та зависимость, которую параллельный вызов инструмента не может разрешить за один раунд, и именно для этого создан цикл ReAct.
Итак, давайте посмотрим!
🍨 DataCream — это новостная рассылка об искусственном интеллекте, данных и технологиях. Если вам интересны эти темы, подпишитесь на неё здесь!
Но что такое цикл ReAct?
Цикл ReAct — это всего лишь три последовательно повторяющихся шага:
Причина
Действие
Наблюдать

В начале цикла модель определяет, какая информация ей уже известна и какой дополнительной информации не хватает для того, чтобы предоставить правильный ответ на запрос пользователя. Затем она вызывает соответствующий инструмент, чтобы получить эту недостающую информацию. Наконец, как только соответствующий вызов инструмента выполнен и его результат передан обратно в модель, модель анализирует результат (добавляет результат инструмента в свой контекст). Затем она снова возвращается к рассуждениям — с той разницей, что на этот раз новое наблюдение находится в её контексте. Этот цикл повторяется до тех пор, пока модель не решит, что имеющейся информации достаточно для ответа на запрос пользователя. На этом этапе она прекращает вызывать tools и просто отвечает текстом.
Но разве это не похоже на вызовы инструментов, которые мы уже знаем? В некотором роде, но не совсем. То, что отличает это от того, что мы описывали в статье о вызове инструмента, — это сам цикл. При одном вызове инструмента модель запрашивает что‑то, получает это, и на этом транзакция завершается в рамках этого вызова. В цикле ReAct диалог остаётся открытым, поскольку каждое новое наблюдение становится новым контекстом для следующего шага рассуждения, и модель может изменить свой план, основываясь на том, что она только что узнала.
Те же инструменты, новый трюк.
Чтобы конкретизировать это, давайте вернёмся к примеру со ставкой из вступления и подумаем, что на самом деле должна делать модель, чтобы дать нам достоверный ответ. Вопрос таков: «Я поспорил со своим другом на 100 евро, что сегодня в Афинах будет дождь. Если я выиграю, сколько это будет в долларах США?» Обратите внимание на условное выражение в середине: «если я выиграю». Нужно ли вообще конвертировать какую‑либо валюту в модели, зависит от того, что возвращает прогноз погоды. Если шёл дождь, модель должна вызвать convertcurrency, указав 100 евро в качестве входного параметра, и вернуть конвертированный выигрыш. Если дождя не было, ставка считается проигранной, convertcurrency не имеет значения, и модель должна просто напрямую вернуть соответствующий текст, не прибегая к повторному вызову.
Иными словами, модель действительно не может заранее спланировать всю последовательность вызовов инструмента. Сначала она должна проверить погоду, посмотреть на результат, проанализировать, что этот результат означает для условия ставки, и только после этого решить, нужен ли повторный вызов инструмента. В отличие от параллельного вызова инструмента, который хорошо зарекомендовал себя при ответе на вопросы «Какая погода в Афинах?» и «Сколько стоят 100 долларов в евро?», для этого вопроса требуется цикл.
Самое приятное в цикле ReAct то, что для него не нужны новые инструменты. Мы всё ещё можем использовать те же функции, только по‑другому. Итак, мы собираемся использовать getcurrentweather и convert_currency точно так же, как создали их в прошлый раз, используя Open‑Meteo для погоды и Frankfurter для конвертации валют (оба по‑прежнему не требуют ключа API):
import requests
import json
from openai import OpenAI
client = OpenAI(api_key="your_api_key")
def get_current_weather(city: str, unit: str = "celsius") -> dict:
# Step 1: geocode the city name to coordinates
geo = requests.get(
"https://geocoding-api.open-meteo.com/v1/search",
params={"name": city, "count": 1}
).json()
lat = geo["results"][0]["latitude"]
lon = geo["results"][0]["longitude"]
# Step 2: fetch current weather
weather = requests.get(
"https://api.open-meteo.com/v1/forecast",
params={
"latitude": lat,
"longitude": lon,
"current": "temperature_2m,precipitation",
"temperature_unit": unit
}
).json()
return {
"city": city,
"temperature": weather["current"]["temperature_2m"],
"precipitation_mm": weather["current"]["precipitation"],
"unit": unit
}
def convert_currency(amount: float, from_currency: str, to_currency: str) -> dict:
response = requests.get(
f"https://api.frankfurter.dev/v2/rate/{from_currency}/{to_currency}"
).json()
rate = response["rate"]
converted = round(amount * rate, 2)
return {
"amount": amount,
"from_currency": from_currency,
"to_currency": to_currency,
"converted_amount": converted,
"rate": rate
}
Обратите внимание на одно небольшое дополнение по сравнению с прошлым разом: функция getcurrentweather теперь также возвращает значение precipitation_mm, поскольку это поле необходимо модели для оценки условий ставки. Всё остальное остаётся прежним. Схема инструментов также не изменилась по сравнению с нашим предыдущим постом:
tools = [
{
"type": "function",
"function": {
"name": "get_current_weather",
"description": "Get the current weather for a given city, including temperature and precipitation",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "The name of the city"},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
},
"required": ["city"]
}
}
},
{
"type": "function",
"function": {
"name": "convert_currency",
"description": "Convert an amount from one currency to another",
"parameters": {
"type": "object",
"properties": {
"amount": {"type": "number", "description": "The amount to convert"},
"from_currency": {"type": "string", "description": "The source currency code, e.g. EUR"},
"to_currency": {"type": "string", "description": "The target currency code, e.g. USD"}
},
"required": ["amount", "from_currency", "to_currency"]
}
}
}
]
Нам также нужно определить поисковый словарь, который наш код будет использовать для передачи выбора инструмента модели в реальную функцию Python:
available_functions = {
"get_current_weather": get_current_weather,
"convert_currency": convert_currency
}
Это позволяет нам перейти от имени инструмента, которое модель возвращает нам в виде строки, к реальной функции Python, которую мы запускаем. Это сопоставление понадобится нам в ближайшее время, поскольку на этот раз мы не знаем заранее, сколько вызовов инструмента нам придётся разрешить, и даже не знаем, будет ли их больше одного.
Наблюдаем за ходом цикла.
Вот часть, которая на самом деле является новой. Вместо того чтобы делать один запрос и считывать вызов инструмента, мы завершаем весь обмен циклом. На каждом этапе мы отправляем модели полный диалог на данный момент, проверяем, запрашивала ли она инструмент, запускаем этот инструмент, если да, добавляем результат и повторяем цикл. Мы останавливаемся только тогда, когда модель отвечает обычным текстом и больше не остаётся вызовов инструментов.
messages = [
{
"role": "user",
"content": "I bet my friend 100 EUR that it would rain in Athens today. If I won, how many USD is that?"
}
]
max_iterations = 5
for i in range(max_iterations):
print(f"--- Step {i + 1}: Reason ---")
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
tools=tools
)
message = response.choices[0].message
messages.append(message)
# If there's no tool call, the model is ready to answer
if not message.tool_calls:
print("Final answer:")
print(message.content)
break
# Otherwise, act on every tool call the model requested
for tool_call in message.tool_calls:
function_name = tool_call.function.name
function_args = json.loads(tool_call.function.arguments)
print(f"--- Step {i + 1}: Act ({function_name}) ---")
print(f"Calling {function_name} with {function_args}")
function_response = available_functions[function_name](**function_args)
print(f"--- Step {i + 1}: Observe ---")
print(function_response)
# Feed the observation back in so the next Reason step can use it
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": json.dumps(function_response)
})
Также обратите внимание на ограничение max_iterations, которое не позволяет модели, решившей, что ей нужна «ещё одна информация», бесконечно выполнять цикл. Это особенно важно, потому что мы платим за каждый вызов модели в каждом из этих циклов.
В конечном счёте итоговое наблюдение цикла добавляется в виде сообщения role: «tool», привязанного к определённому идентификатору toolcallid. Это позволяет модели сопоставить каждый результат с вызвавшим его действием.
И теперь, когда мы всё настроили, мы наконец можем увидеть цикл ReAct в действии.
Итак, наш вопрос о ставке может быть решён двумя способами — в зависимости от того, какая погода на самом деле. Давайте рассмотрим оба варианта.
- Если бы в Афинах шёл дождь, наш код вывел бы в терминале что‑то вроде следующего:
--- Step 1: Reason ---
--- Step 1: Act (get_current_weather) ---
Calling get_current_weather with {'city': 'Athens'}
--- Step 1: Observe ---
{'city': 'Athens', 'temperature': 17.4, 'precipitation_mm': 3.2, 'unit': 'celsius'}
--- Step 2: Reason ---
--- Step 2: Act (convert_currency) ---
Calling convert_currency with {'amount': 100, 'from_currency': 'EUR', 'to_currency': 'USD'}
--- Step 2: Observe ---
{'amount': 100, 'from_currency': 'EUR', 'to_currency': 'USD', 'converted_amount': 108.5, 'rate': 1.085}
--- Step 3: Reason ---
Final answer:
It did rain in Athens today (3.2mm of precipitation), so you won the bet!
Your 100 EUR comes out to 108.50 USD at today's exchange rate.
- И если бы в Афинах не было дождя, мы получили бы следующую распечатку:
--- Step 1: Reason ---
--- Step 1: Act (get_current_weather) ---
Calling get_current_weather with {'city': 'Athens'}
--- Step 1: Observe ---
{'city': 'Athens', 'temperature': 34.1, 'precipitation_mm': 0.0, 'unit': 'celsius'}
--- Step 2: Reason ---
Final answer:
Unfortunately, it did not rain in Athens today, so it looks like you lost the bet.
No currency conversion needed!
Посмотрите, что произошло во втором сценарии: цикл запустился ровно один раз. Модель заметила, что значение precipitationmm равно 0,0, пришла к выводу, что условие ставки не выполнено, и остановилась, даже не вызвав convertcurrency. Никто не говорил ей пропускать второй вызов инструмента, но она, скорее, решила это сама, основываясь исключительно на том, что наблюдала при первом запуске цикла.
Это основное различие (по крайней мере, для этого простого сценария) между параллельным вызовом инструмента и циклом ReAct. При параллельном вызове инструмента мы не смогли бы завершить весь процесс раньше времени и не выполнить вызов convert_currency. Вместо этого при параллельной настройке оба инструмента были бы вызваны заранее, а модель позже составила бы окончательный ответ. Это особенно важно, потому что, помните, мы платим за каждый вызов модели. Таким образом, возможность архитектурно ограничить количество вызовов модели искусственного интеллекта до необходимого минимума, избежав лишних вызовов, очень важна.
Итак, когда цикл ReAct действительно превосходит параллельный вызов инструмента?
Ответ таков: всякий раз, когда количество вызовов инструмента или аргументы для этих вызовов можно определить только после просмотра более раннего результата.
В нашем примере с bet модель не может решить, следует ли вообще вызывать convertcurrency, пока getcurrent_weather не сообщит, шёл ли дождь. Никакие предварительные рассуждения не помогут решить эту проблему, потому что информации просто ещё нет в мире модели. Мы должны выйти за пределы мира модели, взять внешние данные из weather API и добавить их в контекст модели. Напротив, параллельный вызов инструмента предполагает, что модель уже знает, что ей нужно, до того как инициировать какие‑либо вызовы инструмента. Цикл ReAct не требует этого допущения: он позволяет модели определять, что ей нужно, по ходу работы.
В частности, цикл ReAct выигрывает у параллельного вызова инструмента в следующих случаях:
- когда один результат является условием того, нужен ли вообще другой вызов — как в примере с bet;
- когда аргументы для последующего вызова зависят от значения, возвращённого предыдущим вызовом. Например, если бы модели сначала нужно было узнать, какая валюта используется в городе, прежде чем вызывать convert_currency с правильным кодом;
- когда возвращается неожиданный результат — например, пользователь может указать название города, которое не соответствует геокодированию, или API возвращает ошибку, и модели необходимо адаптировать свой план, а не просто сообщить о полученном результате.
Тем не менее в простом случае, когда все необходимые инструменты и их аргументы очевидны уже из сообщения пользователя, параллельный вызов инструмента на самом деле является лучшим выбором — так мы сокращаем число обращений туда и обратно, уменьшаем задержку и при этом получаем тот же результат.
Для меня самая интересная часть перехода от параллельного вызова инструментов к циклу ReAct — это то, как мало на самом деле потребовалось кода 😅: цикл for, оператор if и поиск по словарю. Тем не менее этот небольшой объём кода творит чудеса. Этот цикл реагирования в той или иной форме — реальный механизм, стоящий за большинством того, что люди подразумевают под «агентом».
✨ Спасибо, что прочитали! ✨
Если вы зашли так далеко, то, возможно, вам пригодится pialgorithms — платформа, которую мы разрабатываем. Она помогает командам безопасно управлять организационными знаниями в одном месте.
Понравился этот пост? Присоединяйтесь ко мне в 💌Substack и 💼LinkedIn.
Все изображения принадлежат автору, если не указано иное.
Автор:
Мария Мушутци
Смотрите все статьи Марии Мушутци.
Искусственный интеллект, наука о данных, программирование, Python, технологии.
Поделитесь этой статьёй.
Поделитесь на Facebook.
Поделитесь на LinkedIn.
Поделитесь на X.
Towards Data Science — публикация сообщества. Поделитесь своими идеями с нашей глобальной аудиторией и зарабатывайте с помощью авторской платёжной программы TDS.
Пишите для TDS.