Xでシェア

Writing / AIエージェント

N° 03

CodexのAgent Loopを理解する:モデル推論とツール実行を分けるハーネスの設計

Codex CLIが調査、編集、テスト、再修正を繰り返せる仕組みを、モデル・ハーネス・ツール・環境の責務に分解して解説します。ツール呼び出しを境界に、実行結果の再入力、権限、MCP、終了条件、コンテキスト管理まで追います。

はじめに:Codexはどこで「考え」、どこで「実行」しているのか

Codex CLIへバグ修正を任せると、画面では次のような流れが見える。

plaintext
ファイルを調べる
↓
コードを変更する
↓
テストを実行する
↓
失敗する
↓
追加修正する
↓
再度テストする

一見すると、Codexという一つの知性が、リポジトリを読み、コマンドを実行し、結果を理解しているように見える。しかし「Codexがコマンドを実行する」という表現は便利ではあっても、厳密ではない。

中心にいるモデルは、入力されたコンテキストから次の出力を生成する。ローカルのファイルシステムへ手を伸ばしたり、OSプロセスを直接起動したりする主体ではない。モデルが生成したツール呼び出しを解釈し、現実の環境で実行するのは、その外側にあるハーネスである。実行結果は新しい入力としてモデルへ戻り、次の判断に使われる。

つまり、Codexがソフトウェアを変更できる理由は、モデル自身がコマンドを実行するからではない。モデルが「次に何をするか」を構造化して出力し、ハーネスがそれを実行し、結果を再びモデルへ返すからである。この反復がAgent Loopだ。

本記事ではCodex製品全体ではなく、主にCodex CLIを通して観測できる共通ハーネスの実行モデルを扱う。前提とするのは、LLM、JSON、CLI、終了コード、Git、テスト、Function Callingについての基礎知識である。Responses APIやストリーミングの詳細は、必要な範囲だけ説明する。

モデル・ハーネス・ツール・環境の4層

Agent Loopを理解するには、「Codex」を単一の箱として見ない方がよい。少なくとも、モデル、ハーネス、ツール、環境という4層に分けると、責務の境界が明確になる。

モデル

モデルが担当するのは意思決定である。ユーザーの要求と与えられた証拠を読み、仮説を立て、次に読むべきファイルや使うべきツールを選び、引数を生成する。ツール結果を受け取った後には計画を更新し、作業が完了したと判断すればユーザー向けの最終回答を生成する。

一方、モデル自体はOSプロセスの起動、ファイルへの直接書き込み、ネットワーク接続、権限の強制、タイムアウト処理を担当しない。それらを「したい」と出力することと、実際に副作用を起こすことは別である。

ハーネス

ハーネスはモデルをエージェントとして動かす実行基盤だ。モデルへ渡す入力を組み立て、利用可能なツールとスキーマを登録し、APIを呼び出し、ストリームされたイベントを受信する。モデルがツール呼び出しを返せば、対応する実装へ振り分け、権限や承認、サンドボックスを適用し、結果を次の入力へ整形する。

さらに、会話履歴、コンテキスト圧縮、スレッドの永続化、設定と認証、キャンセル、UIへの進捗通知もハーネス側の関心事である。モデルが同じでも、ハーネスの設計が異なればエージェントの挙動は変わる。

ツール

ツールは、モデルの決定を外部能力へ接続するインターフェースである。シェル、ファイル編集、Web検索、計画更新、MCPサーバー、社内APIなどが例になる。

モデルへは通常、ツール名だけでなく、説明、引数、型、必須項目、制約を含むスキーマが渡される。モデルはそれを能力一覧として読み、「どのツールを、どの引数で呼ぶか」を出力する。

環境

環境はツールが観測し、変更する対象である。リポジトリ、ファイルシステム、Gitの作業ツリー、インストール済みコマンド、テストランナー、ネットワーク、環境変数、認証情報が含まれる。

この関係は、次の一文に要約できる。

