OpenAI Codex · 程式碼模式

往模型上下文裏塞東西,先給它造一個類型

批准前綴、工作區、AGENTS.md、被中斷的 turn,全是告訴模型一件它自己看不到的事實。Codex 先給每一種注入造一個類型,類型自己知道 role、marker 和 body。

課程目標讀完能説清三件事。第一,往模型上下文裏塞的每一段文字,為什麼要先收成一個類型。第二,有標和無標差在哪,壓縮和恢復靠什麼認回。第三,一條發給模型的消息按什麼字段分揀,順序從哪來。
先玩一遍 · 開哪些開關,信封裏就有哪些塊
同一輪輸入:五個 fragment 開關,看消息怎麼被拼出來
打開
開關一變,左邊信封立刻重排。點播放,看每一塊怎麼被問 role、問 marker、再分進三條消息。
發給模型的信封0 條消息 · 0
產地標籤先問類型
邏輯軌跡 · 動畫每一步對應源碼裏的哪一段
  1. 取 role,決定進 user 還是 developerfragment.rs L15
  2. 問要不要單獨成條fragment.rs L18
  3. 實例要 marker,拿去 renderfragment.rs L22
  4. 空 marker 只輸出 body,且永不匹配fragment.rs L41
  5. 視窗身份在循環前推進單獨組session/mod.rs L3661
  6. 其餘 fragment 按 role 和 marker 分揀session/mod.rs L3677
  7. user 側按註冊表 .any() 認回contextual_user_message.rs L18
  8. developer 側按前綴表認回event_mapping.rs L40
打開或關掉開關,看最終注入模型的內容由哪幾塊構成。點播放,看它們怎麼被分揀。
構成從哪來可合併的 developer 段先收成一條消息,必須單獨成條的各成一條,user 段再收成一條。順序寫在類型字段上,字串裏有沒有某個詞幫不上忙。
事後能不能認有標的靠首尾 marker 認回。無標的預設認不回,developer 側用前綴表補了一部分。
為什麼要先有類型組裝入口收的是已實現 trait 的 fragment。隨手 format 出來的標籤進不去,matcher 列表也認不出沒登記的標籤。
教學示意:開關組合與正文措辭為課程化設定,分揀順序對齊 build_initial_context_with_world_state。邏輯軌跡右側行號對應 openai/codex 倉庫 commit 4f39251a01。
思路一 · 注入先變成類型
它解決什麼問題

你給 agent 加過這類功能:用戶剛批准了 npm *,下一輪模型還在問要不要跑 npm test。或者壓縮剛結束,模型突然忘了工作區在哪、沙箱是只讀還是可寫。再或者 fork 一條會話,舊的 AGENTS.md 還在,新的目錄提示疊上去,模型同時看見兩套互相打架的規則。

這三件事的共同來源是同一類操作:運行時往模型上下文裏塞了一段文字。批准前綴、工作區、權限檔案、被中斷的 turn、當前 UTC 時間,全是告訴模型一件它自己看不到的事實。一段 format! 就能寫出來。

問題出在事後。這段文字進了歷史之後,壓縮要不要留它、會話恢復要不要認它、UI 要不要把它藏起來、world state 要不要拿它當模型已經知道了的證據,全都依賴一件事:你還能從純文本裏把它認回來。各處再寫一遍 startsWith,過幾週四處會漂。未知標籤還會漏進用戶消息解析。

思路是什麼

Codex 把每一種注入收成一個實現 ContextualUserFragment 的類型。trait 不在 codex-core 裏,而在獨立 crate codex-context-fragments。每個實現必須同時回答:這段以 user 還是 developer 進 Responses API,頭尾 marker 是什麼,中間 body 怎麼寫,要不要單獨成一條消息。

render() 把頭尾 marker 和 body 直接首尾相接,中間不加分隔符。空白和換行算 body 的。空 marker 只輸出 body。into() 把渲染結果收成一條 ResponseItem::Message,content 只有一個 InputText。注入的終點是協議對象。

codex-rs/context-fragments/src/fragment.rs第 38 至 46 行
    fn render(&self) -> String {
        let (start_marker, end_marker) = self.markers();
        let body = self.body();
        if start_marker.is_empty() && end_marker.is_empty() {
            return body;
        }

        format!("{start_marker}{body}{end_marker}")
    }
