Разбираем агентный цикл Codex
@ai_longreadsOpenAI рассказывает о внутреннем устройстве Codex CLI: как работает агентный цикл, как происходит взаимодействие между пользователем, моделью и инструментами, и какие оптимизации позволяют эффективно управлять контекстом.
Это AI-перевод статьи, сделанный каналом Про AI: Лучшие Статьи и Исследования.
Разбираем агентный цикл Codex
Unrolling the Codex agent loop Автор: Michael Bolin Оригинальный текст:
Codex CLI — это наш кроссплатформенный локальный программный агент, разработанный для создания качественных и надёжных изменений в коде при безопасной и эффективной работе на вашем компьютере. С момента первого запуска CLI в апреле мы многому научились в создании программных агентов мирового класса. Чтобы поделиться этими знаниями, мы начинаем серию публикаций, в которых рассмотрим различные аспекты работы Codex, а также уроки, которые мы извлекли. Для ещё более детального понимания архитектуры Codex CLI загляните в наш репозиторий с открытым исходным кодом: https://github.com/openai/codex. Многие детали наших проектных решений задокументированы в issues и pull requests на GitHub.
Начнём с агентного цикла (agent loop) — ключевой логики Codex CLI, которая отвечает за оркестрацию взаимодействия между пользователем, моделью и инструментами, которые модель вызывает для выполнения полезной работы с кодом.
Небольшое уточнение по терминологии: в OpenAI под названием «Codex» объединено несколько продуктов для программных агентов, включая Codex CLI, Codex Cloud и расширение Codex для VS Code. Этот пост посвящён harness Codex — ядру агентного цикла и логике исполнения, которые лежат в основе всех продуктов Codex и доступны через Codex CLI.
Агентный цикл
В сердце каждого ИИ-агента находится «агентный цикл». Упрощённая иллюстрация выглядит так:
Для начала агент получает входные данные от пользователя, чтобы включить их в набор текстовых инструкций для модели — prompt (промпт).
Следующий шаг — запрос к модели путём отправки инструкций и получения ответа. Этот процесс называется inference (инференс, вывод модели). Во время инференса текстовый промпт сначала преобразуется в последовательность входных tokens (токенов) — целых чисел, которые индексируют словарь модели. Затем эти токены используются для сэмплирования модели, генерируя новую последовательность выходных токенов.
Выходные токены преобразуются обратно в текст, который становится ответом модели. Поскольку токены генерируются инкрементально, это преобразование может происходить по мере работы модели — поэтому многие приложения на базе LLM отображают потоковый вывод. На практике инференс обычно инкапсулирован за API, который работает с текстом, абстрагируя детали токенизации.
В результате инференса модель либо (1) выдаёт финальный ответ на исходный запрос пользователя, либо (2) запрашивает вызов инструмента (tool call), который агент должен выполнить (например, «запусти ls и сообщи результат»). В случае (2) агент выполняет вызов инструмента и добавляет его вывод к исходному промпту. Этот вывод используется для генерации нового запроса к модели; агент может учесть эту новую информацию и продолжить работу.
Этот процесс повторяется, пока модель не перестанет генерировать вызовы инструментов и вместо этого не выдаст сообщение для пользователя (называемое assistant message в моделях OpenAI). Во многих случаях это сообщение напрямую отвечает на исходный запрос пользователя, но иногда это уточняющий вопрос.
Поскольку агент может выполнять вызовы инструментов, которые изменяют локальное окружение, его «выход» не ограничивается сообщением ассистента. Во многих случаях основной результат работы программного агента — это код, который он пишет или редактирует на вашем компьютере. Тем не менее каждый ход всегда завершается сообщением ассистента — например, «Я добавил architecture.md, как вы просили» — что сигнализирует о завершении агентного цикла. С точки зрения агента его работа выполнена, и управление возвращается пользователю.
Путь от входных данных пользователя до ответа агента, показанный на диаграмме, называется одним ходом (turn) разговора (thread в терминологии Codex). Хотя этот ход может включать много итераций между инференсом модели и вызовами инструментов. Каждый раз, когда вы отправляете новое сообщение в существующий разговор, история разговора включается в промпт для нового хода, включая сообщения и вызовы инструментов из предыдущих ходов.
Это означает, что по мере роста разговора увеличивается и длина промпта для сэмплирования модели. Эта длина важна, потому что каждая модель имеет context window (контекстное окно) — максимальное количество токенов, которое она может использовать за один вызов инференса. Заметьте, что это окно включает как входные, так и выходные токены. Как вы можете догадаться, агент может сделать сотни вызовов инструментов за один ход, потенциально исчерпав контекстное окно. По этой причине управление контекстным окном — одна из многих обязанностей агента. Теперь давайте разберёмся, как Codex реализует агентный цикл.
Инференс модели
Codex CLI отправляет HTTP-запросы к Responses API для запуска инференса модели. Рассмотрим, как информация проходит через Codex, который использует Responses API для управления агентным циклом.
Эндпоинт Responses API, который использует Codex CLI, настраивается, поэтому его можно использовать с любым эндпоинтом, реализующим Responses API:
- При использовании логина ChatGPT с Codex CLI используется эндпоинт
https://chatgpt.com/backend-api/codex/responses - При использовании аутентификации по API-ключу с моделями OpenAI используется
https://api.openai.com/v1/responses - При запуске Codex CLI с
--ossдля использования gpt-oss с ollama 0.13.4+ или LM Studio 0.3.39+ по умолчанию используетсяhttp://localhost:11434/v1/responses, работающий локально - Codex CLI можно использовать с Responses API, размещённым у облачного провайдера, например Azure
Рассмотрим, как Codex формирует промпт для первого вызова инференса в разговоре.
Построение начального промпта
Как конечный пользователь, вы не задаёте промпт для сэмплирования модели дословно при запросе к Responses API. Вместо этого вы указываете различные типы входных данных как часть запроса, а сервер Responses API решает, как структурировать эту информацию в промпт, который модель способна обработать. Промпт можно представить как «список элементов»; в этом разделе мы объясним, как ваш запрос преобразуется в этот список.
В начальном промпте каждый элемент списка связан с ролью. role указывает, какой вес должен иметь связанный контент, и принимает одно из следующих значений (в порядке убывания приоритета): system, developer, user, assistant.
Responses API принимает JSON-payload с множеством параметров. Сосредоточимся на трёх:
- `instructions`: системное (или developer) сообщение, вставляемое в контекст модели
- `tools`: список инструментов, которые модель может вызывать при генерации ответа
- `input`: список текстовых, графических или файловых входных данных для модели
В Codex поле instructions читается из model_instructions_file в ~/.codex/config.toml, если указан; иначе используются base_instructions, связанные с моделью. Инструкции для конкретных моделей хранятся в репозитории Codex и встроены в CLI (например, gpt-5.2-codex_prompt.md).
Поле tools — это список определений инструментов, соответствующих схеме Responses API. Для Codex это включает инструменты, предоставляемые Codex CLI, инструменты Responses API, которые должны быть доступны Codex, а также пользовательские инструменты, обычно через MCP-серверы:
[
// Стандартный shell-инструмент Codex для запуска локальных процессов
{
"type": "function",
"name": "shell",
"description": "Runs a shell command and returns its output...",
"strict": false,
"parameters": {
"type": "object",
"properties": {
"command": {"type": "array", "description": "The command to execute", ...},
"workdir": {"description": "The working directory...", ...},
"timeout_ms": {"description": "The timeout for the command...", ...},
...
},
"required": ["command"],
}
},
// Встроенный инструмент планирования Codex
{
"type": "function",
"name": "update_plan",
"description": "Updates the task plan...",
...
},
// Инструмент веб-поиска от Responses API
{
"type": "web_search",
"external_web_access": false
},
// MCP-сервер для получения погоды, настроенный в
// пользовательском ~/.codex/config.toml
{
"type": "function",
"name": "mcp__weather__get-forecast",
"description": "Get weather alerts for a US state",
...
}
]Наконец, поле input JSON-payload — это список элементов. Codex вставляет следующие элементы в input перед добавлением сообщения пользователя:
- Сообщение с
role=developer, описывающее песочницу, которая применяется только кshell-инструменту Codex, определённому в секцииtools. Другие инструменты, например от MCP-серверов, не изолируются Codex и сами отвечают за свои ограничения.
- (Опционально) Сообщение с
role=developer, содержащее значениеdeveloper_instructionsиз пользовательскогоconfig.toml.
- (Опционально) Сообщение с
role=userс «пользовательскими инструкциями», которые агрегируются из нескольких источников. В общем случае более специфичные инструкции появляются позже:
* Содержимое AGENTS.override.md и AGENTS.md в $CODEX_HOME
* С учётом лимита (32 KiB по умолчанию) проверяется каждая папка от корня Git/проекта cwd до самого cwd: добавляется содержимое AGENTS.override.md, AGENTS.md или файлов, указанных в project_doc_fallback_filenames в config.toml
* Если настроены skills: краткое введение о навыках, метаданные каждого навыка и раздел по использованию навыков
- Сообщение с
role=user, описывающее локальное окружение агента. Это указывает текущую рабочую директорию и shell пользователя.
После всех этих вычислений для инициализации input Codex добавляет сообщение пользователя для начала разговора.
После сборки полного JSON-payload для отправки в Responses API выполняется HTTP POST запрос с заголовком Authorization в зависимости от конфигурации эндпоинта в ~/.codex/config.toml.
Когда сервер Responses API OpenAI получает запрос, он использует JSON для формирования промпта модели следующим образом (пользовательская реализация Responses API может сделать другой выбор):
Как видите, порядок первых трёх элементов промпта определяется сервером, а не клиентом. При этом из этих трёх элементов только содержимое системного сообщения также контролируется сервером, поскольку tools и instructions определяются клиентом. За ними следует input из JSON-payload для завершения промпта.
Теперь, когда у нас есть промпт, мы готовы сэмплировать модель.
Первый ход
Этот HTTP-запрос к Responses API инициирует первый «ход» разговора в Codex. Сервер отвечает потоком Server-Sent Events (SSE). data каждого события — JSON-payload с "type", начинающимся с "response":
data: {"type":"response.reasoning_summary_text.delta","delta":"ah ", ...}
data: {"type":"response.reasoning_summary_text.delta","delta":"ha!", ...}
data: {"type":"response.reasoning_summary_text.done", "item_id":...}
data: {"type":"response.output_item.added", "item":{...}}
data: {"type":"response.output_text.delta", "delta":"forty-", ...}
data: {"type":"response.output_text.delta", "delta":"two!", ...}
data: {"type":"response.completed","response":{...}}Codex потребляет поток событий и переиздаёт их как внутренние объекты событий для клиента. События вроде response.output_text.delta используются для поддержки потокового вывода в UI, тогда как события вроде response.output_item.added преобразуются в объекты, добавляемые к input для последующих вызовов Responses API.
Предположим, первый запрос к Responses API включает два события response.output_item.done: одно с type=reasoning и одно с type=function_call. Эти события должны быть представлены в поле input JSON при повторном запросе к модели с ответом на вызов инструмента.
Оглядываясь на нашу первую диаграмму агентного цикла, мы видим, что может быть много итераций между инференсом и вызовами инструментов. Промпт может расти, пока мы наконец не получим сообщение ассистента, означающее конец хода:
data: {"type":"response.output_text.done","text": "I added a diagram to explain...", ...}
data: {"type":"response.completed","response":{...}}В Codex CLI мы показываем сообщение ассистента пользователю и фокусируем поле ввода, чтобы указать, что теперь очередь пользователя продолжить разговор.
Соображения производительности
Вы можете спросить: «Погодите, разве агентный цикл не квадратичен по объёму JSON, отправляемого в Responses API за время разговора?» И вы будете правы. Хотя Responses API поддерживает опциональный параметр previous_response_id для смягчения этой проблемы, Codex не использует его сегодня — в основном для сохранения полной бессессионности запросов и поддержки конфигураций Zero Data Retention (ZDR).
Избегание previous_response_id упрощает жизнь провайдеру Responses API, гарантируя, что каждый запрос бессессионный. Это также позволяет легко поддерживать клиентов с Zero Data Retention (ZDR), поскольку хранение данных для поддержки previous_response_id противоречило бы ZDR. Клиенты ZDR не теряют возможность использовать проприетарные reasoning-сообщения из предыдущих ходов, поскольку связанный encrypted_content может быть расшифрован на сервере.
В целом стоимость сэмплирования модели доминирует над стоимостью сетевого трафика, что делает сэмплирование основной целью наших усилий по оптимизации. Вот почему так важен prompt caching (кэширование промптов) — он позволяет переиспользовать вычисления из предыдущего вызова инференса. При попадании в кэш сэмплирование модели линейно, а не квадратично. Наша документация по кэшированию промптов объясняет это подробнее:
Попадания в кэш возможны только для точных совпадений префикса в промпте. Чтобы получить преимущества кэширования, размещайте статический контент, такой как инструкции и примеры, в начале промпта, а переменный контент, специфичный для пользователя, — в конце. Это также относится к изображениям и инструментам, которые должны быть идентичны между запросами.
С учётом этого рассмотрим, какие операции могут вызвать «промах кэша» в Codex:
- Изменение
tools, доступных модели в середине разговора - Изменение
model, являющейся целью запроса Responses API (на практике это меняет третий элемент исходного промпта, содержащий инструкции для конкретной модели) - Изменение конфигурации песочницы, режима подтверждения или текущей рабочей директории
Команда Codex должна быть внимательна при введении новых функций в Codex CLI, которые могут нарушить кэширование промптов. Например, наша первоначальная поддержка MCP-инструментов привела к багу с непоследовательным порядком перечисления инструментов, вызывающему промахи кэша. MCP-инструменты могут быть особенно коварны, поскольку MCP-серверы могут менять список предоставляемых инструментов на лету через уведомление notifications/tools/list_changed. Учёт этого уведомления в середине длинного разговора может вызвать дорогой промах кэша.
По возможности мы обрабатываем изменения конфигурации в середине разговора, добавляя новое сообщение к input для отражения изменения, а не модифицируя более раннее сообщение:
- При изменении конфигурации песочницы или режима подтверждения мы вставляем новое сообщение с
role=developerтого же формата, что и оригинальный элемент - При изменении текущей рабочей директории мы вставляем новое сообщение с
role=userтого же формата
Мы прилагаем большие усилия для обеспечения попаданий в кэш ради производительности. Есть ещё один ключевой ресурс, которым нам нужно управлять: контекстное окно.
Наша общая стратегия избежания исчерпания контекстного окна — сжатие (compact) разговора при превышении порога по количеству токенов. Конкретно, мы заменяем input новым, меньшим списком элементов, репрезентативным для разговора, позволяя агенту продолжить с пониманием произошедшего. Ранняя реализация сжатия требовала от пользователя ручного вызова команды /compact, которая запрашивала Responses API с существующим разговором плюс кастомные инструкции для суммаризации. Codex использовал результирующее сообщение ассистента с саммари как новый `input` для последующих ходов разговора.
С тех пор Responses API эволюционировал и поддерживает специальный эндпоинт `/responses/compact`, который выполняет сжатие более эффективно. Он возвращает список элементов, которые можно использовать вместо предыдущего input для продолжения разговора при освобождении контекстного окна. Этот список включает специальный элемент type=compaction с непрозрачным encrypted_content, сохраняющим латентное понимание моделью исходного разговора. Теперь Codex автоматически использует этот эндпоинт для сжатия разговора при превышении auto_compact_limit.
Что дальше
Мы представили агентный цикл Codex и прошлись по тому, как Codex формирует и управляет контекстом при запросах к модели. Попутно мы выделили практические соображения и лучшие практики, применимые к любому, кто строит агентный цикл поверх Responses API.
Хотя агентный цикл обеспечивает фундамент для Codex, это только начало. В следующих постах мы углубимся в архитектуру CLI, рассмотрим реализацию использования инструментов и подробнее изучим модель песочницы Codex.
Подпишитесь на канал и каждый день читайте лучшие материалы про AI переведенные на русский!
Нашли интересную статью для перевода? Пришлите нашему боту: @ailongreadsbot