モデルが意思決定を行い、ハーネスが能力と制約を提供し、ツールが環境を観測・変更する。

plaintext
┌─────────────┐
│    User     │
└──────┬──────┘
       │ request
       ▼
┌─────────────┐
│   Harness   │
│ context構築 │
└──────┬──────┘
       │ instructions / tools / input
       ▼
┌─────────────┐
│    Model    │
│  decision   │
└──────┬──────┘
       │ function call または最終回答
       ▼
┌─────────────┐
│   Harness   │
│ policy確認  │
│ tool実行    │
└──────┬──────┘
       ▼
┌─────────────┐
│ Environment │
└──────┬──────┘
       │ stdout / error / diff
       └───────────────→ 次のモデル推論

Agent Loopを最小の疑似コードで表す

実装の細部を除けば、Agent Loopは次の疑似コードで表現できる。

python
history = build_initial_context(
    instructions=instructions,
    tools=available_tools,
    user_message=user_message,
)

while True:
    response = model.infer(history)

    if response.has_final_message():
        return response.final_message

    for tool_call in response.tool_calls:
        result = harness.execute(tool_call)
        history.append(tool_call)
        history.append(result)

    history = compact_if_needed(history)

重要なのは、model.inferharness.executeが異なる処理だという点だ。前者は、与えられたコンテキストから回答またはアクション要求を生成する。後者は、現実の環境で副作用を起こし、観測結果を返す。

実際のハーネスには、ストリーミング、同一レスポンス内の複数項目、承認待ち、タイムアウト、出力の切り詰め、ツールエラー、キャンセル、コンテキスト圧縮、UIイベントなどが加わる。それでも中核は、「推論する、必要なら実行する、結果を追加して再推論する」という循環である。

1回目の推論まで:ハーネスがモデルへ渡すもの

ユーザーが画面へ入力した文だけが、モデルに届くプロンプトではない。Codexのハーネスは、概念的には次の情報から初回入力を構築する。

  • モデル向けの基本指示
  • 利用可能なツールとそのスキーマ
  • サンドボックス、承認、権限に関する説明
  • AGENTS.mdなどのプロジェクト指示
  • Skillsに関する情報
  • 作業ディレクトリとシェル
  • 過去のメッセージ、ツール呼び出し、ツール結果
  • 今回のユーザー入力

OpenAIの解説では、CodexはResponses APIのinstructionstoolsinputを中心に入力を構成し、権限、プロジェクト指示、Skills、作業ディレクトリ、シェルなどの情報も加える。したがって、ユーザーの一文が同じでも、リポジトリや設定、利用可能なツールが違えば、初回の判断は変わり得る。

ツール定義は能力であると同時にプロンプトである

たとえばシェルツールを提供する場合、モデルが見る契約は概念的には次のようになる。

json
{
  "name": "shell",
  "description": "Run a command in the project environment",
  "parameters": {
    "type": "object",
    "properties": {
      "command": {
        "type": "array",
        "items": { "type": "string" }
      },
      "workdir": { "type": "string" }
    },
    "required": ["command", "workdir"]
  }
}

説明が曖昧、引数が過剰、結果の意味が不明確なら、モデルは適切に呼び出しにくい。逆に、目的と制約が明確なツールは行動空間を整理する。ツール設計は単なるAPI設計ではなく、モデルに能力の使い方を教えるプロンプト設計でもある。

同じモデルでもエージェント性能は同じではない

提供するツール、ツールの説明、環境情報、プロジェクト指示、履歴に残す内容、出力の切り詰め方が変われば、同じモデルでも振る舞いは変わる。テストコマンドへ到達できないハーネスでは検証できず、巨大なログを無加工で返すハーネスでは重要な証拠が埋もれやすい。

エージェント性能をモデル性能だけで説明できない理由がここにある。モデルは中核的な意思決定器だが、その判断に使える観測と、実行できる行動を設計するのはハーネスである。

