Разработка ИИ-агентов · Модуль 1 · Урок 1.4

Агентный цикл с нуля: стоп-сигнал, выполнение, дозапись, safety-cap

Один оборот цикла

Теперь соберём части в рабочий цикл. Один его оборот выглядит так:

  1. Send — отправляем модели текущую историю сообщений и список инструментов.
  2. Проверка стоп-сигнала — смотрим, почему модель остановилась. Если она готова ответить — выходим. Если просит инструменты — идём дальше.
  3. Выполнение инструмента — наш код выполняет запрошенные вызовы и собирает результаты.
  4. Дозапись результата — добавляем в историю сначала ход ассистента (с запросом вызовов), затем результаты ролью tool, и повторяем оборот.

Цикл крутится, пока модель не скажет «я закончила». Каждый оборот — это один вызов API.

Останавливаемся по структуре, а не по тексту

Самое важное правило надёжного агента: завершение определяется структурным сигналом, а не разбором текста ответа. В OpenAI-совместимом API это поле finish_reason: значение tool_calls означает «нужно выполнить инструменты», stop — «модель закончила, это финальный ответ». В нативном API Anthropic аналогичную роль играет stop_reason со значениями tool_use и end_turn.

Почему нельзя «искать слово done» в тексте? Формулировки недетерминированы: модель может написать «готово», «done», «всё сделано» или вообще закончить молча. Текстовые эвристики ломаются непредсказуемо. Структурное поле — единственный надёжный признак. Обратите внимание: ответ без запроса инструментов — это уже финал (stop/end_turn), даже если в нём нет слова «ответ».

Порядок дозаписи и роль tool

Порядок сообщений в истории критичен, и его нельзя нарушать. Сначала в историю добавляется ход ассистента — то самое сообщение, в котором модель запросила вызовы (с их id). Только потом — по одному сообщению с ролью tool на каждый вызов, и каждое ссылается на свой tool_call_id. Если перепутать порядок или потерять id, провайдер вернёт ошибку или модель «потеряет нить».

Так замыкается контур: модель попросила — код выполнил — результат вернулся модели — модель учла его на следующем обороте. Память при этом растёт: каждый оборот добавляет сообщения. За управлением этим ростом — отдельный урок Модуля 2.

Safety-cap: страховка, а не условие выхода

Основной выход из цикла — структурный стоп-сигнал. Но в реальности модель может зациклиться: вызывать инструмент за инструментом без прогресса. Поэтому добавляют safety-cap — жёсткий лимит на число оборотов (например, 10–25). Достигли лимита — прерываемся с понятной ошибкой.

Подчеркнём: safety-cap — это страховка от зацикливания, а не нормальное условие завершения. Если агент регулярно упирается в лимит, проблема не в числе — а в описаниях инструментов, промпте или самой задаче. Лимит спасает кошелёк и время, но не заменяет правильный стоп-сигнал.

Агентный цикл: стоп по finish_reason, выполнение инструментов, дозапись, safety-cap
// runAgent крутит цикл, пока модель не закончит или не сработает safety-cap.
func runAgent(messages []Message, tools []Tool) (string, error) {
    const maxTurns = 12 // safety-cap: страховка от зацикливания

    for turn := 0; turn < maxTurns; turn++ {
        choice, err := callLLM(messages, tools) // 1. send
        if err != nil {
            return "", err
        }
        messages = append(messages, choice.Assistant) // дозапись хода ассистента

        // 2. проверка стоп-сигнала
        if choice.FinishReason != "tool_calls" {
            return choice.Assistant.Content, nil // финал: stop/end_turn
        }

        // 3. выполнение инструментов + 4. дозапись результатов
        for _, tc := range choice.Assistant.ToolCalls {
            result := dispatch(tc.Name, tc.ArgsJSON) // наш код выполняет вызов
            messages = append(messages, Message{
                Role:       "tool",
                ToolCallID: tc.ID, // обязательная привязка к запросу
                Content:    result,
            })
        }
    }
    return "", fmt.Errorf("agent: достигнут предел в %d ходов без завершения", maxTurns)
}

Anti-patterns

Анти-паттернПочему плохоКак правильно
Определять конец по тексту («ищем done»)Формулировки недетерминированы — эвристика ломаетсяСтоп по структуре: finish_reason == "stop" / stop_reason == "end_turn"
Считать ответ без tool_calls «незавершённым»Это и есть финал — агент зависнет или зациклитсяНет запроса инструментов → это финальный ответ, выходим
Не дозаписывать результаты ролью toolМодель не видит итог вызова — теряет контекстВернуть каждый результат сообщением tool с верным tool_call_id
Цикл без safety-capЗацикливание, расход токенов, зависаниеЖёсткий лимit оборотов как страховка + честная ошибка при достижении
Делать safety-cap основным условием выходаМаскирует проблему промпта/инструментовНормальный выход — по стоп-сигналу; лимит лишь подстраховывает

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

  • Реализуйте функцию runAgent, выходящую по finish_reason, а не по тексту.
  • Добавьте выполнение инструментов и дозапись результатов ролью tool с правильным tool_call_id.
  • Введите maxTurns (например, 10) и верните понятную ошибку при его достижении.
  • Проверьте сценарий с двумя последовательными вызовами инструментов в рамках одной задачи.
  • Сломайте порядок дозаписи намеренно и понаблюдайте за ошибкой API — так закрепится важность порядка.

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

Агент завершает цикл по условию: «если в тексте ответа есть слово "готово" — останавливаемся».

В чём главный изъян?

  • A Слово "готово" слишком короткое
  • B Завершение нужно определять по структурному сигналу (finish_reason/stop_reason), а текст недетерминирован
  • C Нужно искать слово "done" вместо "готово"
  • D Проблемы нет, подход корректен

Зачем агенту safety-cap на число итераций, если есть стоп-сигнал?

  • A Это основной способ завершить агента
  • B Это страховка от зацикливания и неконтролируемого расхода токенов, когда модель не сходится к ответу
  • C Он ускоряет каждый отдельный вызов модели
  • D Он повышает качество ответов

В каком порядке нужно дозаписывать сообщения после запроса инструментов?

  • A Сначала результаты tool, потом ход ассистента
  • B Сначала ход ассистента (с tool_calls), затем по одному tool-сообщению на каждый вызов с его tool_call_id
  • C Только результаты tool, ход ассистента можно опустить
  • D Порядок и id не важны