Разбор цикла агента Codex

Автор: Майкл Болин (Michael Bolin), технический сотрудник

Codex CLI — это наш кроссплатформенный локальный программный агент, предназначенный для создания качественных и надежных программных изменений при безопасной и эффективной работе на вашем компьютере. Мы извлекли массу полезного опыта о том, как создавать программный агент мирового уровня с момента нашего первого запуска CLI в апреле. В качестве обзора этих выводов эта статья открывает цикл публикаций, в которых мы рассмотрим различные аспекты работы Codex, а также извлеченные на собственном опыте уроки. (Более подробную информацию об устройстве Codex CLI можно найти в нашем репозитории с открытым исходным кодом по адресу https://github.com/openai/codex. Многие мелкие детали наших проектных решений зафиксированы в тикетах и пулл-реквестах на GitHub, если вы захотите узнать больше.)

Для начала мы сосредоточимся на цикле агента (agent loop), который представляет собой основную логику в Codex CLI, отвечающую за координацию взаимодействия между пользователем, моделью и инструментами, вызываемыми моделью для выполнения полезной работы с программным обеспечением. Мы надеемся, что эта статья даст вам хорошее представление о роли, которую наш агент (или «обвязка») играет в задействовании возможностей LLM.

Прежде чем мы углубимся в детали, краткое замечание о терминологии: в OpenAI термин «Codex» охватывает набор предложений программных агентов, включая Codex CLI, Codex Cloud и расширение Codex для VS Code. В этой статье основное внимание уделяется обвязке (harness) Codex, которая обеспечивает базовый цикл агента и логику выполнения, лежащие в основе всех возможностей Codex и представленные через Codex CLI. Для удобства здесь мы будем использовать термины «Codex» и «Codex CLI» как взаимозаменяемые.

Цикл агента

В основе любого ИИ-агента лежит так называемый «цикл агента». Упрощенная иллюстрация цикла агента выглядит следующим образом:

Diagram titled

Для начала агент принимает входные данные (input) от пользователя для включения в набор текстовых инструкций, подготавливаемых для модели, которые известны как промпт (prompt).

Следующим шагом является отправка запроса к модели путем передачи ей наших инструкций и требования сгенерировать ответ — процесс, известный как инференс (inference). Во время инференса текстовый промпт сначала преобразуется в последовательность входных токенов — целых чисел, индексирующих словарь модели. Затем эти токены используются для выборки из модели, создавая новую последовательность выходных токенов.

Выходные токены переводятся обратно в текст, который становится ответом модели. Поскольку токены генерируются постепенно, этот перевод может происходить по мере работы модели, поэтому многие приложения на базе LLM отображают вывод в режиме стриминга (потоковой передачи). На практике инференс обычно инкапсулируется за API, работающим с текстом, скрывая детали токенизации.

В результате этапа инференса модель либо (1) выдает финальный ответ на исходный ввод пользователя, либо (2) запрашивает вызов инструмента (tool call), который агент должен выполнить (например, «запустить ls и сообщить результат»). В случае (2) агент выполняет вызов инструмента и добавляет его вывод к исходному промпту. Этот вывод используется для генерации нового ввода, который применяется для повторного запроса к модели; после этого агент может учесть новую информацию и повторить попытку.

Этот процесс повторяется до тех пор, пока модель не перестанет генерировать вызовы инструментов и вместо этого сформирует сообщение для пользователя (в моделях OpenAI оно называется сообщением ассистента — assistant message). Во многих случаях это сообщение напрямую отвечает на первоначальный запрос пользователя, но оно также может содержать уточняющий вопрос.

Поскольку агент может выполнять вызовы инструментов, изменяющих локальное окружение, его «вывод» не ограничивается сообщением ассистента. Во многих случаях основным результатом работы программного агента является код, который он пишет или редактирует на вашем компьютере. Тем не менее, каждый ход всегда завершается сообщением ассистента — например, «Я добавил architecture.md, о котором вы просили», — что сигнализирует о состоянии завершения в цикле агента. С точки зрения агента, его работа завершена, и управление возвращается пользователю.

Путь от ввода пользователя до ответа агента, показанный на диаграмме, называется одним ходом диалога (в Codex — веткой/потоком, thread). Хотя этот ход диалога может включать множество итераций между инференсом модели и вызовами инструментов. Каждый раз, когда вы отправляете новое сообщение в существующий диалог, история диалога включается в состав промпта для нового хода, что включает в себя сообщения и вызовы инструментов из предыдущих ходов:

Diagram titled

Это означает, что по мере развития диалога увеличивается и длина промпта, используемого для выборки из модели. Эта длина имеет значение, поскольку у каждой модели есть контекстное окно (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.

API ответов (Responses API) принимает полезную нагрузку JSON со множеством параметров. Мы сосредоточимся на этих трех:

  • instructions: сообщение системы (или разработчика), вставляемое в контекст модели
  • tools: список инструментов, к которым модель может обращаться при генерации ответа
  • input: список текстовых, графических или файловых входных данных для модели

В Codex поле instructions считывается из model_instructions_file в ~/.codex/config.toml, если указано; в противном случае используются инструкции, base_instructions связанные с моделью. Инструкции для конкретных моделей хранятся в репозитории Codex и включаются в состав CLI (например, gpt-5.2-codex_prompt.md).

Поле tools представляет собой список определений инструментов, соответствующих схеме, определяемой Responses API. Для Codex это включает инструменты, предоставляемые CLI Codex, инструменты Responses API, которые должны быть доступны для Codex, а также инструменты, предоставляемые пользователем, обычно через серверы MCP:

JavaScript

1
[
2
// Codex's default shell tool for spawning new processes locally.
3
{
4
"type": "function",
5
"name": "shell",
6
"description": "Runs a shell command and returns its output...",
7
"strict": false,
8
"parameters": {
9
"type": "object",
10
"properties": {
11
"command": {"type": "array", "description": "The command to execute", ...},
12
"workdir": {"description": "The working directory...", ...},
13
"timeout_ms": {"description": "The timeout for the command...", ...},
14
...
15
},
16
"required": ["command"],
17
}
18
}
19

20
// Codex's built-in plan tool.
21
{
22
"type": "function",
23
"name": "update_plan",
24
"description": "Updates the task plan...",
25
"strict": false,
26
"parameters": {
27
"type": "object",
28
"properties": {"plan":..., "explanation":...},
29
"required": ["plan"]
30
}
31
},
32

33
// Web search tool provided by the Responses API.
34
{
35
"type": "web_search",
36
"external_web_access": false
37
},
38

39
// MCP server for getting weather as configured in the
40
// user's ~/.codex/config.toml.
41
{
42
"type": "function",
43
"name": "mcp__weather__get-forecast",
44
"description": "Get weather alerts for a US state",
45
"strict": false,
46
"parameters": {
47
"type": "object",
48
"properties": {"latitude": {...}, "longitude": {...}},
49
"required": ["latitude", "longitude"]
50
}
51
}
52
]

Наконец, поле input полезной нагрузки JSON представляет собой список элементов. Codex вставляет следующие элементы в input перед добавлением сообщения пользователя:

1. Сообщение с role=developer, которое описывает песочницу и применяется только к предоставленному Codex shell инструменту, определенному в разделе tools. Это означает, что другие инструменты, например предоставляемые с серверов MCP, не изолируются в песочнице Codex и отвечают за обеспечение собственной безопасности.

Сообщение строится по шаблону, где ключевые фрагменты контента берутся из фрагментов Markdown, входящих в состав CLI Codex, таких как workspace_write.md и on_request.md:

Обычный текст

1
2
- description of the sandbox explaining file permissions and network access
3
- instructions for when to ask the user for permissions to run a shell command
4
- list of folders writable by Codex, if any
5

2. (Необязательно) Сообщение с role=developer, содержимым которого является значение developer_instructions, прочитанное из файла пользователя config.toml.

3. (Необязательно) Сообщение с role=user, содержимым которого являются «инструкции пользователя», которые поступают не из одного файла, а собираются из нескольких источников. Как правило, более специфичные инструкции располагаются ближе к концу:

  • Содержимое AGENTS.override.md и AGENTS.md в $CODEX_HOME
  • С учетом ограничения (по умолчанию 32 КБ) поиск выполняется в каждой папке от корня Git/проекта cwd (если он существует) до самого cwd: добавляется содержимое файлов AGENTS.override.md, AGENTS.md или любого другого имени файла, указанного в project_doc_fallback_filenames in config.toml
  • Если настроены какие-либо навыки:
    • краткое введение о навыках
    • метаданные навыков⁠ для каждого навыка
    • раздел о том, как использовать навыки

4. Сообщение с role=user, которое описывает локальную среду, в которой в данный момент работает агент. Это определяет текущий рабочий каталог и оболочку пользователя:

Обычный текст

1
2
/Users/mbolin/code/codex5
3
zsh
4

Как только Codex выполняет все вышеперечисленные вычисления для инициализации input, он добавляет сообщение пользователя, чтобы начать диалог.

Предыдущие примеры были сосредоточены на содержимом каждого сообщения, однако обратите внимание, что каждый элемент input представляет собой объект JSON, содержащий type, role и content следующим образом:

JSON

1
{
2
"type": "message",
3
"role": "user",
4
"content": [
5
{
6
"type": "input_text",
7
"text": "Add an architecture diagram to the README.md"
8
}
9
]
10
}

Как только Codex формирует полную полезную нагрузку JSON для отправки в Responses API, он выполняет HTTP-запрос POST с заголовком Authorization в зависимости от того, как настроена конечная точка Responses API в ~/.codex/config.toml (при необходимости добавляются дополнительные заголовки HTTP и параметры запроса).

Когда сервер OpenAI Responses API получает запрос, он использует JSON для формирования промпта для модели следующим образом (разумеется, пользовательская реализация Responses API может сделать иной выбор):

Snapshot diagram showing a single step in an AI agent loop. A user request enters the model, which produces a thought, an action with a tool name, and a tool input. The diagram highlights this intermediate reasoning step before the tool is called.

Как видите, порядок первых трех элементов в промпте определяется сервером, а не клиентом. При этом из этих трех элементов только содержимое системного сообщения также контролируется сервером, поскольку tools и instructions определяются клиентом. За ними следует input из полезной нагрузки JSON, чтобы завершить промпт.

Теперь, когда у нас есть промпт, мы готовы сделать выборку из модели.

Первый ход

Этот HTTP-запрос к Responses API инициирует первый «ход» диалога в Codex. Сервер отвечает потоком Server-Sent Events (SSE). Поле data каждого события представляет собой полезную нагрузку JSON с полем "type", которое начинается с "response" и может выглядеть примерно так (полный список событий можно найти в нашей документации по API):

Обычный текст

1
data: {"type":"response.reasoning_summary_text.delta","delta":"ah ", ...}
2
data: {"type":"response.reasoning_summary_text.delta","delta":"ha!", ...}
3
data: {"type":"response.reasoning_summary_text.done", "item_id":...}
4
data: {"type":"response.output_item.added", "item":{...}}
5
data: {"type":"response.output_text.delta", "delta":"forty-", ...}
6
data: {"type":"response.output_text.delta", "delta":"two!", ...}
7
data: {"type":"response.completed","response":{...}}

Codex потребляет поток событий и перепубликует их в виде внутренних объектов событий, которые могут быть использованы клиентом. Такие события, как response.output_text.delta, используются для поддержки потоковой передачи в пользовательском интерфейсе, в то время как другие события, такие как response.output_item.added, преобразуются в объекты, которые добавляются в input для последующих вызовов Responses API.

Предположим, что первый запрос к Responses API включает два события response.output_item.done: одно с type=reasoning, а другое с type=function_call. Эти события должны быть представлены в поле input JSON при повторном запросе к модели с результатом вызова инструмента:

JavaScript

1
[
2
/* ... original 5 items from the input array ... */
3
{
4
"type": "reasoning",
5
"summary": [
6
"type": "summary_text",
7
"text": "**Adding an architecture diagram for README.md**\n\nI need to..."
8
],
9
"encrypted_content": "gAAAAABpaDWNMxMeLw..."
10
},
11
{
12
"type": "function_call",
13
"name": "shell",
14
"arguments": "{\"command\":\"cat README.md\",\"workdir\":\"/Users/mbolin/code/codex5\"}",
15
"call_id": "call_8675309..."
16
},
17
{
18
"type": "function_call_output",
19
"call_id": "call_8675309...",
20
"output": "

npm i -g @openai/codex…»

21
}
22
]

Итоговый промпт, используемый для сэмплирования модели в рамках последующего запроса, будет выглядеть следующим образом:

Diagram labeled

Обратите внимание, в частности, на то, как старый промпт является точным префиксом нового промпта. Это сделано намеренно, так как это делает последующие запросы гораздо более эффективными, позволяя задействовать кэширование промптов (о чем мы поговорим в следующем разделе, посвященном производительности).

Возвращаясь к нашей первой схеме цикла агента, мы видим, что между инференсом и вызовом инструментов может происходить множество итераций. Промпт может продолжать расти до тех пор, пока мы наконец не получим сообщение от ассистента, обозначающее конец хода:

Обычный текст

1
data: {"type":"response.output_text.done","text": "I added a diagram to explain...", ...}
2
data: {"type":"response.completed","response":{...}}

В интерфейсе Codex CLI мы показываем сообщение ассистента пользователю и переводим фокус на поле ввода, чтобы дать пользователю понять, что наступила его очередь («ход») продолжать разговор. Если пользователь отвечает, в запрос к Responses API для начала нового хода необходимо добавить как сообщение ассистента из предыдущего хода, так и новое сообщение пользователя в input:

JavaScript

1
[
2
/* ... all items from the last Responses API request ... */
3
{
4
"type": "message",
5
"role": "assistant",
6
"content": [
7
{
8
"type": "output_text",
9
"text": "I added a diagram to explain the client/server architecture."
10
}
11
]
12
},
13
{
14
"type": "message",
15
"role": "user",
16
"content": [
17
{
18
"type": "input_text",
19
"text": "That's not bad, but the diagram is missing the bike shed."
20
}
21
]
22
}
23
]

И снова, поскольку мы продолжаем разговор, длина input, отправляемого в Responses API, продолжает увеличиваться:

Diagram labeled

Давайте рассмотрим, что этот постоянно растущий промпт означает для производительности.

Вопросы производительности

Вы можете спросить себя: «Погодите, разве цикл агента не имеет квадратичную сложность с точки зрения объема JSON, отправляемого в Responses API по ходу разговора?» И вы будете правы. Хотя Responses API действительно поддерживает необязательный параметр previous_response_id для смягчения этой проблемы, Codex в настоящее время его не использует — главным образом для того, чтобы запросы оставались полностью беззапросными (stateless) и поддерживали конфигурации с нулевым удержанием данных (Zero Data Retention, ZDR).

Избежание previous_response_id упрощает задачу для поставщика Responses API, поскольку гарантирует беззапросный (stateless) характер каждого запроса. Это также упрощает поддержку клиентов, которые выбрали режим нулевого удержания данных (Zero Data Retention, ZDR)⁠, так как хранение данных, необходимых для поддержки previous_response_id, противоречило бы ZDR. Обратите внимание, что клиенты ZDR не лишаются возможности использовать проприетарные сообщения с рассуждениями из предыдущих ходов, поскольку связанный encrypted_content может быть расшифрован на сервере. (OpenAI сохраняет ключ дешифрования клиента ZDR, но не его данные.) См. пулл-реквесты #642⁠ и #1641⁠, касающиеся соответствующих изменений в Codex для поддержки ZDR.

Как правило, затраты на сэмплирование модели превосходят затраты на сетевой трафик, что делает сэмплирование главной целью наших усилий по оптимизации. Именно поэтому кэширование промптов так важно: оно позволяет повторно использовать результаты вычислений от предыдущего вызова инференса. Когда происходят попадания в кэш, сэмплирование модели имеет линейную, а не квадратичную сложность. Наша документация по кэшированию промптов объясняет это более подробно:

Попадания в кэш возможны только при точном совпадении префиксов внутри промпта. Чтобы воспользоваться преимуществами кэширования, размещайте статический контент (например, инструкции и примеры) в начале промпта, а изменяемый контент (например, информацию, специфичную для пользователя) — в конце. Это также относится к изображениям и инструментам, которые должны быть идентичны в разных запросах.

Учитывая это, давайте подумаем, какие типы операций могут вызвать «промах кэша» (cache miss) в Codex:

  • Изменение tools, доступных модели в середине беседы.
  • Изменение model, на который направлен запрос Responses API (на практике это изменяет третий элемент в исходном промпте, поскольку он содержит инструкции, специфичные для модели).
  • Изменение конфигурации песочницы (sandbox), режима подтверждения или текущего рабочего каталога.

Команда Codex должна проявлять бдительность при внедрении новых функций в Codex CLI, которые могут поставить под угрозу кэширование промптов. Например, наша первоначальная поддержка инструментов MCP привнесла баг, из-за которого мы не перечисляли инструменты в стабильном порядке, что приводило к промахам кэша. Обратите внимание, что инструменты MCP могут быть особенно коварными, поскольку серверы MCP могут изменять список предоставляемых ими инструментов «на лету» с помощью уведомления notifications/tools/list_changed. Обработка этого уведомления посреди долгой беседы может вызвать дорогостоящий промах кэша.

По возможности мы обрабатываем изменения конфигурации, происходящие в середине беседы, путем добавления нового сообщения в input для отражения изменений, вместо того чтобы модифицировать более раннее сообщение:

  • Если конфигурация песочницы или режим подтверждения изменяются, мы вставляем новое сообщение role=developer в том же формате, что и исходный элемент .
  • Если текущий рабочий каталог изменяется, мы вставляем новое сообщение role=user в том же формате, что и исходный .

Мы прикладываем максимум усилий для обеспечения попаданий в кэш ради производительности. Но есть еще один ключевой ресурс, которым нам приходится управлять: контекстное окно.

Наша общая стратегия предотвращения исчерпания контекстного окна заключается в компактизации разговора, как только количество токенов превышает определенный порог. В частности, мы заменяем input новым, меньшим списком элементов, который представляет собой репрезентативную выборку беседы, позволяя агенту продолжать работу с пониманием того, что произошло к текущему моменту. Ранняя реализация компактизации требовала от пользователя вручную вызывать команду /compact, которая отправляла запрос к API Responses, используя существующий разговор вместе с пользовательскими инструкциями для суммирования. Codex использовал полученное сообщение ассистента, содержащее сводку, в качестве новой input для последующих шагов диалога.

С тех пор API Responses претерпел изменения и стал поддерживать специальную /responses/compact конечную точку, которая выполняет компактизацию более эффективно. Она возвращает список элементов, которые могут использоваться вместо предыдущего input для продолжения разговора при одновременном освобождении контекстного окна. Этот список включает специальный элемент type=compaction с непрозрачным encrypted_content элементом, который сохраняет скрытое понимание моделью исходного разговора. Теперь Codex автоматически использует эту конечную точку для компактизации беседы при превышении auto_compact_limit.

Далее

Мы познакомились с циклом агента Codex и рассмотрели, как Codex формирует и управляет своим контекстом при запросах к модели. По ходу дела мы выделили практические соображения и лучшие практики, применимые ко всем, кто создает цикл агента поверх API Responses.

Хотя цикл агента образует основу для Codex, это только начало. В следующих статьях мы углубимся в архитектуру CLI, рассмотрим реализацию использования инструментов и поближе познакомимся с моделью песочницы Codex.

Автор

Michael Bolin

Благодарности

Особая благодарность всей команде, создавшей Codex CLI.

Полный текст статьи читайте на OpenAI