1. Что вообще такое агент и зачем он вам
Агенты не являются обязательной частью ChatGPT App: можно создать сколько угодно крутых приложений не используя LLM-агентов. Однако, у меня есть три веские причины рассказать вам про них.
Агенты — это отличный способ добавить интеллект в бэкенд вашего приложения. Умный подбор подарков, анализ текстовых пожеланий пользователя. Сложные сценарии поиска, анализа, обработки и подведения итогов — это всё очень легко делать с помощью LLM-агентов.
ChatGPT выпустил свою AgentsSDK на TS и Python. Она очень хорошая. Оркестрация агентов идёт из коробки. Над одной сложной задачей будет трудиться не один агент, а целая команда. Это очень перспективное направление.
А ведь есть ещё и учебная цель. ChatGPT вызывает mcp-tools точно так же как LLM-агенты вызывают свои tools. Как только вы поймёте как работают LLM-агенты, вам будет понятно, как, например, сделать машину состояний на стороне модели в приложении. Так же, изучение AgentsSDK даёт представление о том, как в будущем будет работать ChatGPT SDK.
Так что приступим.
Что такое LLM-агент
Если ChatGPT App — это красивый и удобный «фронт» вашего сервиса внутри ChatGPT, а MCP‑сервер — это «мотор» с инструментами и бизнес-логикой, то агент — это что-то вроде умного диспетчера, который умеет:
- прочитать цель;
- сам решить, какие инструменты вызывать и в каком порядке;
- при необходимости спросить дополнительные данные;
- повторить шаги, если были ошибки;
- дойти до внятного финального результата.
В формулировке, близкой к официальной документации Agents SDK, агент — это программа. Имея доступ к LLM и набору инструментов, она способна самостоятельно планировать шаги для достижения цели и исполнять их через tool‑calls.
Если провести параллель с тем, что у вас уже есть:
- В обычном ChatGPT App модель ChatGPT оркестрирует вызовы ваших MCP‑инструментов напрямую.
- LLM-агент на бэкенде так же имеет описание задачи, набор инструментов и сам решает, какие tools вызывать, сколько шагов делать, когда остановиться и какой результат вернуть.
В контексте нашего GiftGenius это может выглядеть так:
- Приложение без агента: модель непосредственно вызывает searchGifts, потом filterByBudget, потом getDetails — каждый раз думая заново;
- Приложение с агентом: ChatGPT вызывает mcp‑tool, а бэкенд ставит агенту задачу: «Найди топ‑5 подарков для такого-то профиля». Агент делает несколько шагов: собирает дополнительную информацию, вызывает разные search‑инструменты, фильтрует, сортирует, строит финальные карточки и возвращает уже готовый структурированный ответ.
ChatGPT и LLM-агент на бэкенде — это как директор компании и сотрудник. У ChatGPT гораздо больше свободы: он общается с пользователем и решает какие стратегические задачи запустить (вызвать mcp tools). LLM-агент же работает только на бэкенде, с пользователем не взаимодействует, но тоже «думает» и может вызывать свои tool. Такой себе ChatGPT на минималках.
2. Из чего состоит агент: LLM, инструкции, инструменты и состояние
Удобно думать про агента как про несколько уровней.
Во-первых, под капотом у нас всё та же LLM. Это может быть GPT‑5.1 или другая модель, которую использует Agents SDK. Она генерирует текст, планирует шаги, выбирает инструменты — короче, занимается «мышлением», но уже в вашем контексте оркестрации.
Во-вторых, поверх модели идут инструкции. Это системный промпт агента, который задаёт его роль, границы, стиль общения, способы использования инструментов. Вы уже делали похожее для ChatGPT App, но теперь это применяется к отдельному агенту.
В-третьих, есть набор инструментов агента. Это могут быть:
- функции на TypeScript (классический function calling);
- HTTP/REST‑инструменты;
- обёртки над вашими MCP‑tools, чтобы агент мог ходить в тот же backend, что и ChatGPT App;
- встроенные «hosted» инструменты самого OpenAI (например, web‑search, если вы их подключаете).
И, наконец, есть правила работы со состоянием и шагами: как хранится сессионное состояние (session state), как сохраняются промежуточные результаты, как ограничиваются циклы. Более глубоко про это мы пообщаемся в следующей лекции по памяти и состоянию, но уже сейчас полезно держать в голове, что агент — не «одноразовый запрос», а потенциально длинный процесс с сохранением прогресса.
Если вы посмотрите на это глазами TypeScript‑разработчика, то мысленно получается объект примерно такого вида (псевдокод, близкий к Agents SDK TS):
const giftAgent = new Agent({
model: "gpt-5.1",
systemPrompt: giftAgentPrompt,
tools: { searchGifts, filterGifts, checkoutDraft },
// здесь же — настройки памяти, лимитов шагов и т.п.
});
Сейчас не углубляемся в точные API, важен сам образ: модель, инструкции, инструменты и настройки поведения в одном месте.
3. Роли сообщений: system / user / assistant / tool в мире агента
Вы уже знакомы с классическими ролями system, user, assistant и tool из Chat Completions. В Agents SDK они сохраняются, но получают чуть более прикладной смысл.
Роль system задаёт саму личность и миссию агента. Например, для GiftGenius‑агента это может быть: «Ты — агент подбора подарков. Твоя задача — за минимальное количество шагов подобрать 3–7 релевантных вариантов подарков на основе профиля получателя и бюджета, а затем подготовить структурированный JSON для виджета». Здесь же вы прописываете ограничения: что он не должен делать (например, не выполнять реальные покупки без отдельного шага) и как следует работать с инструментами.
Роль user в контексте агента — это не обязательно «живой человек». Чаще всего это «задание» для агента: цель, сформулированная вашим App, сервисом или другим агентом. Например, ChatGPT App может вызвать агента с user‑сообщением: «Подбери 5 идей подарков для коллеги‑разработчика, бюджет 50 долларов, повод — день рождения».
Роль assistant — это то, что «говорит» сама модель внутри агента. Здесь могут быть как промежуточные рассуждения и планы, так и финальный ответ. Ваша задача — настроить системный промпт так, чтобы эти сообщения были полезны и при необходимости логировались.
Роль tool (или её аналоги в конкретном SDK) описывает результаты вызовов инструментов: «через MCP найдено 50 товаров», «API вернул ошибку таймаута», «БД отдала профиль пользователя». Эти сообщения вместе с assistant‑сообщениями образуют историю run‑цикла агента.
Удобно свести это в маленькую таблицу:
| Роль | Кто говорит | Пример в контексте GiftGenius |
|---|---|---|
|
Вы (как разработчик агента) | «Ты — агент по подбору подарков…» |
|
Внешний вызов (App, другой агент) | «Подбери 5 подарков до 50$…» |
|
Модель внутри агента | «План: 1) спросить детали…» |
|
Результат вызванного инструмента | «searchGifts вернул 20 вариантов…» |
Эта структура важна, потому что именно на её основе строится run‑цикл — главный герой сегодняшней лекции.
4. Как LLM вызывает функции на вашем бэкенде
Когда вы привыкли к режиму «вопрос–ответ», кажется, будто LLM работает по простой схеме: пришёл текст → модель ответила текстом. На самом деле под капотом всё устроено чуть сложнее и именно поэтому работает function calling.
Модель не получает один вопрос, она получает список сообщений — историю диалога. Там уже лежат все предыдущие реплики: системные инструкции («кто ты и что можно/нельзя»), ваши сообщения, прошлые ответы модели, результаты инструментов. На каждый шаг модель смотрит на всю эту ленту, как на чат‑лог, и решает: «Какое следующее сообщение нужно добавить в конец?».
Вот это ключевая мысль: LLM всегда делает один шаг — дописывает следующее сообщение в конец истории. Она не «меняет прошлое» и не редактирует старые сообщения, а просто продолжает список. Вы пишете вопрос, модель отвечает. Вы дописываете второй вопрос, модель опять отвечает, но с учётом всей истории диалога (всех сообщений).
Function calling устроен на этом же принципе. Вместо того чтобы напрямую «запускать функцию», модель делает следующее:
- видит список доступных инструментов/tools и их описания вместе с историей диалога;
- решает: «Сейчас логичнее не просто ответить текстом, а сначала вызвать такой-то инструмент»;
- и в качестве следующего сообщения дописывает в историю не обычный текстовый ответ, а специальное сообщение формата «хочу вызвать такой-то tool с такими-то аргументами».
Дальше уже не модель, а ваш backend читает это новое сообщение в конце истории, понимает, что это запрос на вызов функции, и вызывает нужный инструмент. Затем добавляет в историю ещё одно сообщение — с результатом tool, и снова шлёт полный список сообщений модели. Модель снова смотрит на всю ленту и дописывает следующий шаг: либо ещё один вызов, либо уже финальный человекочитаемый ответ.
То есть:
- для обычного Q&A: «следующее сообщение» = текстовый ответ;
- для function calling: «следующее сообщение» = инструкция вызвать функцию или ответ после использования функции.
Никакой отдельной магической «команды вызвать функцию» нет — это просто особый вид следующего сообщения, которое модель добавляет в конец цепочки.
Модель не вызывает функции вашего бэкенда через публичный API. Она просто «пишет в чат», что хочет вызвать функцию с параметрами. А уже ваш бэкенд вызывает локальную функцию, и её ответ дописывает в чат. И всё начинается по новой.
5. Run‑цикл агента: как он «думает» шаг за шагом
Фактически LLM‑агент — это некий объект/алгоритм на вашем сервере, который вызывает агентный run‑цикл — это расширенный цикл «вопрос → подумать → возможно сделать действие → снова подумать → … → финальный ответ». В документации OpenAI это иногда называют agent loop или ReAct‑паттерном (Reason + Act + Observe).
На концептуальном уровне один run агента выглядит так:
- Агент получает вход: системные инструкции, задание (user‑сообщение), возможно — текущее состояние.
- Модель генерирует шаг: либо текстовый ответ, либо планы и решение вызвать один или несколько инструментов.
- Если модель выбрала tool‑call, агент вызывает соответствующий инструмент в коде (это может быть локальная функция, MCP‑tool, HTTP‑запрос, доступ к БД и т.д.).
- Результаты инструментов добавляются в историю как tool‑сообщения.
- Цикл возвращается к модели с новым контекстом. Модель решает, что делать дальше: продолжать планирование, вызвать другой инструмент или завершить задачу с финальным ответом.
- Когда модель явно или по условиям остановки завершает run, агент возвращает финальный результат вызывающей стороне.
В виде небольшой диаграммы это можно показать так:
flowchart TD
A[Старт run: цель + system] --> B[Вызов модели]
B --> C{Модель хочет
ответить текстом
или вызвать tool?}
C --> D["Текстовый ответ
(assistant)"]
D --> E{Задача завершена?}
E -->|Да| F[Финальный результат]
E -->|Нет| B
C --> G["Tool-call
(описание вызова)"]
G --> H[Вызов функции / MCP / HTTP]
H --> I["Tool-результат
(tool message)"]
I --> B
Если перевести это в упрощённый TypeScript‑псевдокод (далёкий от реального API, но логически верный), получится что-то вроде:
async function runAgent(goal: string) {
let context = buildInitialContext(goal);
while (!isFinished(context)) {
const decision = await callLLM(context); // шаг агента
if (decision.type === "tool_call") { //вызвать функцию?
const toolResult = await callTool(decision.tool, decision.args); //вызываем локальную функцию
context = appendToolResult(context, toolResult); // добавляем результат в конец списка
} else {
context = appendAssistantMessage(context, decision.message);
}
enforceLimits(context); // лимиты шагов/времени/циклов
}
return extractFinalResult(context);
}
Agents SDK берёт на себя большую часть этой рутины: хранение истории, маршаллинг tool‑calls, логику повторов и т.п. Вам остаётся настроить конфигурацию и реализовать сами инструменты.
Run vs step
Важно различать два понятия:
- run — это один запуск агента для какой-то цели: «подбери подарки для этого случая»;
- step — один шаг run‑цикла: конкретный вызов модели, который может привести к текстовому ответу или к tool‑call.
В мониторинге вы будете видеть именно множество шагов внутри одного run, а лимиты по безопасности и стоимости часто задаются либо «на run», либо «на шаг».
Теперь, когда понятно, как агент живёт внутри одного run и шагает по run‑циклу, давайте посмотрим, где вообще есть смысл всё это городить, а где достаточно простых tools.
5. Где в GiftGenius нужен агент и где он лишний
Прежде чем кидаться писать агента на всё, полезно задать себе честный вопрос: «А он вообще здесь нужен?».
Хороший сценарий для агента — это многошаговая задача с ветвлениями, повторами и логикой, которую неудобно держать только в промптах.
В GiftGenius такой задачей может быть «мастер умного подбора подарков», который:
- умеет доспрашивать важные детали (пол получателя, хобби, уровень близости);
- может обратиться к нескольким источникам товаров (разные вендоры через MCP‑tools);
- фильтрует и ранжирует результаты;
- при ошибках источников повторяет попытку или идёт по запасному пути;
- возвращает не просто список текстов, а структурированный список кандидатов с объяснениями и ссылками на SKU из product feed.
Здесь агент будет действительно полезен как «оркестратор», особенно если потом вы захотите дополнить это voice/Realtime‑сценарием или сложной коммерцией (ACP).
А вот для простого вызова getGiftDetails(giftId) агент не нужен: обычный MCP‑tool, вызываемый напрямую из ChatGPT, полностью покрывает кейс. То же самое с банальными «одноступенчатыми» сценариями вроде «скажи описание этого подарка по тексту продуктовой карточки».
Общий здравый подход такой: если можно описать сценарий как «один нормальный tool» — скорее всего, агента не нужно. Если же вы начинаете явно расписывать многошаговый workflow с проверками и повторными попытками, с большой вероятностью агент вас порадует.
6. Детерминизм: как сделать поведение агента предсказуемым
Детерминизм в мире LLM‑агентов — штука хитрая. Теоретически, при одинаковом входе и одинаковых настройках вы хотите получать одинаковый план действий и одинаковую последовательность tool‑calls. На практике модель остаётся стохастической, но у вас есть несколько рычагов управления предсказуемостью.
Во-первых, классика: температура и прочие параметры генерации. Чем ниже температура, тем меньше креатива и тем больше «послушности» модели. Для агента по подбору подарков вы, скорее всего, захотите иметь не нулевой, но и не слишком высокий уровень свободы, иначе модель будет каждое утро придумывать новый способ вызвать один и тот же инструмент.
Во-вторых, чёткие системные инструкции. Если вы размыто описываете поведение вроде «ты можешь звать разные инструменты и делать что хочешь», не удивляйтесь, что агент будет то прыгать по API, то пытаться отвечать «с потолка». Гораздо лучше явно описывать, когда именно следует вызывать инструмент, какие параметры допустимы, как интерпретировать ошибки и в каких случаях нужно завершать задачу.
Например, системный промпт для GiftGenius‑агента может включать фрагмент:
Если у тебя нет полного профиля получателя (возраст, пол, повод, примерный бюджет),
сначала задай уточняющие вопросы через user-facing канал и дождись ответов.
Только после этого вызывай инструмент search_gifts с заполненным профилем.
Не придумывай товары с потолка, всегда опирайся на результаты инструментов.
Такие указания снижают вариативность решений и делают поведение более детерминированным.
В-третьих, дизайн самих инструментов. Если у вас есть три инструмента, которые «примерно одно и то же» ищут подарки, модель неизбежно будет иногда выбирать один, иногда другой. Лучше спроектировать инструменты с чёткими, непересекающимися зонами ответственности и прописать это в описаниях.
Наконец, можно использовать guardrails — правила и схемы, которые проверяют действия агента и результаты модели. В Agents SDK есть встроенная поддержка проверок и ограничений, в том числе на структуру выходных данных. Если модель пытается сгенерировать что-то не по схеме, вы можете мягко её поправить или даже повторить шаг заново.
Мини‑пример: фиксируем формат результата
Допустим, вам нужно, чтобы агент всегда возвращал JSON с полем gifts, а внутри были объекты с id, title и score. Вы можете:
- описать эту схему на уровне агента;
- указать, что final output должен ей соответствовать;
- в случае нарушения — повторить шаг или вернуть безопасную ошибку.
Псевдокод:
const giftResultSchema = z.object({
gifts: z.array(z.object({
id: z.string(),
title: z.string(),
score: z.number().min(0).max(1),
}))
});
// В конфиге агента
const agent = new Agent({
/* ... */
outputSchema: giftResultSchema,
});
Когда модель попытается вернуть что-то странное, runner сообщит об ошибке валидации, а вы сможете либо перепросить модель, либо логировать инцидент.
7. Идемпотентность: почему агент может вызвать ваш API дважды
Если детерминизм — это про «одинаковый план при одинаковых входах», то идемпотентность — про безопасность повторов. В контексте агентов она критически важна по двум причинам.
Во-первых, у вас появляется ещё один уровень ретраев: не только HTTP‑клиенты и load‑балансеры, но и сам агент может решать повторить вызов инструмента, если получил ошибку или неполный результат. Во-вторых, в реальных продакшен‑сценариях добавляются webhooks, очереди, стриминговые каналы — и вы можете случайно обработать один и тот же логический шаг несколько раз.
Вы уже обсуждали идемпотентность на уровне MCP‑tools: не совершать двойные платежи, не создавать один и тот же заказ дважды, использовать idempotency keys в запросах. Сейчас всё то же самое, но умноженное на многошаговую природу агента.
Представим, что в GiftGenius появился инструмент create_checkout_session, который по списку выбранных подарков создаёт draft‑чекаут в ACP/Stripe. Если агент решит повторить этот вызов из‑за сетевой ошибки, вы очень не хотите два отдельных заказа и два списания денег.
Значит, нужно:
- придумывать внешний idempotency key для каждого логического действия (например, runId + stepIndex или явно сгенерированный checkoutDraftId);
- передавать его в ваш backend/ACP‑endpoint;
- на стороне backend’а проверять, не обрабатывали ли вы уже этот ключ, и возвращать сохранённый результат вместо повторного выполнения.
Псевдопример на TypeScript:
async function createCheckoutDraft(runId: string, payload: DraftPayload) {
const key = `gift-checkout-${runId}`;
const existing = await findDraftByKey(key);
if (existing) return existing;
const draft = await stripe.checkout.sessions.create({
/* ... */,
idempotencyKey: key, // либо свой слой поверх
});
await saveDraftWithKey(key, draft);
return draft;
}
Теперь, даже если агент по какой-то причине вызовет этот инструмент дважды с тем же runId, ваш код останется идемпотентным: один и тот же логический шаг → один и тот же фактический результат.
«Сначала проверка, потом действие»
Второй распространённый паттерн идемпотентности — сначала проверять состояние, потом действовать. Например, прежде чем создавать заказ, проверить, не существует ли уже заказ с таким clientReferenceId или тем же набором параметров. Это особенно удобно в долгих workflow, где агент может «забыть», что уже делал что‑то на предыдущем шаге.
Safe‑mode/Fake‑mode
На стадии разработки полезно иметь «безопасный режим» для опасных инструментов: вместо реального действия они только логируют, что было бы сделано, и возвращают псевдорезультат. Для агентов это удобный способ прогнать run‑цикл в боевом окружении, не рискуя деньгами или данными.
8. Мини‑практика: описываем агента GiftGenius человеческим языком
Мы уже поговорили про run‑цикл, детерминизм и идемпотентность инструментов. Давайте на минуту оторвёмся от кода и проверим, как всё это складывается в живой сценарий.
Сейчас полезно сделать маленькое упражнение на бумаге (или в голове), без кода.
Представьте, что вы описываете простого агента:
-
: ты — помощник по подбору подарков; всегда уточняешь важные детали, не придумываешь товары из головы, а используешь только результаты инструментов.system -
: хочу подарок коллеге до $50.user
Опишите словами, какие шаги такой агент должен сделать.
Типичный сценарий может выглядеть так.
- Сначала агент проверяет, достаточно ли информации. Если нет, он задаёт уточняющие вопросы: чем коллега примерно занимается (дизайнер, разработчик, менеджер), есть ли какие-то табу (алкоголь, шутливые подарки), есть ли ограничения по доставке. Ответы попадают либо в session state, либо в параметры вызова инструмента.
- Затем агент вызывает инструмент search_gifts с заполненным профилем: «коллега‑разработчик, бюджет 50, категория — гаджеты и офис». Инструмент возвращает список кандидатов с ценами, категориями и ID товаров.
- Дальше агент может вызвать дополнительный инструмент filter_gifts_by_constraints, если выяснилось, что часть товаров нельзя доставить в нужный регион, или вручную отфильтровать в своём промпте. После этого он сортирует варианты по релевантности и стоимости, возможно, добавляя свои комментарии («подходит, если коллега любит кофе», «хороший вариант для удалёнки»).
- Наконец, агент подготавливает финальный структурированный ответ для ChatGPT App: список 5–7 подарков с brief‑описаниями, подсказками по использованию и ссылками на Checkout (или на следующий шаг — создание draft‑чекаута).
Где здесь нужны tool‑calls? Очевидно, в поиске и фильтрации товаров, в проверке доступности и в создании draft‑чекаута. Какие шаги должны быть идемпотентными? В первую очередь все операции, связанные с заказами и деньгами — создание draft‑чекаута, возможно, запись истории в БД.
9. Типичные ошибки при первых шагах с агентами
Ошибка №1: агент как «второй ChatGPT без ограничений».
Иногда хочется просто выдать модели ещё один промпт и назвать это «агентом». В результате получается штука, которая генерирует много текста, хаотично вызывает инструменты и плохо поддаётся контролю. Чтобы этого избежать, важно чётко описывать роль агента в system, ограничивать список инструментов и думать о нём именно как об оркестраторе с конкретной миссией, а не как о «второй вселенной текстогенерации».
Ошибка №2: отсутствие идемпотентности в инструментах.
Разработчики нередко переносят свои старые HTTP‑handlers под агента «как есть», не учитывая, что теперь runner может автоматически повторять вызовы. В случае с платежами и заказами это может привести к очень неприятным последствиям. Правильный подход — сразу проектировать инструменты так, чтобы повторный вызов с тем же логическим ключом не приводил к повторному действию.
Ошибка №3: слишком креативные настройки модели.
Высокая температура отлично подходит для придумывания тостов и стихов, но для агента, который должен надёжно оркестрировать многошаговые процессы, это путь к непредсказуемому поведению: модель будет каждый раз выбирать разные инструменты, генерировать отличающиеся планы и иногда вообще забывать, что у неё есть tools. Стоит относиться к агентам как к «служебным» сущностям и держать их в более строгом режиме.
Ошибка №4: инструмент «на все случаи жизни».
Иногда хочется сделать один универсальный tool вроде execute_any_sql или do_anything_with_orders, а потом дать его в руки агенту. В сочетании с LLM‑креативностью это почти гарантированная угроза безопасности. Гораздо лучше иметь несколько узкоспециализированных инструментов с чёткими контрактами и правами, чем один «божественный» с полными правами на всё.
Ошибка №5: отсутствие явных критериев завершения run.
Если агенту не объяснить, когда нужно остановиться, он может начать впадать в бесконечные или полубесконечные циклы: ещё раз проверить результаты, ещё раз переспросить пользователя, ещё раз попробовать вызвать инструмент при той же ошибке. Часто это проявляется только под нагрузкой, когда одна из зависимостей нестабильна. Правильный способ — задавать лимиты на количество шагов, время run и количество повторов при одинаковой ошибке, а также описывать в system, что агент должен «честно сдаваться», когда исчерпал разумные опции.
Ошибка №6: хранение всего и сразу в состоянии агента.
Поскольку Agents SDK упрощает работу с session state, есть соблазн складывать туда всё подряд: большие документы, необработанные логи, чувствительные данные. Это раздувает контекст, увеличивает стоимость и создаёт риски безопасности. Состояние агента должно хранить только то, что действительно нужно для продолжения работы; всё остальное — в БД, логах и других слоях, причём с учётом приватности.
Ошибка №7: попытка использовать агента там, где достаточно простого MCP‑tool.
Иногда разработчики начинают с агента, даже если задача — просто вызвать одну функцию и вернуть результат. Это добавляет сложность, где она не нужна: появляются run‑цикл, состояние, дополнительные логи и потенциальные точки отказа. Если сценарий укладывается в один tool‑call без сложного workflow, лучше оставить его таким и подключать агента только когда появляется реальная многошаговость.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