Codex で仕事を分担させる仕組みを技術検証として試作し、その中で、Codex に JSON Schema を渡して JSON を返させ、私たちの側でもそれを検査してから保存していました。その検査で、正しい JSON が「構造化出力が不正」という分類のエラーで落ちました。この分類だけを見るとモデルの生成の失敗に見えますが、落としていたのは、常駐ワーカーの組み立てでだけ上書きしていた大きさの上限でした。

結論

  • 失敗した2回の最後の出力は 56,859 バイトと 57,215 バイトでした。同じ JSON Schema で検証し直すと、どちらも通りました。
  • 仕組みの正規の上限は 1 MiB と決めていましたが、常駐ワーカーを組み立てる箇所でだけ 48 * 1_024(48 KiB)に上書きしていました。
  • 出力を2つの経路から読んで同じ上限と schema で検査し、診断には JSON path と検証のキーワードだけを残すようにしました。すると原因が $ (maxBytes) まで絞れました。
  • 「schema のエラー」という大きな分類だけを見て、モデルの生成の失敗と決めつけないことにしました。

試した環境

試作した仕組みでは、常駐するワーカーが Codex の CLI を動かし、出力の JSON Schema に合う JSON を最後の応答として返させていました。ワーカーはその JSON をバイト数の上限と schema で検査し、通った物だけを成果物として保存していました。

このとき Codex にさせていたのは、いくつもの調査結果を1本にまとめる「横断の統合」の仕事です。

何が起きたか

私たちのワーカーの検査は、統合の仕事の出力を2回とも「構造化出力が不正」という分類のエラーで落としました。ところが Codex のセッションの中を見ると、出力の schema に合う JSON ができていました。失敗した2回の最後の応答を取り出して同じ schema で検証し直すと、どちらも通ります。

schema には合っているのに、検査では落ちたことになっている。この食い違いが手がかりでした。大きさを並べると、次のようになります。

失敗した2回の出力は56,859バイトと57,215バイトで、常駐ワーカーの上限49,152バイト(48 KiB)を超えていた。直した後に保存した統合は55,881バイト。

診断を「どこで」と「何の規則で」に絞る

最初の診断は、Codex CLI の --output-last-message が書き出すファイルだけに頼っていました。そこで、Codex が出す JSONL のうち、最後の item.completed の agent_message を2本目の出力の経路として足しました。

次の図は、直した後の出力の読み方です。出力ファイルを主の経路、JSONL の最後の応答を予備の経路とし、どちらも同じ上限と同じ schema で検査します。

出力を2つの経路から読み、同じ検査にかける流れ

2つの経路が違う内容を返したときに、片方を無条件に採用することはしません。検査に落ちたときの診断には、出力の値そのものは残しません。どこで(JSON path)、何の規則に(検証のキーワード)落ちたかだけを残します。ログに出力の中身を書き出さずに、落ちた場所ははっきりさせる設計です。

この形にしてから診断を見ると、落ちた所は $ (maxBytes) でした。JSON の根($)が、バイト数の上限(maxBytes)に引っかかったという意味です。形ではなく大きさで落ちていたことが、ここで分かりました。

同じ荷物を運ぶ2本の流れが、1つの検査の場所に集まっている絵

48 KiB はどこにあったか

この仕組みでは、構造化出力の上限を正規の契約として 1 MiB と決めていました。約 57 KB の出力なら十分に収まります。ところが、常駐ワーカーを組み立てる処理でだけ、この値を上書きしていました。

// 直す前: 常駐ワーカーの組み立て(抜粋)
// The internal completion endpoint is deliberately capped at 64 KiB.
maxStructuredOutputBytes: 48 * 1_024,

この行には、内部の完了通知の口は 64 KiB に抑えてある、というコメントを付けていました。直したときに、このコメントを、ワーカーとのやり取りの包み(envelope)は 8 MiB という別の、より大きい上限を持つ、という説明に書き直しました。通信の包みの上限と、その中に入る構造化出力の上限は別の物です。片方の事情を、もう片方の値に持ち込んでいたことになります。

上書きしていたのは常駐ワーカーの組み立てだけだったので、正規の契約の値を見ているだけでは見つかりません。上限を探すときは、既定の値を決めている所だけでなく、部品を組み立てる箇所で上書きしていないかも見る必要があります。

直し方

まず、常駐ワーカーの上限を、名前の付いた 1 MiB の定数にそろえました。

export const CODEX_WORKER_MAX_STRUCTURED_OUTPUT_BYTES = 1024 * 1024;

// 常駐ワーカーの組み立て(抜粋)
maxStructuredOutputBytes: CODEX_WORKER_MAX_STRUCTURED_OUTPUT_BYTES,

最後に、上限は1つにまとめず、対象ごとに別の有限の値を持たせたままにしました。

対象上限
プロンプト1 MiB
出力の JSON Schema256 KiB
構造化出力の本文1 MiB
Codex の JSONL8 MiB

どう確かめたか

直した版を配った後、止まっていた統合の仕事を取り直すと、55,881 バイトのまとめが保存できました。

確かめていないこと

  • 1 MiB は、この仕組みで当時決めていた契約の値です。モデルや API の一般的な上限を示す物ではありません。
  • プロンプト、schema、構造化出力の本文、JSONL、通信の包みの上限は、それぞれ別の物として扱う必要があります。

おわりに

構造化出力が「schema のエラー」で落ちたら、まず出力を取り出して、同じ schema で検証し直してみてください。それで通るなら、疑うべきはモデルではなく、出力を受け取る側の上限や組み立てです。

※ Codex CLI のオプションや出力の形式は執筆時点の情報で、いまは変わっている可能性があります。