源碼快照説明:依據本地倉庫 openai/codex,核對文件 codex-rs/context-fragments/src/fragment.rs,commit 4f39251a01,核對日期 2026-08-22。程式碼塊保留源碼原文,這段就是類型把自己收成一段可注入文本的全部規則。

markers()self,渲染走實例。type_markers() 不帶 self,識別走類型。手裏只有歷史文本、沒有原對象時,仍然能問這段像不像某某 fragment。dyn ContextualUserFragment 調不了 matches_text,反向識別必須點到具體類型。

加一種注入要新建檔案、實現 trait、掛進 core/src/context/mod.rs。目錄裏 39 個模組。這個摩擦力本身就是治理:臨時 format! 走不通,因為組裝函式收的是 Box<dyn ContextualUserFragment>出處:codex-rs/core/src/context/mod.rs 第 3 至 41 行;codex-rs/core/src/session/mod.rs 第 3677 至 3707 行

運行時事實 工作區、指令、中斷 fragment 類型 role · markers · body render 首尾相接 Message 事後只剩一段 InputText 用 type_markers 問具體類型,matches_text 看首尾,沒有實例也能認回
教學化結構圖:渲染走實例,識別走類型,兩套調用讀同一對 marker。
為什麼長期成立

渲染和識別共用一份定義。換個語言重寫,最小形態仍是一份接口、一份註冊表、同一對 marker。組裝函式的參數寫成 fragment 類型,業務側就失去了隨手拼 XML 的調用點。識別函式只從註冊表做匹配,不許再寫一套 text.startsWith

思路二 · 有標和無標要寫清楚
它解決什麼問題

所有注入都帶標,一次性通知也會在壓縮後被重認、再餵一遍。所有注入都不帶標,壓縮後的歷史裏分不清用戶原話和運行時塞進去的説明。fork 會把過期的 <environment_context> 當成用戶輸入重新提交。

思路是什麼

視窗身份、環境、中斷、圖片縮放、管理側開發者指令,事後要認、要 diff、要在壓縮後決定是否重注,所以帶 begin 和 end。一次性通知,比如批准前綴、網絡規則入冊、剩餘 token 一句提醒,type_markers() 返回兩個空串。預設 matches_text 恆為 false。

匹配規則只看整段文本的首尾。先 trim 開頭比前綴,再 trim 結尾比後綴,ASCII 大小寫不敏感,兩個都中才算命中。中間夾什麼不管。無標 fragment 主動放棄可逆性。出處:codex-rs/context-fragments/src/fragment.rs 第 89 至 103 行

developer 側另有一張前綴表,在 event_mapping.rs,用來補一部分無標識別。覆蓋面比 user 側 matcher 列表窄。前綴表裏至今留着 <token_budget> 舊標籤,註釋寫明是為了認舊版本持久化下來的包裝。marker 一旦寫進 rollout,就變成恢復合同的一部分。改標籤等於改協議。

UserInstructions 有一處容易看走眼。它的 begin marker 是 Markdown 標題 # AGENTS.md instructions,end marker 是 </INSTRUCTIONS>。協議裏另有 <user_instructions> 那對標籤,這個 struct 沒用。按協議常量去寫 matches_text,會認漏倉庫裏真實渲染出來的文本。出處:codex-rs/core/src/context/user_instructions.rs 第 18 至 19 行;codex-rs/protocol/src/protocol.rs 第 112 至 113 行

有標 · 事後要認 環境 / 窗口 / 中斷 begin + body + end matches_text 能認回 無標 · 只當時説一聲 批准前綴 / 網絡規則 只輸出 body 預設識別放棄 developer 靠前綴表補
教學化對照:需要壓縮後重認的做成協議,一次性通知主動放棄可逆。
為什麼長期成立

需要事後認的做成協議,一次性的主動放棄,並且在類型上寫清楚。壓縮後要重認的強制 begin 和 end,一次性通知可以無標,但要在註釋裏寫放棄可逆。

思路三 · 組裝按類型字段分揀
它解決什麼問題