推論と実行の境界:ツール呼び出しという契約

モデルの出力は、大きく二つに分けて考えられる。一つはユーザーへの最終メッセージ、もう一つはツール呼び出しである。

最終メッセージなら、ハーネスは通常、そのターンの制御をユーザーへ返す。

plaintext
修正が完了しました。対象のテストも通過しています。

ツール呼び出しなら、モデルは構造化された実行要求を返す。

json
{
  "type": "function_call",
  "name": "shell",
  "arguments": {
    "command": ["rg", "UserService", "src"],
    "workdir": "/workspace/project"
  },
  "call_id": "call_123"
}

この時点ではrgはまだ実行されていない。モデルが出力したのは、「shellというツールをこの引数で呼び出してほしい」という要求にすぎない。具体的な項目名はAPIやツール実装の更新で変わり得るが、構造化された要求を境界に推論と実行を分離する原理は変わりにくい。

ハーネスが要求を実行へ変換する

ハーネスはツール呼び出しを受け取ると、概念的に次の処理を行う。

  1. ツール名を登録済み実装へ解決する。
  2. 引数を解析し、スキーマと照合する。
  3. コマンド、パス、ネットワークなどに対するポリシーを確認する。
  4. 必要ならユーザー承認を要求し、応答までターンを一時停止する。
  5. 許可された実行環境またはサンドボックス内でツールを動かす。
  6. 標準出力、標準エラー、終了コード、差分、例外などを収集する。
  7. モデルへ返せるツール結果へ整形する。

この分離には実務的な意味がある。副作用の前に権限を確認でき、危険な操作を拒否でき、監査ログを残せる。ツールの実装を差し替えたり、同じモデルを異なる環境へ接続したりすることもできる。

モデルの主張とポリシー決定を混同しない

モデルが「このコマンドは安全です」と説明しても、それは許可を意味しない。モデルはポリシーの対象であって、最終的な強制主体ではない。許可するパス、ネットワークアクセス、書き込み範囲、承認条件は、ハーネスまたは実行基盤がモデル外部で決定すべきである。

さもなければ、自然言語で定義した制約を、同じ自然言語を解釈するモデル自身に強制させることになる。安全境界には、検証可能で回避しにくい仕組みが必要だ。

shellとMCPは同じ安全境界ではない

Codexが提供するshellツールのサンドボックスと、外部MCPサーバーが提供するツールを同一視してはいけない。公式解説でも、Codexの権限に関する説明が直接適用されるのはCodex提供のshellツールであり、MCPなど外部ツールの副作用には、そのツール自身のガードレールが必要だと説明されている。

たとえばMCPツールが本番データベースへ変更を書き込めるなら、シェルのファイルサンドボックスだけでは防御にならない。MCPサーバー側で認証、認可、入力検証、操作範囲、監査、必要に応じた承認を設計する必要がある。

承認待ちは終了ではなく一時停止である

危険または権限外の操作にユーザー承認が必要な場合、Agent Loopは完了したわけではない。ハーネスは承認要求をUIへ通知し、回答を待ち、許可または拒否をツール実行へ反映する。Codex App Serverの解説でも、サーバーからクライアントへ承認を要求し、応答までターンを停止できる双方向通信が説明されている。

ツール結果は次の推論における「観測」である

ツールの実行後、ハーネスは呼び出しと対応する結果を履歴へ追加する。概念的には次の形式だ。

json
{
  "type": "function_call_output",
  "call_id": "call_123",
  "output": "src/service/UserService.java:42: public User createUser(...)"
}

call_idは、どの要求に対する結果かを関連付ける。モデルは、この結果を含む更新済みコンテキストから再び出力を生成する。

重要なのは、ツール結果を「観測」と捉えることだ。モデルの内部に、現在のリポジトリ、直近のGit差分、実際のテスト結果が最初から入っているわけではない。外部状態はツールを通じて初めて観測される。

plaintext
仮説:UserServiceのバリデーションが不足している
  ↓
