Разработка ИИ-агентов · Модуль 1 · Урок 1.4
Агентный цикл с нуля: стоп-сигнал, выполнение, дозапись, safety-cap
Один оборот цикла
Теперь соберём части в рабочий цикл. Один его оборот выглядит так:
- Send — отправляем модели текущую историю сообщений и список инструментов.
- Проверка стоп-сигнала — смотрим, почему модель остановилась. Если она готова ответить — выходим. Если просит инструменты — идём дальше.
- Выполнение инструмента — наш код выполняет запрошенные вызовы и собирает результаты.
- Дозапись результата — добавляем в историю сначала ход ассистента (с запросом вызовов), затем результаты ролью
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 — это страховка от зацикливания, а не нормальное условие завершения. Если агент регулярно упирается в лимит, проблема не в числе — а в описаниях инструментов, промпте или самой задаче. Лимит спасает кошелёк и время, но не заменяет правильный стоп-сигнал.
// 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 — так закрепится важность порядка.
Проверка знаний
Агент завершает цикл по условию: «если в тексте ответа есть слово "готово" — останавливаемся».
В чём главный изъян?
Верный ответ: B
B верно. Текстовые эвристики ненадёжны: модель может закончить без любого ключевого слова или, наоборот, употребить его в середине. Надёжно — структурное поле: finish_reason == "stop" (или stop_reason == "end_turn"). C меняет одну хрупкую эвристику на другую.
Зачем агенту safety-cap на число итераций, если есть стоп-сигнал?
Верный ответ: B
B верно. Safety-cap защищает от бесконечного цикла и трат, но это страховка, а не нормальный выход. Штатно агент завершается по стоп-сигналу. A путает страховку с основным механизмом; C и D к лимиту не относятся.
В каком порядке нужно дозаписывать сообщения после запроса инструментов?
Верный ответ: B
B верно. Сначала фиксируем ход ассистента с запросами вызовов, затем привязываем каждый результат к своему tool_call_id. Нарушение порядка или потеря id ведут к ошибке провайдера и потере контекста.