Перейти к содержимому
2. Агентные циклы

Раздел 2 — Агентные циклы и обработка stop_reason

Что покрывает этот раздел

Как строить центральный поток управления агента Claude: отправить запрос, проверить stop_reason, выполнить инструменты, которые запросил Claude, добавить результаты в историю и повторить. Каждый паттерн более высокого уровня (оркестратор–воркеры, субагенты, оценщик–оптимизатор, Agent SDK) строится поверх этого цикла.

Исходный материал (из официального руководства)

Требуемые знания

  • Жизненный цикл агентного цикла: отправить запрос в Claude, проверить stop_reason ("tool_use" vs "end_turn"), выполнить запрошенные инструменты, вернуть результаты для следующей итерации.
  • Как результаты инструментов добавляются в историю диалога, чтобы модель могла рассуждать о следующем действии.
  • Различие между принятием решений моделью (Claude рассуждает, какой инструмент вызвать дальше, исходя из контекста) и предварительно сконфигурированными деревьями решений (разработчик жёстко прописывает последовательность инструментов).

Требуемые навыки

  • Реализовать поток управления агентного цикла, который продолжается, пока stop_reason == "tool_use", и завершается, когда stop_reason == "end_turn".
  • Добавлять результаты инструментов в контекст диалога между итерациями, чтобы модель могла включать новую информацию в свои рассуждения.
  • Избегать анти-паттернов: разбор естественноязыковых сигналов для завершения цикла, использование произвольного ограничения итераций как основного механизма останова или проверка текстового содержимого ассистента как индикатора завершения.

Агентный цикл от начала до конца

Рабочее определение агента у Anthropic — самое простое в отрасли: “LLMs autonomously using tools in a loop.” Расширенная LLM (модель + инструменты + поиск + память) — базовый строительный блок; каждый паттерн рабочего процесса (последовательность промптов, маршрутизация, параллелизация, оркестратор–воркеры, оценщик–оптимизатор) собирается из него.

  ┌──────────────────────────────────────────────────────────────┐
  │  user prompt + tool definitions  ─────────► messages array   │
  └──────────────────────────────────────────────────────────────┘
  ┌──────────────────────────────────────────────────────────────┐
  │  POST /v1/messages  (Claude reasons about the next action)   │
  └──────────────────────────────────────────────────────────────┘
            ┌──────  inspect response.stop_reason  ──────┐
            │                                            │
   "tool_use"                                       "end_turn"
            │                                            │
            ▼                                            ▼
  ┌───────────────────────────┐               ┌─────────────────┐
  │ 1. append assistant turn  │               │ return final    │
  │    (incl. tool_use blocks)│               │ text to caller  │
  │ 2. execute each tool      │               └─────────────────┘
  │ 3. append a user turn     │
  │    with tool_result blocks│
  │ 4. loop back to /messages │
  └───────────────────────────┘

Разбор одной итерации:

  1. Отправьте messages плюс схему tools на POST /v1/messages.
  2. Claude возвращает сообщение от assistant. Его content — список блоков: ноль или больше блоков text и ноль или больше блоков tool_use. Верхнеуровневый stop_reason обобщает, почему генерация остановилась.
  3. Если stop_reason == "tool_use": добавьте ход ассистента дословно, выполните каждый запрошенный инструмент, добавьте один новый ход от user, чьё содержимое — список блоков tool_result (по одному на tool_use_id), и снова вызовите API с обновлённой историей.
  4. Если stop_reason == "end_turn": модель решила, что задача завершена. Возвращаемся.

Результаты инструментов добавляются в историю диалога, а не отбрасываются после суммирования. Каждый новый запрос несёт всю историю целиком, поэтому Claude может выстраивать рассуждения через множество ходов. Модель — а не ваш код — решает, какой инструмент вызвать следующим, исходя из того, что она наблюдает. Это и есть различие между принятием решений моделью (Claude выбирает инструмент N+1 из текущего контекста) и предварительно сконфигурированными деревьями решений (ваш код статически вызывает tool_a()tool_b()tool_c()). Деревья решений — это рабочие процессы; агентные циклы — это агенты. Опубликованные рекомендации Anthropic предписывают предпочитать более простой рабочий процесс всегда, когда путь можно жёстко прописать.

Значения stop_reason, которые нужно знать

stop_reason присутствует в каждом успешном ответе Messages API. Это единственный сигнал, по которому следует разветвлять решение, продолжать ли цикл. Полный набор задокументированных значений приведён ниже.