検索を実行する
  ↓
該当する検証が見つからない
  ↓
仮説を更新する
  ↓
ControllerとDTOの変換処理を調べる

失敗も情報になる

ツールの終了コードが0である必要はない。終了コード1、コンパイルエラー、テスト失敗、ファイル不在、権限拒否、タイムアウト、コマンド未導入は、すべて次の判断材料になる。

テスト失敗はAgent Loopの失敗ではない。むしろ、実装仮説を反証する高品質な観測になり得る。ハーネスがエラーを隠さず、終了コードや関連出力を明瞭に返せば、モデルは修正箇所やテスト前提を見直せる。

観測は絶対的な真実ではない

ただし、ツール結果が現実を完全に表すとは限らない。出力が切り詰められている、検索範囲が狭い、コマンド引数が誤っている、キャッシュを参照している、テストが一部しか走っていない、実行環境が本番と異なる、といった可能性がある。

したがって「コマンドが成功した」と「目的が達成された」は同義ではない。モデルは観測の範囲と信頼性を考え、必要なら別のツールやより広い検証で確かめる。ハーネスも、切り詰めの有無、終了コード、実行対象を曖昧にしない方がよい。

実例:バグ修正を1ループずつ追う

次の依頼を考える。

plaintext
ユーザー名が空文字でも登録できてしまう問題を修正し、
関連するテストを追加してください。

ステップ1:探索

初回推論では、モデルは変更箇所をまだ知らない。まず検索を選ぶ。

json
{
  "name": "shell",
  "arguments": {
    "command": ["rg", "createUser|UserService", "src"],
    "workdir": "/workspace/project"
  }
}

ハーネスはポリシーを確認し、rgを起動し、標準出力と終了コードを返す。ここで得たファイル名や行番号が次の推論の証拠になる。

ステップ2:対象コードを読む

検索結果からUserServiceUserControllerが候補になれば、モデルは該当範囲の読み取りを要求する。

bash
sed -n '1,240p' src/main/java/example/UserService.java

モデルがファイルを「知った」のは、この出力がコンテキストへ入った後である。読み取っていない別ファイルの内容は、依然として仮説にすぎない。

ステップ3:編集する

モデルは、空文字だけでなく空白だけの名前も拒否すべきか、既存の例外形式を維持すべきか、検証をサービス層とDTOのどちらへ置くべきかを、観測したコードと指示から判断する。そして編集ツールの呼び出しを生成する。

副作用を起こすのは編集ツールであり、モデルではない。ハーネスは対象パスと権限を確認し、変更を適用し、結果または差分を履歴へ戻す。

ステップ4:テストする

次に、モデルは関連テストの実行を要求する。

bash
./gradlew test --tests UserServiceTest

結果が次だったとする。

plaintext
Expected IllegalArgumentException but no exception was thrown

これは作業の終端ではない。新しい観測である。

ステップ5:反証から再推論する

モデルは、少なくとも次の仮説を比較できる。

  • 修正したメソッドが実際の登録経路で呼ばれていない。
  • Controllerまたはmapperが入力値を変換している。
  • テストが別のサービス実装を生成している。
  • 検証条件またはテストの期待値が誤っている。

追加の検索やファイル読み取りを要求し、証拠を増やす。ここで重要なのは、「最初の計画を最後まで実行する」のではなく、外部状態によって計画を更新する点である。

ステップ6:再編集と再検証

原因が実際の経路にある別メソッドだと分かれば、モデルは修正を更新し、対象テストを再実行する。その後、必要に応じて関連テスト群、静的解析、ビルド、Git差分の確認へ進む。

