剛剛,OpenAI 開發者團隊發了一篇部落格,直接把 Codex 的「內核」給拆開了。
文章完整講解了從使用者輸入到最終回應的全過程,包括 prompt 構建、模型推理、工具呼叫、上下文管理等核心細節。
對於想了解 AI 程式設計助理底層機制的開發者、正在建構自己 Agent 系統的工程師、以及對 Codex 感興趣的使用者來說,本文值得一讀。
全文如下
你有沒有想過,當你給 Codex 發一條指令,到它給你返回結果之間,到底發生了什麼?
每一輪對話都會經歷:組裝輸入、執行推理、執行工具、把結果餵回上下文。
如此循環,直到任務完成。
這是 OpenAI 關於 Codex 技術揭秘系列的第一篇,且 OpenAI 承諾後續還會有更多內容放出。
什麼是 Agent Loop
Agent Loop(智慧體循環)是 Codex CLI 的核心邏輯,負責協調使用者、模型和工具之間的互動。
簡單來說,流程是這樣的:
Agent 接收使用者的輸入,把它組裝成發給模型的指令(也就是 prompt)。
下一步是推理(inference):把 prompt 發給模型,讓它生成回應。在推理過程中,文本 prompt 首先被轉換成一串 token(整數索引),然後模型基於這些 token 採樣,產生新的 token 序列。
輸出的 token 再轉回文本,就是模型的回應。因為 token 是逐個生成的,所以很多 LLM 應用都支援串流輸出。你能看到回答一個字一個字蹦出來。
推理結束後,模型要麼(1)直接給出最終答案,要麼(2)請求執行一個工具呼叫(比如「跑一下 ls 命令然後告訴我結果」)。如果是後者,Agent 就去執行工具,把輸出拼到原來的 prompt 後面,然後重新查詢模型。
這個過程會一直循環,直到模型不再請求工具呼叫,而是產出一條給使用者的訊息(在 OpenAI 的術語裡叫 assistant message)。
這條訊息可能直接回答了使用者的問題,也可能是向使用者追問。
從使用者輸入到 Agent 回應,這個過程叫做一「輪」對話(turn)。在 Codex 裡叫「thread」。
雖然是一輪對話,但可能包含多次「模型推理 → 工具呼叫」的迭代。
每當使用者向已有對話發送新訊息時,之前的對話歷史(包括之前的訊息和工具呼叫)都會作為新一輪的 prompt 的一部分。
這意味著對話越長,prompt 也越長。
這很關鍵,因為每個模型都有上下文視窗(context window),也就是單次推理能處理的最大 token 數。注意這個視窗包括輸入和輸出的 token。
一個 Agent 可能在一輪對話裡呼叫幾百次工具,很容易就把上下文撐爆了。
所以,上下文視窗管理是 Agent 的重要職責之一。
模型推理
Codex CLI 透過向 Responses API 發送 HTTP 請求來執行模型推理。
Codex CLI 使用的 Responses API 端點是可設定的,所以它可以相容任何實現了 Responses API 的端點:
使用 ChatGPT 登入時,端點是
https://chatgpt.com/backend-api/codex/responses使用 API key 認證時,端點是
https://api.openai.com/v1/responses使用
--oss參數跑 gpt-oss 時(配合 ollama 0.13.4+ 或 LM Studio 0.3.39+),預設用本地的http://localhost:11434/v1/responses也可以用 Azure 等雲端服務商託管的 Responses API
構建初始 Prompt
作為使用者,你不需要自己寫完整的 prompt。
你只需要在請求裡指定各種輸入,Responses API 伺服器會決定怎麼把這些資訊組裝成模型能理解的 prompt。
你可以把 prompt 想像成一個「列表」。
初始 prompt 裡的每個條目都有一個 role,表示這段內容的權重高低,從高到低依次是:system、developer、user、assistant。
Responses API 接受的 JSON 裡有很多參數,最關鍵的是這三個:
instructions:插入到模型上下文的 system(或 developer)訊息tools:模型可以呼叫的工具列表input:發給模型的文本、圖片或檔案輸入列表
在 Codex 裡,instructions 欄位來自 ~/.codex/config.toml 裡的 model_instructions_file,如果沒設定,就用模型自帶的 base_instructions。
不同模型的指令檔案打包在 CLI 裡(比如 gpt-5.2-codex_prompt.md)。
tools 欄位是一個工具定義列表,包括 Codex CLI 自帶的工具、Responses API 提供的工具,以及使用者透過 MCP server 設定的工具:
[
// Codex 自帶的 shell 工具,用於在本地執行命令
{
"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 內建的 plan 工具
{
"type": "function",
"name": "update_plan",
"description": "Updates the task plan...",
"strict": false,
"parameters": {
"type": "object",
"properties": {"plan":..., "explanation":...},
"required": ["plan"]
}
},
// Responses API 提供的網頁搜尋工具
{
"type": "web_search",
"external_web_access": false
},
// 使用者設定的 MCP server,比如天氣查詢
{
"type": "function",
"name": "mcp__weather__get-forecast",
"description": "Get weather alerts for a US state",
"strict": false,
"parameters": {
"type": "object",
"properties": {"latitude": {...}, "longitude": {...}},
"required": ["latitude", "longitude"]
}
}
]input 欄位是一個條目列表。在新增使用者訊息之前,Codex 會先插入以下內容:
1. 一條 role=developer 的訊息,描述沙箱環境,但僅適用於 Codex 自帶的 shell 工具。也就是說,MCP server 提供的工具不受 Codex 沙箱保護,需要自行實作安全機制。
這條訊息用範本產生,核心內容來自打包在 CLI 裡的 Markdown 檔案(如 workspace_write.md 和 on_request.md):
<permissions instructions>
- 沙箱說明,解釋檔案權限和網路存取
- 什麼時候該向使用者請求執行 shell 命令的權限
- Codex 可寫的資料夾列表(如果有的話)
</permissions instructions>2.(可選)一條 role=developer 的訊息,內容是使用者 config.toml 裡的 developer_instructions。
3.(可選)一條 role=user 的訊息,即「使用者指令」。這不是來自單一檔案,而是從多個來源聚合而來。越具體的指令出現得越靠後:
$CODEX_HOME目錄下的AGENTS.override.md和AGENTS.md內容從 Git/專案根目錄到目前目錄的每個資料夾裡(受 32 KiB 限制),尋找
AGENTS.override.md、AGENTS.md或project_doc_fallback_filenames指定的檔案如果設定了 skills:
一段關於 skills 的簡短說明
每個 skill 的元資料
關於如何使用 skills 的說明
4. 一條 role=user 的訊息,描述 Agent 當前執行的本地環境,包括目前工作目錄和使用者的 shell:
<environment_context>
<cwd>/Users/mbolin/code/codex5</cwd>
<shell>zsh</shell>
</environment_context>上述計算完成後,Codex 把使用者訊息追加到 input 裡,開始對話。
每個 input 元素都是一個 JSON 物件,包含 type、role 和 content:
{
"type": "message",
"role": "user",
"content": [
{
"type": "input_text",
"text": "Add an architecture diagram to the README.md"
}
]
}Codex 構建好完整的 JSON 後,就向 Responses API 發送 HTTP POST 請求(帶上 Authorization header,以及設定中指定的其他 header 和參數)。
當 OpenAI 的 Responses API 伺服器收到請求後,會按如下方式從 JSON 構建 prompt:
可以看到,前三項的順序由伺服器決定,而非客戶端。
不過在這三項中,只有 system message 的內容也由伺服器控制,tools 和 instructions 都是客戶端決定的。之後是 JSON 裡的 input,構成完整的 prompt。
有了 prompt,就可以採樣模型了。
第一輪對話
向 Responses API 發送的 HTTP 請求啟動了 Codex 對話的第一「輪」。伺服器以 Server-Sent Events(SSE)串流的形式返回回應。每個事件的 data 是一個 JSON,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 這樣的事件會被轉換成物件,追加到後續 Responses API 呼叫的 input 裡。
假設第一次請求返回了兩個 response.output_item.done 事件:一個 type=reasoning,一個 type=function_call。當我們再次查詢模型時,這些事件必須在 input 裡體現出來:
[
/* ... input 陣列裡原來的 5 個條目 ... */
{
"type": "reasoning",
"summary": [
{
"type": "summary_text",
"text": "**Adding an architecture diagram for README.md**\n\nI need to..."
}
],
"encrypted_content": "gAAAAABpaDWNMxMeLw..."
},
{
"type": "function_call",
"name": "shell",
"arguments": "{\"command\":\"cat README.md\",\"workdir\":\"/Users/mbolin/code/codex5\"}",
"call_id": "call_8675309..."
},
{
"type": "function_call_output",
"call_id": "call_8675309...",
"output": "<p align=\"center\"><code>npm i -g @openai/codex</code>..."
}
]後續查詢的 prompt 會變成這樣:
注意,舊 prompt 是新 prompt 的精確前綴。這是故意設計的,這樣後續請求就能利用 prompt 快取,大幅提升效率(後面會講)。
回顧我們最開始的 Agent Loop 圖,推理和工具呼叫之間可能有很多次迭代。Prompt 會不斷增長,直到我們最終收到一條 assistant message,標誌著這一輪結束:
data: {"type":"response.output_text.done","text": "I added a diagram to explain...", ...}
data: {"type":"response.completed","response":{...}}在 Codex CLI 裡,我們把 assistant message 展示給使用者,並聚焦到輸入框,表示現在輪到使用者繼續對話了。如果使用者回覆,上一輪的 assistant message 和使用者的新訊息都要追加到新請求的 input 裡:
[
/* ... 上次 Responses API 請求的所有條目 ... */
{
"type": "message",
"role": "assistant",
"content": [
{
"type": "output_text",
"text": "I added a diagram to explain the client/server architecture."r> }
]
},
{
"type": "message",
"role": "user",
"content": [
{
"type": "input_text",
"text": "That's not bad, but the diagram is missing the bike shed."r> }
]
}
]因為是在繼續對話,發給 Responses API 的 input 長度會持續增加:
效能考量
你可能會問:「等等,這個 Agent Loop 在整個對話過程中發送的 JSON 量不是平方級增長嗎?」
沒錯。雖然 Responses API 支援可選的 previous_response_id 參數來緩解這個問題,但 Codex 目前不用它,主要是為了保持請求完全無狀態,以及支援零資料保留(ZDR)設定。
避免使用 previous_response_id 簡化了 Responses API 提供方的實現,因為每個請求都是無狀態的。這也讓支援 ZDR 客戶變得簡單:儲存支援 previous_response_id 所需的資料會與 ZDR 相矛盾。ZDR 客戶仍然可以受益於之前輪次的專有推理訊息,因為相關的 encrypted_content 可以在伺服器端解密。(OpenAI 保留 ZDR 客戶的解密金鑰,但不保留他們的資料。)
一般來說,採樣模型的成本遠超網路傳輸成本,所以採樣是效率優化的主要目標。這就是為什麼 prompt 快取 如此重要:它能讓我們重用之前推理呼叫的計算。
當快取命中時,採樣模型是線性的而非平方級的。
OpenAI 的 prompt 快取文件這樣解釋:
快取命中只對 prompt 內精確的前綴匹配有效。要實現快取收益,把靜態內容(如指令和範例)放在 prompt 開頭,把可變內容(如使用者特定資訊)放在末尾。圖片和工具也是如此,它們必須在不同請求之間完全一致。
考慮到這點,哪些操作可能導致 Codex 的「快取未命中」呢?
- 在對話中途改變可用的
tools - 改變 Responses API 請求的目標
model(實際上這會改變原始 prompt 的第三項,因為它包含模型特定的指令) - 改變沙箱設定、審批模式或目前工作目錄
Codex 團隊在引入可能影響 prompt 快取的新功能時必須謹慎。舉個例子,最初支援 MCP 工具時有一個 bug,工具列舉順序不一致,導致快取未命中。MCP 工具特別棘手,因為 MCP server 可以透過 notifications/tools/list_changed 通知動態改變工具列表。在長對話中途回應這個通知可能導致昂貴的快取未命中。
在可能的情況下,我們透過在 input 後面追加新訊息來處理對話中途的設定變更,而不是修改之前的訊息:
- 如果沙箱設定或審批模式變更,我們插入一條新的
role=developer訊息,格式與原來的<permissions instructions>相同 - 如果目前工作目錄變更,我們插入一條新的
role=user訊息,格式與原來的<environment_context>相同
我們竭盡全力確保快取命中以提升效能。還有另一個關鍵資源需要管理:上下文視窗。
我們避免上下文視窗耗盡的通用策略是:當 token 數超過某個閾值時,壓縮對話。具體來說,我們用一個更小的、能代表原對話的條目列表替換 input,讓 Agent 能帶著對已發生內容的理解繼續工作。
早期的壓縮實現需要使用者手動執行 /compact 命令,它會用已有對話加上自訂的摘要指令查詢 Responses API,然後用返回的 assistant message 作為後續對話輪次的新 input。
後來,Responses API 演進出了專門的 /responses/compact 端點來更高效地執行壓縮。它返回一個條目列表,可以替代之前的 input 繼續對話,同時釋放上下文視窗。這個列表包含一個特殊的 type=compaction 條目,帶有不透明的 encrypted_content,保留了模型對原始對話的隱式理解。現在,當超過 auto_compact_limit 時,Codex 會自動使用這個端點壓縮對話。
後續計畫
OpenAI 介紹了 Codex 的 Agent Loop,並詳細講解了 Codex 在查詢模型時如何構建和管理上下文。同時也分享了一些對任何在 Responses API 上構建 Agent Loop 的人都適用的實踐考量和最佳實踐。
雖然 Agent Loop 是 Codex 的基礎,但這只是開始。在後續文章中,他們會深入探討 CLI 的架構、工具使用的實現,以及 Codex 的沙箱模型。
相關連結:
原文部落格:https://openai.com/index/unrolling-the-codex-agent-loop/
Codex CLI 開源倉庫:https://github.com/openai/codex
Codex 開發者文件:https://developers.openai.com/codex/cli
Responses API 文件:https://platform.openai.com/docs/api-reference/responses