ЗначениеСмыслЧто должен делать ваш цикл
end_turnClaude естественно завершил ответ.Выйти из цикла. Вернуть вызывающему коду блоки text из response.content.
tool_useОтвет содержит один или несколько блоков tool_use; Claude ожидает, что вы их выполните.Добавить ход ассистента, выполнить каждый блок tool_use, добавить ход от user с соответствующими блоками tool_result (используйте тот же tool_use_id) и снова вызвать API.
max_tokensВывод достиг параметра max_tokens. Ответ обрезан и может содержать неполный блок tool_use.Обнаружьте обрезание в середине вызова инструмента, проверив, что у последнего блока содержимого type == "tool_use"; повторите запрос с большим max_tokens. Иначе запросите продолжение или покажите предупреждение об обрезании.
stop_sequenceВывод совпал с пользовательской строкой из stop_sequences. Совпавшая последовательность находится в response.stop_sequence.Считать это успешным терминальным остановом для данного паттерна. Продолжить или завершить в зависимости от вашего протокола.
pause_turnСерверный цикл сэмплирования достиг своего ограничения итераций при работе с серверными инструментами (web search, web fetch, code execution и т. п.). Ответ может содержать блок server_tool_use без соответствующего server_tool_result.Добавить ответ ассистента без изменений и снова вызвать API с теми же инструментами. Повторять до тех пор, пока не получите stop reason, отличный от pause_turn.
refusalМодель отказалась по соображениям безопасности (фильтр безопасности API в Sonnet 4.5+ / Opus 4.1+).Не зацикливаться. Показать отказ вызывающему коду; при необходимости перефразировать, перенаправить на другую модель (например, Haiku 4.5) или эскалировать.
model_context_window_exceededГенерация остановилась, потому что ответ достиг полного контекстного окна модели (а не max_tokens). По умолчанию в Sonnet 4.5+; более ранним моделям нужен бета-заголовок.Обрабатывать аналогично max_tokens — ответ корректен, но ограничен. Продолжить, суммировать или сжать контекст.

Ветвление по stop_reasonединственный корректный тест завершения. Не разбирайте текст вида “I’m done” или “Final answer:” — это канонический анти-паттерн, упомянутый ниже.

Эталонные реализации

Python — сырой цикл на Messages API

Минимальная исполняемая форма с использованием Python SDK anthropic (тот же паттерн цикла, который Anthropic показывает в своей документации).

from anthropic import Anthropic

client = Anthropic()
MODEL = "claude-opus-4-7"

tools = [{
    "name": "get_weather",
    "description": "Get current weather for a city.",
    "input_schema": {
        "type": "object",
        "properties": {"location": {"type": "string"}},
        "required": ["location"],
    },
}]

def run_tool(name: str, tool_input: dict) -> str:
    if name == "get_weather":
        return f"Weather in {tool_input['location']}: 72F, clear"
    raise ValueError(f"unknown tool: {name}")

def agent_loop(user_prompt: str) -> str:
    messages = [{"role": "user", "content": user_prompt}]
    while True:
        resp = client.messages.create(
            model=MODEL, max_tokens=4096, tools=tools, messages=messages,
        )
        if resp.stop_reason == "end_turn":
            return "".join(b.text for b in resp.content if b.type == "text")
        if resp.stop_reason == "pause_turn":
            messages.append({"role": "assistant", "content": resp.content})
            continue
        if resp.stop_reason == "tool_use":
            messages.append({"role": "assistant", "content": resp.content})
            tool_results = [
                {"type": "tool_result", "tool_use_id": b.id,
                 "content": run_tool(b.name, b.input)}
                for b in resp.content if b.type == "tool_use"
            ]
            messages.append({"role": "user", "content": tool_results})
            continue
        raise RuntimeError(f"unhandled stop_reason: {resp.stop_reason}")

Замечания: ход ассистента добавляется дословно (блоки tool_use должны сохраниться в истории). Результаты инструментов возвращаются в одном сообщении от user, чей content — это список блоков tool_result, по одному на tool_use_id. pause_turn требует повторной отправки содержимого ассистента без изменений; не синтезируйте результат инструмента сами.

TypeScript — Claude Agent SDK

Для более высокоуровневого Claude Agent SDK от Anthropic (@anthropic-ai/claude-agent-sdk) цикл уже реализован за вас. Вы потребляете асинхронный поток типизированных сообщений и проверяете терминальный ResultMessage.

import { query } from "@anthropic-ai/claude-agent-sdk";

const stream = query({
  prompt: "Find the failing tests in auth.ts and fix them.",
  options: {
    model: "claude-opus-4-7",
    maxTurns: 20,
    maxBudgetUsd: 1.0,
    permissionMode: "acceptEdits",
    allowedTools: ["Read", "Edit", "Bash", "Grep", "Glob"],
  },
});

