Claude Certified Architect · Модуль 1 · Урок 1.1
Проектирование агентного цикла для автономного выполнения задач
Суть
Агентный цикл (agentic loop) завершается только по структурному признаку — полю stop_reason в ответе API, а не по тексту ответа. Claude сигнализирует о завершении значением stop_reason == "end_turn". Любой разбор текста («ищем слово done») ненадёжен: формулировки недетерминированы.
Жизненный цикл итерации
- Отправить запрос с историей сообщений и описанием инструментов.
- Проверить
stop_reason. Еслиend_turn— задача завершена, выходим. - Если
tool_use— выполнить запрошенные инструменты. - Добавить в историю сначала ход ассистента, затем ход пользователя с
tool_result, и повторить.
Почему не текст и не «нет tool_use»
Ответ без блоков tool_use — это всё ещё корректный ход рассуждения с stop_reason == "end_turn". Проверка «нет блоков tool_use → выходим» ошибочно срабатывает на промежуточных текстовых ходах и обрывает работу. Предельное число итераций — это страховка от зацикливания, а не основной критерий выхода.
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_result | API требует чередования ходов; структура портится | Добавить ход ассистента, затем ход пользователя с результатами |
Практическое задание (T1)
- Построить цикл, где единственный критерий выхода —
stop_reason. - Добавить предел в 25 итераций с явным предупреждающим логом.
- Проверить цепочку из 3 последовательных вызовов инструментов и убедиться, что все результаты добавляются в историю.
- Намеренно сломать цикл, опустив ход ассистента, и увидеть ошибку API.
- Добавить ветку завершения по тексту и доказать, что она срабатывает неверно.
Проверка знаний
Агент поддержки клиентов
Цикл реализован с проверкой: «если в ответе нет блоков tool_use — выйти». При нагрузочном тестировании часть обращений обрывается на середине расследования. В чём корень проблемы?
Верный ответ: B
B верно. Claude способен выдать чисто текстовый ход рассуждения с stop_reason == "end_turn" — так же, как при настоящем завершении. Проверка «нет tool_use» ошибочно срабатывает на таких промежуточных ходах и обрывает работу. A и C лечат не тот слой. D привёл бы к зацикливанию/повторам, а не к раннему обрыву.
Многоагентная исследовательская система
Цикл завершается так: if "research complete" in response.content[0].text: break. Иногда работа обрывается раньше времени, иногда не завершается вовсе. Что исправить?
Верный ответ: C
C верно. Текст недетерминирован: Claude может написать фразу в середине работы («этот раздел готов, перехожу дальше…») или завершить без неё. Единственный структурный и надёжный сигнал — stop_reason. A и B лишь латают симптомы. D не делает текст детерминированным.