plaintext
User      Harness       Model        Tool        Repository
 |           |             |            |              |
 | request   |             |            |              |
 |---------->| inference   |            |              |
 |           |------------>|            |              |
 |           | tool call   |            |              |
 |           |<------------|            |              |
 |           | execute ---------------->|              |
 |           | result <-----------------|              |
 |           | inference + result       |              |
 |           |------------>|            |              |
 |           | edit call   |            |              |
 |           |<------------| apply ------------------->|
 |           | test ------------------->|              |
 |           | fail <-------------------|              |
 |           | inference   |            |              |
 |           |------------>|            |              |
 |           |       ...反復...         |              |

ステップ7:最終回答

検証が十分だとモデルが判断すると、次のツール要求ではなくユーザー向けメッセージを返す。

plaintext
空文字と空白のみのユーザー名を拒否するよう修正しました。
UserServiceTestに関連ケースを追加し、対象テストは通過しています。

この出力によって、通常のターンは終了する。「テスト成功」というイベント自体がループを機械的に止めるのではない。

Agent Loopはいつ終了するのか

基本的な終了条件は、モデルが追加のツール呼び出しではなく、ユーザー向けの最終アシスタントメッセージを生成することだ。したがって、Agent Loopは「正解するまで必ず回るループ」ではなく、「モデルが次の行動を要求し、それをハーネスが継続できる間は回るループ」である。

現実には、ほかにもターンを止める、または中断する条件がある。

  • モデルが完了したと判断する。
  • ユーザーへの質問や確認が必要になる。
  • ユーザーがキャンセルする。
  • 必要な権限または承認を得られない。
  • ツールが存在しない、または利用不能である。
  • タイムアウト、上限、回復不能なエラーに達する。

テストが通れば自動的に完成するわけでも、失敗すれば必ず継続するわけでもない。モデルが早く完了を宣言すれば、十分な検証なしで終了する可能性がある。逆に、完了条件が曖昧なら、価値の低い追加探索を続ける可能性もある。

そのため、ハーネスを実装する側は最大ターン数や時間、キャンセル、エラー分類を設計し、利用者は観測可能な完了条件を指示する必要がある。

長いループを支えるコンテキスト管理

ループが続くほど、履歴にはユーザーメッセージ、モデル出力、ツール呼び出し、ファイル内容、ログ、エラー、差分が増えていく。次回の推論には過去の経緯が必要だが、コンテキストウィンドウは無限ではない。

OpenAIのAgent Loop解説では、後続リクエストは以前の入力を接頭辞として保ちつつ、ツール呼び出しと結果を追加する。正確な接頭辞を維持する構成は、プロンプトキャッシュも活用しやすい。しかし長いログや大きなファイルを追加し続ければ、入力サイズ、レイテンシ、重要情報の見つけやすさに影響する。

出力を増やすことと観測を良くすることは違う

たとえば巨大なログ全体を返すより、まずエラー候補を絞る方が有用である。

bash
rg "ERROR|Exception" app.log | tail -n 100

ただし絞り込みすぎれば原因を失う。良い観測は、仮説を判定するのに十分で、無関係な情報が少なく、対象範囲が明示されている。ハーネスでは最大出力量だけでなく、先頭・末尾の保持、切り詰め表示、構造化結果、後から追加取得する手段も設計対象になる。

コンパクションとその代償

履歴がしきい値へ近づくと、詳細な履歴をより小さな表現へ置き換えるコンパクションが必要になる。公式解説によれば、Codexは設定された上限を超える場合にResponses APIのcompactエンドポイントを利用する。

コンパクションは長時間の作業を続ける余地を作る一方、細かな根拠、未解決の仮説、初期の制約が弱くなる可能性がある。だから、重要な不変条件を一時的な会話だけに置かないことが重要だ。ビルド手順、禁止事項、必須テスト、アーキテクチャ上の制約をAGENTS.mdなどの再取得可能なプロジェクト指示へ残すことには、長いAgent Loopでの情報劣化を抑える意味がある。

Agent Loopを理解するとCodexの使い方が変わる

内部構造の理解は、単なる実装知識ではない。Codexへの指示と失敗分析を具体的に改善できる。

完了条件を観測可能にする

