Агентный цикл
Слово «агент» звучит как что-то отдельное от обычного API, но это не так. Агент — это тот же messages.create, вызванный в цикле, где между вызовами ваш код выполняет то, о чём попросила модель. В этом уроке разберём этот цикл по шагам на примере агента, который проверяет pull request, и посмотрим, какие правила нужно соблюдать, чтобы цикл не развалился.
Модель без рук
Модель не может сама открыть файл, сходить в GitLab или запустить тесты. Всё, что она умеет, — читать текст и писать текст. Чтобы она «что-то сделала», нужен посредник: ваша программа, которая понимает запрос модели, выполняет действие и возвращает результат.
Договорённость выглядит так. Вы описываете модели список доступных действий — инструментов. Модель, когда ей нужно действие, вместо текста возвращает специальный блок: «вызови инструмент такой-то с такими параметрами». Ваш код видит этот блок, выполняет действие и отправляет результат обратно как новое сообщение. Модель читает результат и решает, что дальше: вызвать ещё один инструмент или написать финальный ответ.
Это и есть агентный цикл. Если вы проходили курс по Claude Code, то видели его в действии: там точно такой же цикл, только инструменты (чтение файлов, bash, поиск) уже написаны за вас. Об этом — в уроке «Как работает Claude Code». Здесь мы пишем цикл сами.
Цикл по шагам
Возьмём ревьюера PR. У него два инструмента: get_diff (получить изменения по номеру PR) и read_file (прочитать файл из репозитория целиком, чтобы понять контекст).
- Отправляем запрос: системный промпт с правилами ревью, список инструментов и сообщение «Проверь PR #482».
- Модель отвечает блоком
tool_use: «вызовиget_diffс{ pr: 482 }».stop_reasonравенtool_use. - Код вызывает API GitLab, получает diff и добавляет в историю два сообщения: ответ модели (как есть, целиком) и сообщение от
userс блокомtool_result, содержащим diff. - Снова отправляем запрос — теперь уже с историей из трёх сообщений.
- Модель видит diff, замечает, что изменённая функция вызывается ещё где-то, и просит
read_fileдля этого файла. Повторяем шаги 3–4. - Модели достаточно информации, она пишет замечания текстом.
stop_reasonравенend_turn. Цикл завершён.
Сколько будет итераций, заранее неизвестно. Это и отличает агента от воркфлоу: порядок действий выбирает модель, а не ваш код.
Как это выглядит в коде
Полное описание инструментов — в следующем уроке, здесь важен сам скелет цикла.
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
async function reviewPullRequest(prNumber: number): Promise<string> {
const messages: Anthropic.MessageParam[] = [
{ role: "user", content: `Проверь pull request #${prNumber}.` },
];
for (let iteration = 0; iteration < 20; iteration++) {
const response = await client.messages.create({
model: "claude-opus-5",
max_tokens: 16000,
system: REVIEW_RULES,
tools: TOOLS,
messages,
});
if (response.stop_reason === "end_turn") {
return response.content
.filter((b): b is Anthropic.TextBlock => b.type === "text")
.map((b) => b.text)
.join("\n");
}
if (response.stop_reason !== "tool_use") {
throw new Error(`Неожиданная остановка: ${response.stop_reason}`);
}
// 1. Ответ модели добавляем в историю целиком
messages.push({ role: "assistant", content: response.content });
// 2. Выполняем все запрошенные инструменты
const results: Anthropic.ToolResultBlockParam[] = [];
for (const block of response.content) {
if (block.type !== "tool_use") continue;
results.push({
type: "tool_result",
tool_use_id: block.id,
content: await runTool(block.name, block.input),
});
}
// 3. Все результаты — одним сообщением от user
messages.push({ role: "user", content: results });
}
throw new Error("Слишком много итераций");
}runTool здесь — обычная функция-диспетчер, которая по имени вызывает нужный API. Ничего специфичного для ИИ в ней нет.
Правила, без которых цикл ломается
Несколько вещей в коде выше сделаны не случайно.
Ответ модели добавляется в историю целиком. Не только текст, а весь response.content: блоки tool_use, блоки рассуждения, всё. Если вы сохраните только текст, в следующем запросе tool_result будет ссылаться на tool_use_id, которого в истории нет, и API вернёт ошибку.
Все результаты — в одном сообщении. Модель может запросить несколько инструментов сразу (например, прочитать три файла). Выполните все и верните все tool_result одним сообщением от user. Если разбивать по одному, модель со временем перестанет делать параллельные вызовы.
Ошибка инструмента — тоже результат. Если GitLab вернул 404, не кидайте исключение из цикла. Верните tool_result с полем is_error: true и текстом ошибки: модель прочитает его и попробует иначе или честно скажет, что PR не найден.
Ограничение на итерации. Без него цикл, в котором инструмент раз за разом возвращает что-то бесполезное, будет крутиться, пока не кончатся деньги. Двадцать итераций для ревью — разумная граница, для других задач подберите свою.
Причины остановки
Цикл управляется полем stop_reason, и его значений стоит знать все.
end_turn— модель закончила. Выход из цикла.tool_use— модель просит инструменты. Выполнить и продолжить.max_tokens— ответ обрезан. Обычно значит, чтоmax_tokensслишком мал для агента; поднимите его.pause_turn— модель сделала паузу в долгой серии серверных инструментов (о них в уроке про встроенные инструменты). Нужно добавить ответ в историю и отправить запрос снова без нового сообщения от пользователя.refusal— модель отказалась продолжать. Прочитайтеstop_detailsи завершите сценарий; повторять запрос бессмысленно.
Где проходит граница ответственности
Модель решает, что вызвать. Ваш код решает, можно ли это вызвать. Инструмент «оставить комментарий в PR» — безобидный, его можно выполнять сразу. Инструмент «влить PR в main» безобидным не является, и перед его выполнением ваш код должен спросить человека или вовсе не давать такой инструмент агенту. Модель не знает вашей политики безопасности; она знает только описания инструментов, которые вы ей дали.
Отсюда практическое правило: чем опаснее действие, тем более узким и отдельным инструментом оно должно быть. Один инструмент bash даёт модели возможность сделать что угодно, но вашему коду — только непрозрачную строку команды, которую невозможно проверить. Отдельный инструмент merge_pr с параметром pr можно поставить на подтверждение, залогировать и посчитать.
Попробуйте сами
10–15 мин на рабочем месте- Нарисуйте на бумаге агентный цикл для одной задачи из вашего проекта: какие инструменты нужны, какие из них опасные, где нужно подтверждение человека.
- Возьмите скелет цикла из урока и подставьте один-единственный инструмент, который возвращает захардкоженную строку. Запустите и посмотрите в логах, сколько итераций сделала модель и как выглядел
tool_useблок. - Заставьте инструмент вернуть
is_error: trueс текстом «сервис недоступен». Посмотрите, как модель отреагирует.
Коротко
- Агент — это
messages.createв цикле; между вызовами ваш код выполняет запрошенные инструменты. - Модель возвращает блок
tool_use, вы отвечаете блокомtool_resultс тем жеtool_use_id. - В историю добавляйте весь
response.content, все результаты возвращайте одним сообщением. - Ошибка инструмента — это
tool_resultсis_error: true, а не исключение. - Цикл управляется
stop_reason:end_turn,tool_use,max_tokens,pause_turn,refusal. - Модель решает, что вызвать; ваш код решает, можно ли. Опасные действия — отдельными узкими инструментами с подтверждением.
Видеоверсия
Сценарий озвучки · 484 слова, ≈ 4 мин
Слово «агент» звучит как что-то особенное, отдельное от обычного API. На самом деле агент — это тот же самый запрос к модели, только в цикле. В этом уроке разберём этот цикл на примере агента, который проверяет pull request.
Начнём с очевидного: модель не может сама открыть файл, сходить в GitLab или запустить тесты. Она умеет читать текст и писать текст. Чтобы она что-то сделала, нужен посредник — ваша программа. Договорённость такая. Вы описываете модели список доступных действий, их называют инструментами. Когда модели нужно действие, она вместо текста возвращает специальный блок: вызови такой-то инструмент с такими параметрами. Ваш код выполняет действие и отправляет результат обратно как новое сообщение. Модель читает результат и решает, что дальше: ещё один инструмент или финальный ответ.
Посмотрим на ревьюера pull request. У него два инструмента: получить изменения по номеру и прочитать файл целиком. Отправляем запрос: правила ревью, список инструментов и просьба проверить пул-реквест четыреста восемьдесят два. Модель отвечает: вызови «получить изменения». Причина остановки — «вызов инструмента». Наш код идёт в GitLab, забирает дифф и добавляет в историю два сообщения: ответ модели целиком и результат инструмента. Отправляем запрос снова. Модель видит дифф, замечает, что изменённая функция вызывается ещё где-то, и просит прочитать тот файл. Повторяем. Наконец, модели хватает информации, она пишет замечания текстом, причина остановки — «закончила». Цикл завершён. Сколько будет итераций — заранее неизвестно. Именно это отличает агента от воркфлоу: порядок действий выбирает модель.
В коде это цикл с ограничением на число итераций. Внутри — запрос к модели. Если она закончила — возвращаем текст. Если просит инструменты — добавляем её ответ в историю целиком, выполняем все запрошенные инструменты, собираем результаты в одно сообщение от пользователя и идём на следующую итерацию.
Несколько правил, без которых цикл ломается. Первое: ответ модели добавляйте в историю целиком, а не только текст. Иначе результат инструмента будет ссылаться на вызов, которого в истории нет, и API вернёт ошибку. Второе: если модель запросила несколько инструментов сразу, верните все результаты одним сообщением. Третье: ошибка инструмента — это тоже результат, с флагом «ошибка», а не исключение. Модель прочитает текст ошибки и попробует иначе. Четвёртое: всегда ограничивайте число итераций, иначе зацикленный агент будет крутиться, пока не кончатся деньги.
Причин остановки пять. Закончила. Просит инструмент. Упёрлась в лимит длины — поднимите его. Пауза в долгой серии серверных инструментов — просто отправьте запрос ещё раз. И отказ — тогда сценарий нужно завершить.
Обратите внимание, что функция-диспетчер, которая по имени инструмента ходит в нужный API, — обычный код без всякой специфики ИИ. Вся «магия» агента — в том, что порядок вызовов этой функции выбирает модель. Если вы умеете писать обработчик HTTP-запросов, вы умеете писать агентный цикл.
И последнее — про границу ответственности. Модель решает, что вызвать. Ваш код решает, можно ли. Оставить комментарий в pull request — безобидно. Влить его в main — нет, и перед этим код должен спросить человека или вообще не давать агенту такой инструмент. Модель не знает вашей политики безопасности, она знает только описания инструментов. Поэтому чем опаснее действие, тем более узким и отдельным инструментом оно должно быть. В следующем уроке разберём, как правильно описывать инструменты.
