04. Автономность и Циклы — ReAct Loop¶
Зачем это нужно?¶
В этой главе мы реализуем паттерн ReAct (Reason + Act) — сердце автономного агента.
Без автономного цикла агент работает как чат-бот: один запрос → один ответ. С автономным циклом агент может выполнить 10 действий подряд для решения одной задачи, самостоятельно принимая решения на основе результатов предыдущих действий.
Реальный кейс¶
Ситуация: Пользователь пишет: "У меня кончилось место на сервере. Разберись."
Без автономного цикла:
- Агент: "Я проверю использование диска" → вызывает
check_disk→ получает "95%" - Агент: [Останавливается, ждет следующей команды пользователя]
С автономным циклом:
- Агент: "Я проверю использование диска" → вызывает
check_disk→ получает "95%" - Агент: "Диск переполнен. Очищу логи" → вызывает
clean_logs→ получает "Освобождено 20GB" - Агент: "Проверю снова" → вызывает
check_disk→ получает "40%" - Агент: "Готово! Освободил 20GB."
Разница: Агент сам решает, что делать дальше, основываясь на результатах предыдущих действий.
Теория простыми словами¶
ReAct Loop (Цикл Автономности)¶
ReAct — это аббревиатура от Reason + Act (Рассуждай + Действуй). Это паттерн, при котором агент:
- Reason (Рассуждает): Анализирует ситуацию и решает, что делать
- Act (Действует): Выполняет действие (вызывает инструмент)
- Observe (Наблюдает): Видит результат действия
- Повторяет: Снова рассуждает на основе результата
Это не магия — это просто цикл, где модель видит результаты предыдущих действий в контексте и генерирует следующий шаг.
Автономный агент работает в цикле:
While (Задача не решена):
1. Отправить историю в LLM
2. Получить ответ
3. ЕСЛИ это текст → Показать пользователю и ждать нового ввода
4. ЕСЛИ это вызов инструмента →
a. Выполнить инструмент
b. Добавить результат в историю
c. GOTO 1 (ничего не спрашивая у пользователя!)
Ключевой момент: Пункт 4.c дает "магию" — агент сам смотрит на результат и решает, что делать дальше. Но это не настоящая магия: модель видит результат инструмента в контексте (messages[]) и генерирует следующий шаг на основе этого контекста.
Замыкание круга¶
После выполнения инструмента мы не спрашиваем пользователя, что делать дальше. Мы отправляем результат обратно в LLM. Модель видит результат своих действий и решает, что делать дальше.
Пример диалога внутри памяти:
Магия vs Реальность: Как работает цикл¶
❌ Магия (как обычно объясняют):
Агент сам решил вызвать
clean_logs()после проверки диска
✅ Реальность (как на самом деле):
Итерация 1: Первый запрос
// messages перед первой итерацией
messages := []openai.ChatCompletionMessage{
{Role: "system", Content: "You are an autonomous DevOps agent."},
{Role: "user", Content: "Кончилось место."},
}
// Отправляем в модель
resp1, _ := client.CreateChatCompletion(ctx, openai.ChatCompletionRequest{
Model: openai.GPT3Dot5Turbo,
Messages: messages,
Tools: tools,
})
msg1 := resp1.Choices[0].Message
// msg1.ToolCalls = [{ID: "call_1", Function: {Name: "check_disk_usage", Arguments: "{}"}}]
// Добавляем ответ ассистента в историю
messages = append(messages, msg1)
// Теперь messages содержит:
// [system, user, assistant(tool_call: check_disk_usage)]
Итерация 2: Выполнение инструмента и возврат результата
// Выполняем инструмент
result1 := checkDiskUsage() // "95% usage"
// Добавляем результат как сообщение с ролью "tool"
messages = append(messages, openai.ChatCompletionMessage{
Role: "tool",
Content: result1, // "95% usage"
ToolCallID: "call_1",
})
// Теперь messages содержит:
// [system, user, assistant(tool_call), tool("95% usage")]
// Отправляем ОБНОВЛЕННУЮ историю в модель снова
resp2, _ := client.CreateChatCompletion(ctx, openai.ChatCompletionRequest{
Model: openai.GPT3Dot5Turbo,
Messages: messages, // Модель видит результат check_disk_usage!
Tools: tools,
})
msg2 := resp2.Choices[0].Message
// msg2.ToolCalls = [{ID: "call_2", Function: {Name: "clean_logs", Arguments: "{}"}}]
messages = append(messages, msg2)
// Теперь messages содержит:
// [system, user, assistant(tool_call_1), tool("95%"), assistant(tool_call_2)]
Итерация 3: Второй инструмент
// Выполняем второй инструмент
result2 := cleanLogs() // "Freed 20GB"
messages = append(messages, openai.ChatCompletionMessage{
Role: "tool",
Content: result2, // "Freed 20GB"
ToolCallID: "call_2",
})
// Теперь messages содержит:
// [system, user, assistant(tool_call_1), tool("95%"), assistant(tool_call_2), tool("Freed 20GB")]
// Отправляем снова
resp3, _ := client.CreateChatCompletion(ctx, openai.ChatCompletionRequest{
Model: openai.GPT3Dot5Turbo,
Messages: messages, // Модель видит оба результата!
Tools: tools,
})
msg3 := resp3.Choices[0].Message
// msg3.ToolCalls = [] // Пусто! Модель решила ответить текстом
// msg3.Content = "Я почистил логи, теперь места достаточно."
// Это финальный ответ - выходим из цикла
Почему это не магия:
- Модель видит всю историю — она не "помнит" прошлое, она видит его в
messages[] - Модель видит результат инструмента — результат добавляется как новое сообщение с ролью
tool - Модель решает на основе контекста — видя "95% usage", модель понимает, что нужно освободить место
- Runtime управляет циклом — код проверяет
len(msg.ToolCalls)и решает, продолжать ли цикл
Ключевой момент: Модель не "сама решила" — она увидела результат check_disk_usage в контексте и сгенерировала следующий tool call на основе этого контекста.
Визуализация: Кто что делает?¶
┌─────────────────────────────────────────────────────────┐
│ LLM (Модель) │
│ │
│ 1. Видит в контексте: │
│ - System Prompt: "Ты DevOps агент" │
│ - User Input: "Кончилось место" │
│ - Tools Schema: [{name: "check_disk", ...}] │
│ │
│ 2. Генерирует tool_call: │
│ {name: "check_disk_usage", arguments: "{}"} │
│ │
│ 3. НЕ выполняет код! Только генерирует JSON. │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ Runtime (Ваш код на Go) │
│ │
│ 1. Получает tool_call из ответа модели │
│ 2. Валидирует: существует ли инструмент? │
│ 3. Выполняет: checkDiskUsage() → "95% usage" │
│ 4. Добавляет результат в messages[]: │
│ {role: "tool", content: "95% usage"} │
│ 5. Отправляет обновленную историю обратно в LLM │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ LLM (Модель) - следующая итерация │
│ │
│ 1. Видит в контексте: │
│ - Предыдущий tool_call │
│ - Результат: "95% usage" ← Runtime добавил! │
│ │
│ 2. Генерирует следующий tool_call: │
│ {name: "clean_logs", arguments: "{}"} │
│ │
│ 3. Цикл повторяется... │
└─────────────────────────────────────────────────────────┘
Ключевой момент: LLM не "помнит" прошлое. Она видит его в messages[], который собирает Runtime.
Реализация цикла¶
for i := 0; i < maxIterations; i++ {
// 1. Отправляем запрос
resp, err := client.CreateChatCompletion(ctx, openai.ChatCompletionRequest{
Model: openai.GPT3Dot5Turbo,
Messages: messages,
Tools: tools,
})
msg := resp.Choices[0].Message
messages = append(messages, msg) // Сохраняем ответ
// 2. Проверяем тип ответа
if len(msg.ToolCalls) == 0 {
// Это финальный текстовый ответ
fmt.Println("Agent:", msg.Content)
break
}
// 3. Выполняем инструменты
for _, toolCall := range msg.ToolCalls {
result := executeTool(toolCall.Function.Name, toolCall.Function.Arguments)
// 4. Добавляем результат в историю
messages = append(messages, openai.ChatCompletionMessage{
Role: openai.ChatMessageRoleTool,
Content: result,
ToolCallID: toolCall.ID,
})
}
// Цикл продолжается автоматически!
// Но это не магия: мы отправляем обновленную историю (с результатом инструмента)
// в модель снова, и модель видит результат и решает, что делать дальше
}
Обработка ошибок в цикле¶
Важно: Не забудьте обрабатывать ошибки и добавлять их в историю! Если инструмент упал, LLM должна это узнать и попробовать что-то другое.
Правильная обработка ошибок:
for _, toolCall := range msg.ToolCalls {
result, err := executeTool(toolCall.Function.Name, toolCall.Function.Arguments)
if err != nil {
// Ошибка — это тоже результат! Добавляем её в историю
result = fmt.Sprintf("Error: %v", err)
}
// Добавляем результат (или ошибку) в историю
messages = append(messages, openai.ChatCompletionMessage{
Role: openai.ChatMessageRoleTool,
Content: result, // Модель увидит ошибку!
ToolCallID: toolCall.ID,
})
}
Что происходит:
- Инструмент возвращает ошибку:
Error: connection refused - Ошибка добавляется в историю как результат инструмента
- Модель видит ошибку в контексте
- Модель может:
- Попробовать другой инструмент
- Сообщить пользователю о проблеме
- Эскалировать проблему
Пример:
Итерация 1:
Action: check_database_status("prod")
Observation: Error: connection refused
Итерация 2 (модель видит ошибку):
Thought: "База недоступна. Проверю сетевую связность"
Action: ping_host("db-prod.example.com")
Observation: "Host is unreachable"
Итерация 3:
Thought: "Сеть недоступна. Сообщу пользователю о проблеме"
Action: [Финальный ответ] "База данных недоступна. Проверьте сетевую связность."
Анти-паттерн: Не скрывайте ошибки от модели!
// ПЛОХО: Скрываем ошибку
if err != nil {
log.Printf("Error: %v", err) // Только в лог
continue // Пропускаем инструмент
}
// ХОРОШО: Показываем ошибку модели
if err != nil {
result := fmt.Sprintf("Error: %v", err)
messages = append(messages, ...) // Добавляем в историю
}
Типовые ошибки¶
Ошибка 1: Зацикливание¶
Симптом: Агент повторяет одно и то же действие бесконечно.
Причина: Нет лимита итераций и детекции повторяющихся действий.
Решение:
// ХОРОШО: Лимит итераций + детекция застревания
for i := 0; i < maxIterations; i++ {
// ...
// Детекция повторяющихся действий
if lastNActionsAreSame(history, 3) {
break
}
}
// ХОРОШО: Улучшите промпт
systemPrompt := `... If action doesn't help, try a different approach.`
Ошибка 2: Результат инструмента не добавляется в историю¶
Симптом: Агент не видит результат инструмента и продолжает выполнять то же действие.
Причина: Результат выполнения инструмента не добавляется в messages[].
Решение:
// ПЛОХО: Результат не добавляется
result := executeTool(toolCall)
// История не обновлена!
// ХОРОШО: ОБЯЗАТЕЛЬНО добавляйте результат!
messages = append(messages, openai.ChatCompletionMessage{
Role: openai.ChatMessageRoleTool,
Content: result,
ToolCallID: toolCall.ID, // Важно для связи!
})
Ошибка 3: Агент не останавливается¶
Симптом: Агент продолжает вызывать инструменты, даже когда задача решена.
Причина: System Prompt не инструктирует агента останавливаться, когда задача решена.
Решение:
// ХОРОШО: Добавьте в System Prompt
systemPrompt := `... If task is solved, answer user with text. Don't call tools unnecessarily.`
Мини-упражнения¶
Упражнение 1: Добавьте детекцию зацикливания¶
Реализуйте проверку, что последние 3 действия одинаковые:
func isStuck(history []ChatCompletionMessage) bool {
// Проверьте, что последние 3 действия одинаковые
// ...
}
Ожидаемый результат:
- Функция возвращает
true, если последние 3 действия одинаковые - Функция возвращает
falseв противном случае
Упражнение 2: Добавьте логирование¶
Логируйте каждую итерацию цикла:
fmt.Printf("[Iteration %d] Agent decided: %s\n", i, action)
fmt.Printf("[Iteration %d] Tool result: %s\n", i, result)
Ожидаемый результат:
- Каждая итерация логируется с номером и действием
- Результаты инструментов логируются
Критерии сдачи / Чек-лист¶
✅ Сдано:
- Агент выполняет цикл автономно
- Результаты инструментов добавляются в историю
- Агент останавливается, когда задача решена
- Есть защита от зацикливания (лимит итераций + детекция)
- Ошибки инструментов обрабатываются и добавляются в историю
❌ Не сдано:
- Агент не продолжает цикл после выполнения инструмента
- Результаты инструментов не видны агенту (не добавляются в историю)
- Агент зацикливается (нет защиты)
- Агент не останавливается, когда задача решена
Связь с другими главами¶
- Инструменты: Как инструменты вызываются и возвращают результаты, см. Главу 03: Инструменты
- Память: Как история сообщений (
messages[]) растет и управляется, см. Главу 09: Анатомия Агента - Безопасность: Как остановить цикл для подтверждения, см. Главу 05: Безопасность
Что дальше?¶
После изучения автономности переходите к:
- 05. Безопасность и Human-in-the-Loop — как защитить агента от опасных действий
Навигация: ← Инструменты | Оглавление | Безопасность →