Перейти до основного вмісту
OpenAI

23 січня 2026 р.

Інженерія

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

Майкл Болін, співробітник технічного підрозділу

Завантаження…

Codex CLI(відкривається у новому вікні) — це наш кросплатформний локальний агент програмного забезпечення, створений для внесення високоякісних і надійних змін у програмне забезпечення, безпечно та ефективно працюючи на вашому комп’ютері. Ми дізналися надзвичайно багато про те, як створити програмного агента світового класу відтоді, як запустили CLI у квітні. Щоб розібрати ці висновки, ми створили цей перший допис у тривалій серії, де ми досліджуватимемо різні аспекти роботи Codex, а також розглянемо здобутий досвід. (Щоб отримати ще детальніше уявлення про те, як створено Codex CLI, перегляньте наш відкритий репозиторій за адресою https://github.com/openai/codex(відкривається у новому вікні). Якщо ви хочете дізнатися більше, багато тонких деталей наших дизайнерських рішень задокументовано в issues і запитах на злиття на GitHub.)

Для початку ми зосередимося на циклі агента, який є основною логікою в Codex CLI, що відповідає за координацію взаємодії між користувачем, моделлю та інструментами, які модель викликає для виконання змістовної роботи з програмним забезпеченням. Сподіваємося, що цей допис дасть вам чітке уявлення про роль, яку наш агент (або програма, «harness») відіграє у використанні LLM.

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

Цикл роботи агента

У центрі кожного ШІ-агента знаходиться так званий «цикл агента». Спрощена ілюстрація циклу агента виглядає наступним чином:

Діаграма під назвою «Agent loop», що ілюструє, як система штучного інтелекту обробляє запит користувача, викликає інструменти, спостерігає результати, оновлює свій план і повертає результати. Стрілки з'єднують етапи, такі як введення користувача, міркування моделі, дії інструментів та кінцева відповідь.

Для початку агент отримує вхідні дані від користувача, щоб включити їх до набору текстових інструкцій, які він готує для моделі, відомої як запит, або ж промпт.

Наступний крок — надіслати моделі наші інструкції та попросити її згенерувати відповідь — процес, відомий як інференція. Під час інференції текстовий запит спочатку перетворюється на послідовність вхідних токенів(відкривається у новому вікні)—цілих чисел, які індексують словник моделі. Потім ці токени використовуються для вибірки моделі, створюючи нову послідовність вихідних токенів.

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

У результаті кроку інференції модель або (1) формує остаточну відповідь на початковий запит користувача, або (2) запитує виклик інструмента, який агент має виконати (наприклад, «запусти ls і повідом про результат»). У випадку (2) агент виконує виклик інструмента та додає його результати до оригінального промпту. Цей результат використовується для створення нового введення, яке використовується для повторного запиту до моделі; агент може врахувати цю нову інформацію та спробувати ще раз.

Цей процес повторюється, доки модель не припинить генерувати виклики інструментів і не створить повідомлення для користувача (в моделях OpenAI це називається повідомленням асистента). У багатьох випадках це повідомлення безпосередньо відповідає на початковий запит користувача, але також може бути додатковим запитанням для користувача.

Оскільки агент може виконувати виклики інструментів, що змінюють локальне середовище, його «результат» не обмежується повідомленням асистента. У багатьох випадках основним результатом роботи програмного агента є код, який він пише або редагує на вашому комп'ютері. Проте кожен хід завжди завершується повідомленням асистента — наприклад, «Я додав architecture.md, який ти просив», — що сигналізує про стан завершення в циклі агента. З точки зору агента, його робота завершена, і керування повертається до користувача.

Шлях від введення користувача до відповіді агента, показаний на діаграмі, називається одним ходом розмови (у Codex — тредом ). Хоча цей хід розмови може включати багато ітерацій між висновками моделі та викликами інструментів. Щоразу, коли ви надсилаєте нове повідомлення в існуючу розмову, історія розмови включається як частина промпту для нового ходу, що містить повідомлення та виклики інструментів з попередніх ходів:

Діаграма під назвою «Multi-turn agent loop», що показує, як ШІ-агент ітеративно приймає введені користувачем дані, генерує дії, звертається до інструментів, оновлює стан і повертає результати. Позначені кроки, стрілки та приклади результатів роботи інструментів, що ілюструють цикл міркувань агента.

Це означає, що в міру зростання об'єму розмови збільшується й довжина запиту, який використовується для вибірки моделі. Ця довжина має значення, оскільки кожна модель має контекстне вікно, що є максимальною кількістю токенів, які вона може використати для одного виклику інференції. Зверніть увагу, що це вікно включає вхідні та вихідні токени. Як ви можете уявити, агент може вирішити зробити сотні викликів інструментів за один хід, що може вичерпати контекстне вікно. З цієї причини управління контекстним вікном є одним із багатьох обов’язків агента. Тепер дізнаємося, як Codex виконує цикл агента.

Інференція моделі

Codex CLI надсилає HTTP-запити до Responses API(відкривається у новому вікні), щоб виконати інференцію моделі. Ми розглянемо, як інформація проходить через Codex, що використовує Responses API для керування циклом агента.

Кінцева точка Responses API, яку використовує Codex CLI, є налаштовуваною(відкривається у новому вікні), тому її можна використовувати з будь-якою кінцевою точкою, яка реалізує Responses API(відкривається у новому вікні):

Розглянемо, як Codex створює промпт для першого виклику інференції в розмові.

Створення початкового промпту

Як кінцевий користувач, ви не уточнюєте промпт, що використовується для дослівного семплювання моделі, коли ви надсилаєте запит до Responses API. Натомість ви вказуєте різні типи введення у своєму запиті, а сервер Responses API вирішує, як структурувати цю інформацію у промпт, який модель призначена споживати. Ви можете уявити промпт як «список елементів»; у цьому розділі буде пояснено, як ваш запит перетворюється на цей список.

У початковому запиті кожен елемент списку пов’язаний з роллю. Роль вказує на те, яку вагу має мати пов’язаний контент, і може приймати одне з таких значень (у порядку спадання пріоритету): system, developer, user, assistant.

Responses API(відкривається у новому вікні) приймає JSON-повідомлення з багатьма параметрами. Ми зосередимося на цих трьох:

У 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 servers:

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, яке описує пісочницю, що застосовується лише до інструмента shell, наданого Codex, визначеного в розділі tools. Тобто інші інструменти, такі як ті, що надаються серверами MCP, не ізольовані в пісочниці Codex і повинні самостійно забезпечувати свої бар'єри.

Повідомлення створюється на основі шаблону, де ключові фрагменти вмісту беруться з фрагментів Markdown, вбудованих у Codex CLI, таких як workspace_write.md(відкривається у новому вікні) і on_request.md(відкривається у новому вікні):

Простий текст

1
<permissions instructions>
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
</permissions instructions>

2. (Необов’язково) Повідомлення з role=developer, вміст якого — це значення developer_instructions, прочитане з файлу config.toml користувача.

3. (Необов’язково) Повідомлення з role=user, вмістом якого є «інструкції користувача», які не беруться з одного файлу, а збираються з кількох джерел(відкривається у новому вікні). Загалом, більш конкретні інструкції з'являються пізніше:

4. Повідомлення з role=user, яке описує локальне середовище, в якому агент зараз працює. Це вказує на поточний робочий каталог і оболонку користувача(відкривається у новому вікні):

Простий текст

1
<environment_context>
2
<cwd>/Users/mbolin/code/codex5</cwd>
3
<shell>zsh</shell>
4
</environment_context>

Після того як 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 може зробити інший вибір):