plaintext
いい感じに直して

という依頼では、モデルが何を観測して完了を判断すべきか分かりにくい。代わりに、次のように書く。

plaintext
修正後にUserServiceTestを実行してください。
既存APIのレスポンス形式は変更しないでください。
最後に変更ファイルとテスト結果を報告してください。

テスト、ビルド、型検査、差分確認のような観測可能な条件は、ループにフィードバックを与える。ここでも、テストが仕様全体を保証するわけではないため、守るべき不変条件も明記する。

調査と実装の境界を指定する

plaintext
まず原因を調査し、変更候補と影響範囲を説明してください。
私の確認後に実装してください。

この指示は、モデルの知性を変えるのではなく、許可する副作用とターンの終了条件を変える。調査フェーズでは読み取りと分析に限定し、実装は次のユーザー判断へ委ねられる。

必要な観測へ絞る

巨大ファイルやログを丸ごと読むより、検索、範囲指定、対象テストを使う方がコンテキスト効率はよい。モデルへ「最初に関連箇所を特定し、その後に必要な範囲だけ読む」と指示することもできる。

一方、絞り込みによる見落としを防ぐため、最後には関連範囲を広げた検索やテストを求める。探索時の効率と完了時の網羅性は別に設計する。

「モデルを変える」以外の改善点を見る

期待した挙動にならないとき、モデルだけを疑うのは早い。次も確認すべきである。

  • ツールの説明とスキーマは明確か。
  • 必要なコマンド、ファイル、ログへ到達できるか。
  • 権限が狭すぎる、または広すぎないか。
  • ツール結果に終了コードや切り詰め状態が含まれるか。
  • プロジェクト指示が矛盾していないか。
  • 検証可能な完了条件があるか。
  • MCP側にも適切な認可とガードレールがあるか。

エージェントのデバッグとは、モデル出力だけでなく、入力構築、ツール契約、実行ポリシー、観測、履歴更新の連鎖を調べることである。

よくある誤解

誤解1:モデルがシェルを直接操作している

モデルはツール呼び出しを生成する。実際にプロセスを起動するのはハーネスまたはツール実装である。

誤解2:画面上の説明がモデル内部の推論のすべてである

UIに現れる進捗、要約、ツールイベントは観測可能なインターフェースであり、モデル内部の計算そのものではない。Agent Loopを理解するうえでは、入力、ツール呼び出し、結果、最終メッセージという確認可能な境界を追う方が正確である。

誤解3:テストが通れば必ず正しい

実行したテストが狭い、期待値が誤っている、本番条件と異なる可能性がある。Agent Loopが成功したことと、仕様が正しいことは別である。

誤解4:サンドボックスがあれば全ツールが安全である

Codexのshellに適用される制限が、外部MCPツールの副作用まで自動的に制御するとは限らない。安全性は各境界で設計する必要がある。

誤解5:高性能なモデルならハーネスは単純でよい

実務では、コンテキスト構築、ツール設計、権限、承認、出力制御、エラー処理、観測可能性が品質を左右する。強いモデルも、見えない状態を正確に観測したり、存在しない能力を使ったりはできない。

まとめ:Agent Loopは推論と現実を接続する実行基盤

Agent Loopでは、モデルが次の行動を決め、ハーネスがその要求をポリシーの下で実行し、ツールが環境を観測または変更する。実行結果は新しい証拠としてモデルへ戻り、モデルが最終メッセージを返すまで反復される。

この境界を理解すると、権限はどこで強制すべきか、MCPには何を実装すべきか、失敗をどこで観測すべきか、なぜコンテキスト管理が品質へ影響するのかが見えやすくなる。

Codexを理解するうえで重要なのは、モデルがどれだけ賢いかだけではない。モデルの判断をどのように現実の操作へ変換し、その結果をどのような情報として再びモデルへ返すかである。Agent Loopとは、推論とソフトウェア開発環境を接続するための実行基盤なのだ。