Claude Certified Architect · Модуль 1 · Урок 1.1

Проектирование агентного цикла для автономного выполнения задач

Суть

Агентный цикл (agentic loop) завершается только по структурному признаку — полю stop_reason в ответе API, а не по тексту ответа. Claude сигнализирует о завершении значением stop_reason == "end_turn". Любой разбор текста («ищем слово done») ненадёжен: формулировки недетерминированы.

Жизненный цикл итерации

  1. Отправить запрос с историей сообщений и описанием инструментов.
  2. Проверить stop_reason. Если end_turn — задача завершена, выходим.
  3. Если tool_use — выполнить запрошенные инструменты.
  4. Добавить в историю сначала ход ассистента, затем ход пользователя с tool_result, и повторить.

Почему не текст и не «нет tool_use»

Ответ без блоков tool_use — это всё ещё корректный ход рассуждения с stop_reason == "end_turn". Проверка «нет блоков tool_use → выходим» ошибочно срабатывает на промежуточных текстовых ходах и обрывает работу. Предельное число итераций — это страховка от зацикливания, а не основной критерий выхода.

Корректный агентный цикл (production pattern)
func runAgent(client *Client, tools []Tool, userInput string) (*Response, error) {
	const maxIterations = 25
	messages := []Message{{Role: "user", Content: userInput}}

	for i := 0; i < maxIterations; i++ {
		resp, err := client.CreateMessage(MessageRequest{
			Model:     "claude-opus-4-8",
			MaxTokens: 4096,
			Tools:     tools,
			Messages:  messages,
		})
		if err != nil {
			return nil, err
		}

		// Единственный надёжный сигнал завершения — stop_reason.
		if resp.StopReason == "end_turn" {
			return resp, nil
		}

		if resp.StopReason == "tool_use" {
			// Сначала ход ассистента, затем ход пользователя с результатами.
			messages = append(messages, Message{Role: "assistant", Content: resp.Content})

			var toolResults []ContentBlock
			for _, block := range resp.Content {
				if block.Type == "tool_use" {
					output := executeTool(block.Name, block.Input)
					toolResults = append(toolResults, ContentBlock{
						Type:      "tool_result",
						ToolUseID: block.ID,
						Content:   output,
					})
				}
			}
			messages = append(messages, Message{Role: "user", Content: toolResults})
		}
	}
	return nil, errors.New("достигнут предел итераций — это страховка, а не штатное завершение")
}

Anti-patterns

ЛовушкаПочему не работаетВерный паттерн
Завершение по тексту (ищем «done»)Формулировки недетерминированы и меняютсяПроверять только stop_reason == "end_turn"
Предел итераций как основной критерий выходаМолча обрезает корректную работуПредел — страховка; внутри проверять stop_reason
Не добавлять ход ассистента перед результатамиЛомает чередование user/assistantСначала ход ассистента, затем tool_result
Жёсткое дерево решений вместо рассужденияЛишает модель возможности выбирать инструментДать Claude выбирать инструменты по контексту

Exam traps

ЛовушкаПочему не работаетВерный паттерн
Разбирать текст ответа на сигналы завершенияClaude может сформулировать иначе или употребить слово в середине работыОпираться исключительно на stop_reason
Проверять отсутствие блоков tool_useТекстовый ход тоже валиден и не означает завершенияСмотреть только поле stop_reason
Опускать ход ассистента перед tool_resultAPI требует чередования ходов; структура портитсяДобавить ход ассистента, затем ход пользователя с результатами

Практическое задание (T1)

  • Построить цикл, где единственный критерий выхода — stop_reason.
  • Добавить предел в 25 итераций с явным предупреждающим логом.
  • Проверить цепочку из 3 последовательных вызовов инструментов и убедиться, что все результаты добавляются в историю.
  • Намеренно сломать цикл, опустив ход ассистента, и увидеть ошибку API.
  • Добавить ветку завершения по тексту и доказать, что она срабатывает неверно.

Проверка знаний

Агент поддержки клиентов

Цикл реализован с проверкой: «если в ответе нет блоков tool_use — выйти». При нагрузочном тестировании часть обращений обрывается на середине расследования. В чём корень проблемы?

  • A Описания инструментов слишком расплывчаты
  • B Условие завершения неверно — нужно проверять stop_reason == "end_turn"
  • C Нужно увеличить предел итераций
  • D Результаты инструментов не добавляются в историю

Многоагентная исследовательская система

Цикл завершается так: if "research complete" in response.content[0].text: break. Иногда работа обрывается раньше времени, иногда не завершается вовсе. Что исправить?

  • A Сравнение чувствительно к регистру — добавить .lower()
  • B Проверять все блоки контента, а не только первый
  • C Завершение по тексту в принципе ненадёжно — использовать stop_reason == "end_turn"
  • D Внедрить нужную фразу в system prompt для стабильности