← Модуль 4: Architect
4.1

Агентный цикл: 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_customer100% гарантия. Ошибка невозможна.

Для критических бизнес-операций (деньги, права доступа, удаление) — всегда программный контроль, не промпт.

Паттерны эскалации при ошибках

Плохо: подагент возвращает "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.

Спроектируй на бумаге:

  1. Какие подагенты нужны? (customer lookup, policy check, refund processor, human escalator?)
  2. Какие инструменты каждому? (ограниченно!)
  3. Какие правила программного контроля? (что ни при каких условиях не делаем промптами?)
  4. Какой stop_reason flow?
  5. Как обрабатываешь ошибку каждого подагента?

В следующих уроках рассмотрим конкретные практики tool design и структурированного вывода.

Что дальше

Урок 4.2: tool design. Почему описания инструментов — главный механизм выбора LLM, и как из-за плохих описаний системы ломаются.