for await (const message of stream) {
  if (message.type === "assistant") {
    console.log(`turn: ${message.message.content.length} blocks`);
  }
  if (message.type === "result") {
    if (message.subtype === "success") {
      console.log("done:", message.result);
    } else {
      console.error("stopped early:", message.subtype);
    }
  }
}

Внутри SDK запускает тот же цикл, управляемый stop_reason: Claude рассуждает, запрашивает инструменты, SDK их выполняет, результаты автоматически возвращаются обратно, и один полный ход Claude плюс выполнение инструментов — это то, что SDK называет ходом. Цикл заканчивается, когда Claude выдаёт сообщение ассистента без блоков tool_use. maxTurns и maxBudgetUsd — это предохранители, а не основной механизм останова; при срабатывании они выдают ResultMessage с подтипом error_max_turns или error_max_budget_usd.

Анти-паттерны, которых нужно избегать

  • Разбор естественноязыковых сигналов для завершения цикла. Поиск “Final answer:” или “DONE” в response.content хрупок — модель может сформулировать завершение бесконечным числом способов и всё равно захотеть вызвать ещё один инструмент. Правильно: ветвиться только по response.stop_reason.
  • Использование ограничения итераций как основного механизма останова. Жёсткое for _ in range(10): и выход по достижении предела означают, что вы прервёте работу в середине задачи на сложных запросах и потратите токены на простых. Правильно: пусть цикл завершает stop_reason == "end_turn"; держите ограничения итераций и max_budget_usd только как защитные предохранители.
  • Проверка текстового содержимого ассистента для решения о завершении. Один ход может содержать одновременно блоки text и tool_use (Claude может комментировать, одновременно запрашивая инструмент). Трактовка “есть текст” как “готово” приводит к потере вызовов инструментов. Правильно: проверяйте stop_reason; итерируйте по блокам содержимого по их type.
  • Отбрасывание хода ассистента при добавлении результатов инструментов. Отправка результатов инструментов без предварительного добавления хода ассистента с tool_use приводит к некорректному массиву messages и ошибке API. Правильно: добавляйте ход ассистента дословно, затем один ход от пользователя с блоками tool_result.
  • Добавление дополнительного текста после блоков tool_result. Хвостовые блоки text в том же ходе от пользователя приучают Claude ожидать пользовательский текст после каждого вызова инструмента, что приводит к пустым ответам с end_turn. Правильно: ход пользователя после tool_use должен содержать только блоки tool_result.
  • Игнорирование pause_turn. При работе с серверными инструментами сервер достигает собственного ограничения в 10 итераций и возвращает pause_turn без tool_result, который вы могли бы предоставить. Трактовка этого как end_turn обрывает агента. Правильно: добавьте ответ ассистента без изменений и вызовите API снова.
  • Игнорирование обрезания по max_tokens внутри блока tool_use. Если stop_reason == "max_tokens" и последний блок — tool_use, JSON-вход неполон, и повтор с тем же лимитом снова провалится. Правильно: обнаружьте этот случай и повторите запрос с большим max_tokens.
  • Жёстко прописанная последовательность инструментов. Вызовы read_file → search → write_file из вашего собственного кода без участия модели в рассуждении — это рабочий процесс, а не агент. Это нормально, когда путь известен, — но не ждите от него восстановления на новых входах.

Точки фокусировки в стиле экзамена

  • По заданному значению stop_reason определить корректное действие цикла (продолжить с результатами инструментов, добавить и повторно отправить для pause_turn, выйти на end_turn, повторить с увеличенным бюджетом на max_tokens в середине инструмента, показать отказ).
  • Определить, какие изменения messages нужны между итерациями: добавить ход ассистента дословно (включая блоки tool_use), затем добавить ход пользователя с блоками tool_result, привязанными по tool_use_id.
  • Отличать принятие решений моделью от предварительно сконфигурированных деревьев решений и выбирать правильный паттерн для описанной задачи (открытая задача — агент; чётко определённый фиксированный путь — рабочий процесс).
  • Замечать анти-паттерны в примере кода: разбор текста для завершения, ограничение итераций как останов, отсутствующий ход ассистента, лишние блоки text после tool_result, игнорирование pause_turn.
  • Знать, что max_turns / max_budget_usd в Claude Agent SDK — это предохранители; основной завершитель цикла по-прежнему “нет блоков tool_use в ответе ассистента”.

Ссылки

Последнее обновление