各處隨手 push 字串,順序靠約定,單獨成條靠註釋。TokenBudgetContext 會和權限説明擠在同一條 developer 消息裏。壓縮濾網沒法按條處理。混裝之後回滾也拆不開。event_mapping.rs 的註釋承認:build_initial_context 可能把 contextual fragment 和持久 developer 文本捆在一起。出處:codex-rs/core/src/event_mapping.rs 第 69 至 71 行

思路是什麼

第一次組裝發生在 Session::build_initial_context_with_world_state。它把 fragment 按 role()markers() 的開頭、requires_separate_message() 分進幾組:可合併的 developer 段、必須單獨成條的 developer 段、user 段,以及要置頂或置底的特殊段。

視窗身份這一條有點特別。Feature::TokenBudget 打開且模型有 context window 時,它在 world_state 循環之前就被推進單獨組。輸出順序是:先一條合併好的 developer 消息,再一條條單獨的 developer 消息,再一條 contextual user 消息。分揀依據是類型字段。出處:codex-rs/core/src/session/mod.rs 第 3630 至 3728 行

requires_separate_message() 把能不能和別人擠在同一條 developer 消息裏也收進類型。TokenBudgetContextImageResizeNoticeManagedDeveloperInstructions 選擇單獨成條。單獨成條的代價是多佔一條消息、多一次 role 切換。好處是壓縮濾網可以按條處理。

已打開的類型 批准前綴 視窗身份 環境信息 AGENTS.md 中斷通知 分揀 role() markers().0 requires_separate 看字段,不掃正文 developer · 可合併成一條 developer · 各成一條 user · 收成一條 contextual
教學化分揀圖:輸出順序是合併 developer、單獨 developer、contextual user。
點進模型輸入的任意一段,都能回到某個 fragment 類型。
為什麼長期成立

組裝入口只收已登記類型。這是把沒登記的注入擋在門外的通用形狀。類型擋得住沒登記,擋不住登記了但每輪塞 40KB。那一層是評審規則,下一章展開。

橫向對比 · 同一道題的另一種答法

閘開在哪:DeepSeek Harness 選擇發請求時對賬

DSH 寫在倉庫根 AGENTS.md 第 107 行,中文原則收成「模型可見即已記錄」。抵達模型請求的一切都必須能從會話日誌重建,新增一項模型可見輸入就需要新增一個會話事件。

執行面是 invariant.ts。它在 llm/stream 上掛監聽,從 session 日誌 deriveMessages() 得到期望值,再和即將發出的 options.messagesJSON.stringify 比較,對不上就 fail。閘開在崩潰點,能抓住組裝之後又改了 messages 這類只有運行時才出現的漂移。關掉 invariants 服務,這道閘就沒了。

兩側均已核對源碼 · 2026-08-22 · DSH · Model-visible ⟺ logged

真源不同:事件日誌,還是封閉的類型集合

DSH 可以沒有 fragment trait,因為它的真源是事件日誌,messages 是投影。Codex 把能出現在上下文裏的東西先收成封閉的類型集合,再用 marker 做事後識別。

代價也不同。Codex 的類型擋得住沒實現 trait 就塞進 render_full,擋不住 impl 裏面 format! 出來的動態字串。matches_text 認的是文本形狀。兩邊都為可追溯付錢,一個付在每次請求的斷言,一個付在每次加註入的類型摩擦。

兩側均已核對源碼 · 2026-08-22
課堂練習
01

認漏的會是哪一段

UserInstructions 的 begin marker 是 # AGENTS.md instructions,協議常量卻是 <user_instructions>。如果有人按協議常量去寫 matches_text,會認漏現在倉庫裏真實渲染出來的哪一種文本?

再推一步:把上面演示裏的批准前綴打開,壓縮之後預設 matches_text 還能不能把它認回來。developer 側要靠哪張表補這一刀,改錯常量時編譯器會不會響。

Takeaway:往模型上下文裏塞的每一段,先收成一個類型,類型自己知道 role、marker 和 body。需要事後認的帶 begin 和 end,一次性通知主動放棄可逆。組裝按類型字段分揀,沒登記的字串進不了信封。