Діаграма, що показує один крок у циклі роботи ШІ-агента. Запит користувача надходить до моделі, яка генерує думку, дію з назвою інструменту та вхідні дані для інструменту. Діаграма виділяє цей проміжний етап міркування перед викликом інструмента.

Як ви можете бачити, порядок перших трьох елементів у запиті визначається сервером, а не клієнтом. Водночас із цих трьох елементів лише вміст системного повідомлення також контролюється сервером, оскільки інструменти і інструкції визначаються клієнтом. Після цього йде input з JSON-повідомлення для завершення запиту.

Тепер, коли у нас є наш промпт, ми готові розпочати семплювання моделі.

Перший хід

Цей HTTP-запит до Responses API ініціює перший «етап» розмови в Codex. Сервер відповідає потоком подій сервера (SSE(відкривається у новому вікні)). Дані кожної події — це 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": "<p align=\"center\"><code>npm i -g @openai/codex</code>..."
21
}
22
]

Отриманий промпт для вибірки моделі в наступному запиті виглядатиме так:

Діаграма під назвою «Snapshot 2», що показує ШІ-агента після виклику інструмента. Модель отримує спостереження за інструментом і генерує нову думку та дію. Стрілки з’єднують вхідні дані, спостереження та виходи, щоб показати, як агент повторює свій цикл міркувань.

Зокрема, зверніть увагу, що старий промпт є точним префіксом нового промпту. Це зроблено навмисно, оскільки це робить наступні запити набагато ефективнішими, адже дає нам змогу скористатися кешуванням запитів (про що ми поговоримо в наступному розділі про продуктивність).

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

Простий текст

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

У Codex CLI ми показуємо користувачеві повідомлення асистента та фокусуємо композер, щоб вказати користувачеві, що тепер його «черга» продовжити розмову. Якщо користувач відповідає, то як повідомлення асистента з попереднього ходу, так і нове повідомлення користувача потрібно додати до input у запиті Responses API, щоб розпочати новий хід:

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, постійно збільшується:

Діаграма під назвою «Snapshot 3», що показує фінальний етап циклу ШІ-агента. Після отримання результатів інструментів модель формує підсумкову думку та остаточну відповідь, яку повертає користувачу. Стрілки показують перехід від результатів роботи інструмента до завершеної відповіді.

Розглянемо, що означає цей постійно зростаючий запит для продуктивності.

Міркування щодо продуктивності

Можливо, ви запитуєте себе: «Зачекайте, хіба цикл агента не є квадратичним з точки зору обсягу JSON, надісланого до Responses API протягом розмови?» Так, ви маєте рацію. Хоча Responses API підтримує необов’язковий параметр previous_response_id(відкривається у новому вікні) для пом’якшення цієї проблеми, Codex наразі його не використовує, головним чином для збереження запитів повністю без фіксування стану та для підтримки конфігурацій нульового збереження даних (ZDR).

Уникання previous_response_id спрощує роботу для постачальника Responses API, оскільки гарантує, що кожен запит не зберігає стан. Це також спрощує підтримку клієнтів, які обрали нульове збереження даних (ZDR)(відкривається у новому вікні), оскільки зберігання даних, необхідних для підтримки previous_response_id, суперечило б принципам ZDR. Зверніть увагу, що клієнти, які обрали ZDR, не втрачають можливості отримувати вигоду з пропрієтарних повідомлень міркування з попередніх кроків, оскільки пов’язаний encrypted_content можна розшифрувати на сервері. (OpenAI зберігає ключ розшифрування клієнта, але не його дані.) Перегляньте пул-реквести #642(відкривається у новому вікні) та #1641(відкривається у новому вікні) щодо відповідних змін у Codex для підтримки ZDR.

Загалом, вартість вибірки моделі перевищує вартість мережевого трафіку, що робить вибірку основною метою наших зусиль з підвищення ефективності. Ось чому кешування промптів настільки важливе, адже воно дозволяє повторно використовувати обчислення з попереднього виклику інференції. Коли ми отримуємо влучання в кеш, вибірка моделі є лінійною, а не квадратичною. Наша документація з кешування промптів (відкривається у новому вікні)пояснює це докладніше:

Влучання в кеш можливі лише для точних збігів префікса в межах промпту. Щоб скористатися перевагами кешування, розміщуйте статичний контент, такий як інструкції та приклади, на початку запиту, а змінний контент, наприклад, інформацію, специфічну для користувача, — наприкінці. Це також стосується зображень та інструментів, які повинні бути ідентичними між запитами.

З огляду на це, розгляньмо, які типи операцій можуть спричинити «промах кешу» у Codex:

  • Зміна доступних для моделі tools посеред розмови.
  • Зміна моделі, яка є ціллю запиту до Responses API (на практиці це змінює третій елемент в оригінальному промпті, оскільки він містить інструкції, специфічні для моделі).
  • Зміна конфігурації пісочниці, режиму затвердження або поточного робочого каталогу.

Команда Codex повинна бути уважною при впровадженні нових функцій у Codex CLI, які можуть скомпрометувати кешування промптів. Наприклад, наша початкова підтримка інструментів MCP призвела до помилки, через яку ми не змогли перелічити інструменти в узгодженому порядку(відкривається у новому вікні), що спричиняло промахи кешу. Зверніть увагу, що інструменти MCP можуть бути особливо складними, оскільки сервери MCP можуть змінювати список інструментів, які вони надають, на ходу через сповіщення notifications/tools/list_changed(відкривається у новому вікні). Обробка цього сповіщення посеред довгої розмови може спричинити вартісний промах кешу.

Коли це можливо, ми обробляємо зміни конфігурації, що відбуваються під час розмови, додаючи нове повідомлення до input, щоб відобразити зміну, а не змінюючи попереднє повідомлення:

Ми докладаємо значних зусиль, щоб забезпечити влучання в кеш для підвищення продуктивності. Є ще один важливий ресурс, яким ми повинні керувати: контекстне вікно.

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

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

Що далі

Ми представили цикл агента Codex і розглянули, як Codex створює та керує своїм контекстом під час запиту до моделі. У процесі ми висвітлили практичні міркування та найкращі практики, які застосовні для будь-кого, хто створює цикл агента на основі Responses API.

Хоча цикл агента є основою для Codex, це лише початок. У наступних дописах ми детально розглянемо архітектуру CLI, дослідимо, як реалізовано використання інструментів, і уважніше придивимося до моделі ізоляції Codex.

Автор

Michael Bolin

Подяки

Особлива подяка всій команді, яка створила Codex CLI.