Агентный цикл: stop_reason и hub-and-spokes
Цель: понять как реально работает агентный цикл под капотом. Это фундамент для Architect-уровня.
Что такое агентный цикл
Когда ты пишешь Claude через Agent SDK, вот что происходит:
1. Ты → запрос
2. Claude → ответ с stop_reason
3. Если stop_reason == "tool_use":
- Выполни запрошенные инструменты
- Добавь результаты в историю
- Отправь обратно → goto 2
4. Если stop_reason == "end_turn":
- Финальный ответ, цикл завершён
stop_reason — единственный правильный сигнал завершения
| Значение | Что значит |
|---|---|
"tool_use" | Модель хочет вызвать инструмент, продолжаем цикл |
"end_turn" | Модель закончила, выходим |
"max_tokens" | Упёрлись в лимит, нужно обработать |
"stop_sequence" | Встретили стоп-последовательность |
Антипаттерн: парсить текст ассистента
Плохой код:
if "I'm done" in response.text or "task complete" in response.text:
returnПочему плохо:
- Модель может сказать "I'm done" посреди рассуждения
- Формулировки меняются между моделями/версиями
- Не работает на других языках
Правильно: проверяй stop_reason. Никогда не парси текст.
Как результаты инструментов добавляются в историю
messages.append({"role": "user", "content": [
{
"type": "tool_result",
"tool_use_id": "toolu_01A...",
"content": "42 results found for query"
}
]})Результат — это новое user-сообщение с типом tool_result. Модель читает его на следующей итерации и рассуждает о следующем шаге.
Антипаттерн: произвольные лимиты итераций
# Плохо
for _ in range(10):
response = claude.call()
if stop_reason == "end_turn": breakЖёсткий лимит 10 итераций = задача может быть не решена, цикл прерван.
Правильно: лимит — защитный fallback, не основной механизм. Основной — stop_reason.
Hub-and-spokes — многоагентная оркестрация
[ Координатор ]
/ | \
[Search] [Analyst] [Synth]
(Haiku) (Sonnet) (Sonnet)
Координатор (hub):
- Декомпозирует задачу
- Делегирует подагентам через
Task - Агрегирует результаты
- Выбирает подагентов на основе сложности запроса
Подагенты (spokes):
- Работают в изолированном контексте
- Не наследуют историю координатора
- Возвращают структурированный результат
- Не знают друг о друге
Критическое: контекст передаётся явно
Подагенты не наследуют контекст родителя. Всё что им нужно знать — должно быть в промпте при вызове Task.
# Плохо — подагент не знает контекста
coordinator.call_task("search_agent", prompt="find relevant articles")
# Хорошо — контекст явный
coordinator.call_task("search_agent", prompt="""
User asked about: "AI in creative industries"
Search scope (avoid duplicates across agents):
- Your focus: ACADEMIC PAPERS on digital art, design, film
- Other agents cover: music, literature, visual photography
- Return: 5-10 papers with titles, authors, abstracts, relevance score
""")Параллельное выполнение
Координатор может вызвать несколько Task в одном ответе:
# Эти 3 подагента запускаются параллельно
messages = [{
"role": "assistant", "content": [
{"type": "tool_use", "name": "Task", "input": {"agent": "search", "prompt": "..."}},
{"type": "tool_use", "name": "Task", "input": {"agent": "analyst", "prompt": "..."}},
{"type": "tool_use", "name": "Task", "input": {"agent": "synth", "prompt": "..."}}
]
}]Экономит время vs последовательный вызов.
Конфигурация AgentDefinition
search_agent = AgentDefinition(
name="search",
description="Web search specialist. Returns structured article metadata.",
system_prompt="""You search the web for academic papers.
Return: [{"title", "authors", "abstract", "relevance_score"}]""",
tools=["web_search", "web_fetch"], # минимальный набор!
model="haiku" # дёшево для чтения
)Принципы:
- Ограниченные инструменты — каждому агенту только его роль
- Модель по задаче — Haiku для чтения, Sonnet для анализа, Opus для синтеза
- Чёткое description — чтобы координатор правильно выбирал
Управление через форки
Ты исследуешь подход A, но хочешь попробовать B из той же точки:
claude --resume <session-id> --fork-sessionСоздаёт независимую ветку. Можно параллельно пробовать разные гипотезы.
Когда детерминированный контроль > промпт
Промпт: "пожалуйста, проверяй клиента перед возвратом" → Claude обычно проверит. Но ненулевая вероятность что пропустит.
Программный контроль: PreToolUse hook блокирующий process_refund до успешного get_customer
→ 100% гарантия. Ошибка невозможна.
Для критических бизнес-операций (деньги, права доступа, удаление) — всегда программный контроль, не промпт.
Паттерны эскалации при ошибках
Плохо: подагент возвращает "search unavailable" при таймауте → Координатор теряет возможность восстановления.
Хорошо: структурированный контекст ошибки
{
"success": false,
"error_type": "timeout",
"partial_results": [...],
"attempted_approach": "semantic search with k=10",
"alternative_suggestions": ["try keyword search", "reduce scope"]
}Координатор видит что произошло и может адаптироваться.
Практика (не кодовая — концептуальная, 15 минут)
Представь задачу: построить агента который обрабатывает возвраты в e-commerce.
Спроектируй на бумаге:
- Какие подагенты нужны? (customer lookup, policy check, refund processor, human escalator?)
- Какие инструменты каждому? (ограниченно!)
- Какие правила программного контроля? (что ни при каких условиях не делаем промптами?)
- Какой stop_reason flow?
- Как обрабатываешь ошибку каждого подагента?
В следующих уроках рассмотрим конкретные практики tool design и структурированного вывода.
Что дальше
Урок 4.2: tool design. Почему описания инструментов — главный механизм выбора LLM, и как из-за плохих описаний системы ломаются.