0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Codex CLI + context-modeの効果を実プロジェクトで検証する:ログ解析からコード修正まで

0
Last updated at Posted at 2026-07-21

Codex CLI + context-modeの効果を実プロジェクトで検証する:ログ解析からコード修正まで

前回の記事では、WSL上にCodex CLIとcontext-modeを導入し、MCPとして使えるところまで試しました。

導入自体はできましたが、実際の開発でどのくらい役に立つのかは、まだよく分かっていません。

そこで今回は、FastAPIとDuckDBで簡単なAPIを作り、わざと500エラーを発生させます。そのうえで、通常のCodex CLIだけで調べた場合と、context-modeを有効にした場合で、ログの扱い方や修正までの流れに違いが出るかを見ていきます。

今回用意するエラーは、SQLのカラム名を1か所間違えるだけの単純なものです。正直なところ、ログを数行読めば人でも原因に気付けます。最初から複雑な題材にすると何を比べているのか分かりにくくなるため、まずは小さな例から始めることにしました。

今回やること

全体の流れは次のとおりです。
ChatGPT Image 2026年7月18日 13_56_08.png

見るのは、単純なトークン数だけではありません。

  • どのファイルを調べたか
  • 長いログをそのまま読み込んでいないか
  • 修正したファイルが必要以上に増えていないか
  • テストまで実行したか
  • Codex CLI側の使用量
  • context-modeのctx statsに表示された内容

厳密なベンチマークではありませんが、条件がずれないように、同じコミットから2つの作業環境を作ります。

検証環境

この記事では、次の環境を使います。

項目 内容
ホストOS Windows 11 Pro
Linux環境 WSL2 / Ubuntu 24.04 LTS
Python管理 pyenv
Python 3.13.14
仮想環境 Python標準のvenv
Node.js 22.5以上
AIコーディングツール Codex CLI
MCPプラグイン context-mode
Web API FastAPI
データベース DuckDB
テスト pytest

この記事では、上のバージョンを例に進めます。バージョンが変わっても大まかな流れは同じですが、context-modeは更新が速いため、導入部分は公式READMEも合わせて確認してください。

Step 1:WSL上にpyenvでPython環境を作る

前回はNode.jsをnvmで管理しました。今回はPythonをpyenvで管理します。

Pythonはpyenv、Node.jsはnvmと分けておくと、プロジェクトごとのバージョンを切り替えやすくなります。
anyenvを使ってもよいと思います。

1.1 必要なパッケージを入れる

WSLのUbuntuを起動し、Pythonのビルドに必要なパッケージを入れます。

sudo apt update

sudo apt install -y \
  build-essential \
  curl \
  git \
  jq \
  libbz2-dev \
  libffi-dev \
  liblzma-dev \
  libncursesw5-dev \
  libreadline-dev \
  libsqlite3-dev \
  libssl-dev \
  libxml2-dev \
  libxmlsec1-dev \
  tk-dev \
  xz-utils \
  zlib1g-dev

jqはPythonのビルドには使いません。後半でCodex CLIが出力するJSONLを読むために、この段階で一緒に入れています。

1.2 pyenvをインストールする

anyenvでもOKです。
この部分は、それぞれの環境に合わせてください。

curl -fsSL https://pyenv.run | bash

Bashからpyenvを使えるように、.bashrc.profileへ設定を追加します。

echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bashrc
echo '[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bashrc
echo 'eval "$(pyenv init - bash)"' >> ~/.bashrc

echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.profile
echo '[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.profile
echo 'eval "$(pyenv init - bash)"' >> ~/.profile

設定を反映します。

exec "$SHELL"

バージョンが表示されれば準備完了です。

pyenv --version

1.3 Python 3.13.14を入れる

pyenv install 3.13.14

ソースからビルドするため、少し時間がかかります。終わったら、インストール済みのバージョンを見ておきます。

pyenv versions

1.4 プロジェクトを作る

mkdir -p ~/projects/codex-context-mode-test
cd ~/projects/codex-context-mode-test

このディレクトリで使うPythonを指定します。ChatGPT Image 2026年7月18日 13_56_08.png

pyenv local 3.13.14

pyenv localを実行すると、カレントディレクトリに.python-versionが作られます。

cat .python-version
python --version

続いて、Python標準のvenvで仮想環境を作ります。

python -m venv .venv
source .venv/bin/activate

Pythonの参照先も見ておきます。

which python
python --version
python -m pip --version

which pythonがプロジェクト内の.venv/bin/pythonを指していれば問題ありません。

1.5 Node.jsとCodex CLIも見ておく

context-modeはNode.js 22.5以上、またはBunを前提としています。前回の記事で導入済みでも、別のシェルを開いたときにPATHが変わることがあるため、念のため確認します。

node --version
npm --version
codex --version
command -v node
command -v codex

nvmを使っている場合は、nvmが有効になったWSLのシェルからCodex CLIを起動します。

Step 2:検証用のFastAPIプロジェクトを作る

今回作るのは、DuckDBに保存した雨量観測値をJSONで返すだけの小さなAPIです。

2.1 フォルダーを作る

cd ~/projects/codex-context-mode-test

mkdir -p app/routers tests data logs scripts

touch app/__init__.py
touch app/routers/__init__.py

最終的な構成は次のようになります。

codex-context-mode-test/
├─ app/
│  ├─ __init__.py
│  ├─ main.py
│  ├─ database.py
│  ├─ schemas.py
│  └─ routers/
│     ├─ __init__.py
│     └─ observations.py
├─ data/
│  └─ observations.duckdb
├─ logs/
│  ├─ uvicorn.log
│  └─ curl_response.txt
├─ scripts/
│  └─ capture_error.sh
├─ tests/
│  └─ test_observations.py
├─ .gitignore
├─ .python-version
├─ AGENTS.md
├─ pyproject.toml
└─ requirements.txt

2.2 requirements.txt

requirements.txt
fastapi==0.139.2
duckdb==1.5.4
uvicorn[standard]==0.51.0
pytest==8.4.2
httpx==0.28.1

インストールします。

python -m pip install --upgrade pip
python -m pip install -r requirements.txt

2.3 pyproject.toml

pytestからプロジェクト直下のappパッケージを読み込めるようにします。

pyproject.toml
[tool.pytest.ini_options]
pythonpath = ["."]
testpaths = ["tests"]

2.4 DuckDBの初期化処理

app/database.py
"""DuckDBの初期化と接続先を管理するモジュール。"""

from pathlib import Path

import duckdb

PROJECT_ROOT = Path(__file__).resolve().parents[1]
DB_PATH = PROJECT_ROOT / "data" / "observations.duckdb"


def initialize_database() -> None:
    """検証用テーブルとサンプルデータを作成する。"""
    DB_PATH.parent.mkdir(parents=True, exist_ok=True)

    with duckdb.connect(str(DB_PATH)) as connection:
        connection.execute(
            """
            CREATE TABLE IF NOT EXISTS observations (
                id INTEGER PRIMARY KEY,
                station_name VARCHAR NOT NULL,
                observed_at TIMESTAMP NOT NULL,
                rain_mm DOUBLE NOT NULL
            )
            """
        )

        count = connection.execute(
            "SELECT COUNT(*) FROM observations"
        ).fetchone()[0]

        if count == 0:
            connection.executemany(
                """
                INSERT INTO observations
                    (id, station_name, observed_at, rain_mm)
                VALUES (?, ?, ?, ?)
                """,
                [
                    (1, "観測所A", "2026-07-17 09:00:00", 2.5),
                    (2, "観測所A", "2026-07-17 10:00:00", 7.0),
                    (3, "観測所B", "2026-07-17 10:00:00", 1.5),
                ],
            )

2.5 レスポンスモデル

app/schemas.py
"""APIレスポンスのデータ構造を定義する。"""

from datetime import datetime

from pydantic import BaseModel


class Observation(BaseModel):
    """雨量観測値1件を表すレスポンスモデル。"""

    id: int
    station_name: str
    observed_at: datetime
    rain_mm: float

2.6 わざと不具合を入れたAPIルーター

ここで、存在しないobservation_time列を指定します。

正しい列名はobserved_atですが、この時点では直しません。

app/routers/observations.py
"""雨量観測値を返すAPIルーター。"""

import duckdb
from fastapi import APIRouter

from app.database import DB_PATH
from app.schemas import Observation

router = APIRouter(prefix="/observations", tags=["observations"])


@router.get("", response_model=list[Observation])
def list_observations() -> list[Observation]:
    """DuckDBから観測値を読み込み、時刻順で返す。"""
    with duckdb.connect(str(DB_PATH), read_only=True) as connection:
        rows = connection.execute(
            """
            SELECT
                id,
                station_name,
                observation_time,
                rain_mm
            FROM observations
            ORDER BY observed_at
            """
        ).fetchall()

    return [
        Observation(
            id=row[0],
            station_name=row[1],
            observed_at=row[2],
            rain_mm=row[3],
        )
        for row in rows
    ]

2.7 FastAPIのエントリーポイント

app/main.py
"""FastAPIアプリケーションのエントリーポイント。"""

from collections.abc import AsyncIterator
from contextlib import asynccontextmanager

from fastapi import FastAPI

from app.database import initialize_database
from app.routers.observations import router as observations_router


@asynccontextmanager
async def lifespan(_: FastAPI) -> AsyncIterator[None]:
    """アプリ起動時に検証用データベースを初期化する。"""
    initialize_database()
    yield


app = FastAPI(
    title="context-mode検証用API",
    lifespan=lifespan,
)
app.include_router(observations_router)

2.8 テストを作る

tests/test_observations.py
"""雨量観測値APIのテスト。"""

from fastapi.testclient import TestClient

from app.main import app


def test_list_observations() -> None:
    """観測値一覧が正常に返ることを確認する。"""
    with TestClient(app, raise_server_exceptions=False) as client:
        response = client.get("/observations")

    assert response.status_code == 200

    payload = response.json()
    assert len(payload) == 3
    assert payload[0]["station_name"] == "観測所A"

raise_server_exceptions=Falseを付けているため、アプリ側で例外が発生してもpytestへ直接投げ直されず、HTTP 500として確認できます。

2.9 エラーログを保存するスクリプト

Uvicornの起動とcurlの実行を毎回手作業で行うと条件がずれやすいため、簡単なスクリプトにしました。

scripts/capture_error.sh
#!/usr/bin/env bash
set -euo pipefail

PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
PORT="${1:-8000}"
LOG_DIR="$PROJECT_ROOT/logs"
UVICORN_LOG="$LOG_DIR/uvicorn.log"
CURL_LOG="$LOG_DIR/curl_response.txt"

mkdir -p "$LOG_DIR"
cd "$PROJECT_ROOT"

"$PROJECT_ROOT/.venv/bin/uvicorn" \
  app.main:app \
  --host 127.0.0.1 \
  --port "$PORT" \
  > "$UVICORN_LOG" 2>&1 &

UVICORN_PID=$!

cleanup() {
  kill "$UVICORN_PID" 2>/dev/null || true
  wait "$UVICORN_PID" 2>/dev/null || true
}
trap cleanup EXIT

sleep 2
curl -s -i "http://127.0.0.1:${PORT}/observations" | tee "$CURL_LOG"
sleep 1

実行権限を付けます。

chmod +x scripts/capture_error.sh

2.10 AGENTS.md

Codexへ毎回同じ条件を伝えるため、プロジェクトルートにAGENTS.mdを置きます。

AGENTS.md
# Working agreements

- Python 3.13を前提とする。
- 変更前に原因を短く説明する。
- 不具合と関係のないファイルは変更しない。
- Pythonコマンドは`.venv/bin/python`を使用する。
- 修正後に`.venv/bin/python -m pytest -q`を実行する。
- Pythonソースには型ヒントと日本語docstringを付ける。
- `.venv/``data/``logs/`はGitへ追加しない。

Codex CLIは、作業開始時にプロジェクト内のAGENTS.mdを読み込みます。プロンプトを長くしすぎずに、プロジェクト固有のルールを渡せるので便利です。

2.11 .gitignore

.gitignore
.venv/
__pycache__/
.pytest_cache/
*.pyc
data/*.duckdb
logs/*.log
logs/*.txt
logs/*.jsonl
logs/*.stderr

Step 3:500エラーを再現する

まずpytestを実行します。

cd ~/projects/codex-context-mode-test
source .venv/bin/activate
python -m pytest -q

テストは失敗します。

F                                                                        [100%]

assert response.status_code == 200
E       assert 500 == 200

1 failed

Uvicorn側のログも保存します。

./scripts/capture_error.sh 8000

レスポンスは500です。

HTTP/1.1 500 Internal Server Error
content-type: text/plain; charset=utf-8

Internal Server Error

logs/uvicorn.logを開くと、DuckDBのエラーが記録されています。

_duckdb.BinderException: Binder Error:
Referenced column "observation_time" not found in FROM clause!

Candidate bindings: "observed_at", "station_name", "rain_mm"

このログだけでも原因はかなり見えていますが、ここでは自分で直さず、Codex CLIへ調査を任せます。

Step 4:context-modeの状態を見ておく

前回の記事で導入済みでも、比較を始める前に動作状態を見ておきます。

2026年7月時点では、Codex CLIのプラグインマーケットプレイスから追加できます。

codex plugin marketplace add mksglu/context-mode

Codex CLIを起動します。

codex

Codex内で/pluginsを開き、追加したマーケットプレイスからcontext-modeをインストールします。

プラグインが提供するhooksを使う場合は、~/.codex/config.tomlの既存の[features]へ次を追加します。

[features]
plugin_hooks = true
hooks = true

すでに[features]がある場合は、同じ見出しを追加せず、既存の中へ2行を足します。

Codexを再起動し、hooksの承認画面が出た場合は内容を読んで許可します。

まずは次を入力します。

ctx doctor

続けて、統計情報も見ておきます。

ctx stats

ctx statsが応答すればMCPサーバーには接続できています。ただし、hooksまで有効になっているとは限りません。プラグインの有効状態、hooksplugin_hooksの設定、承認状況も合わせて見ておきます。

Step 5:同じ壊れた状態を2つ用意する

最初はブランチを切り替えながら比べようと思いましたが、一方の修正がもう一方へ混ざると分かりにくくなります。今回はGit worktreeを使い、作業フォルダー自体を分けました。

5.1 壊れた状態をコミットする

cd ~/projects/codex-context-mode-test

rm -f data/observations.duckdb

git init
git add .
git commit -m "Add broken FastAPI sample for context-mode test"

Gitのユーザー情報をまだ設定していない場合は、先に登録します。

git config --global user.name "Your Name"
git config --global user.email "you@example.com"

5.2 worktreeを作る

cd ~/projects/codex-context-mode-test

git worktree add ../codex-plain -b experiment/plain
git worktree add ../codex-context -b experiment/context

次の2つの作業フォルダーができます。

~/projects/codex-plain
~/projects/codex-context

5.3 それぞれに仮想環境を作る

cd ~/projects/codex-plain
pyenv local 3.13.14
python -m venv .venv
.venv/bin/python -m pip install -r requirements.txt
./scripts/capture_error.sh 8001

cd ~/projects/codex-context
pyenv local 3.13.14
python -m venv .venv
.venv/bin/python -m pip install -r requirements.txt
./scripts/capture_error.sh 8002

少し手間はかかりますが、これでソースコード、テスト、Uvicornログの開始条件をそろえられます。

Step 6:context-modeなしで調査する

先にcontext-modeを無効にした状態で試します。

Codex CLIで/pluginsを開き、Installedからcontext-modeを選んで無効にします。設定を変えた後は、念のためCodexを再起動します。

6.1 同じ依頼文を使う

比較するたびに文章を打ち直すと条件が変わるため、依頼文をシェル変数へ入れます。

read -r -d '' PROMPT <<'EOF' || true
FastAPIのGET /observationsで500エラーが発生しています。

logs/uvicorn.log、テスト、関係するソースコードを調査してください。
長いログをそのまま貼り付けず、原因と関係する部分を絞り込んでください。

原因を短く説明してから、必要なファイルだけを修正してください。
関係のないファイルは変更しないでください。
最後に.venv/bin/python -m pytest -qを実行し、結果を報告してください。
EOF

6.2 codex execで実行する

--jsonを付けると、コマンド実行、ファイル変更、MCP呼び出し、トークン使用量などがJSONLへ記録されます。

cd ~/projects/codex-plain
mkdir -p logs

codex exec \
  --json \
  --sandbox workspace-write \
  "$PROMPT" \
  > logs/codex_plain.jsonl \
  2> logs/codex_plain.stderr

今回は作業ディレクトリ内の修正だけで足りるため、workspace-writeを使っています。danger-full-accessは必要ありません。

6.3 差分とテスト結果を見る

git diff --stat
git diff
.venv/bin/python -m pytest -q

最後のターンに記録された使用量は、次のコマンドで取り出せます。

jq -s '
  [.[] | select(.type == "turn.completed") | .usage] | last
' logs/codex_plain.jsonl

実行された項目の種類も数えておきます。

jq -r '
  select(.type == "item.completed")
  | .item.type
' logs/codex_plain.jsonl \
  | sort \
  | uniq -c

この段階では、トークン数だけでなく、次の内容もメモしておきます。

  • 実行したコマンド数
  • 調べたファイル
  • 変更したファイル
  • pytestの結果
  • 原因の説明が分かりやすかったか

Step 7:context-modeありで調査する

次にcontext-modeを有効へ戻します。

/pluginsで有効にした後、Codexを再起動し、ctx doctorctx statsが動くことを確認してから実行します。

依頼文は、context-modeなしの場合と同じものを使います。

cd ~/projects/codex-context
mkdir -p logs

codex exec \
  --json \
  --sandbox workspace-write \
  "$PROMPT" \
  > logs/codex_context.jsonl \
  2> logs/codex_context.stderr

実行後は、同じコマンドで差分とテスト結果を見ます。

git diff --stat
git diff
.venv/bin/python -m pytest -q

Codex CLI側の使用量を取り出します。

jq -s '
  [.[] | select(.type == "turn.completed") | .usage] | last
' logs/codex_context.jsonl

MCPツールが呼ばれたかどうかも確認できます。

jq -r '
  select(.type == "item.completed")
  | .item.type
' logs/codex_context.jsonl \
  | sort \
  | uniq -c

最後にCodex CLIを対話モードで起動し、ctx statsの値も控えておきます。

ctx stats

Step 8:結果の見方を決める

実行結果は、利用するモデルや推論設定、Codex CLIとcontext-modeのバージョンによって変わります。そのため、固定の数値を例として載せるより、同じ条件で取得した結果を並べる方が分かりやすくなります。

今回記録する項目と取得元を整理すると、次のようになります。

確認項目 取得元 見るところ
Codexの入力・出力トークン turn.completedusage context-modeの有無で極端な差があるか
キャッシュ済み入力 turn.completedusage キャッシュの影響が混ざっていないか
コマンド実行数 JSONLのitem.type 調査が遠回りしていないか
MCPツール呼び出し JSONLのitem.type context-modeが実際に使われたか
修正ファイル git diff --stat 必要以上に変更していないか
修正内容 git diff 原因に沿った修正になっているか
テスト結果 pytest 修正後に正常終了したか
context-mode側の統計 ctx stats 大きな出力をどの程度絞ったか

ここで少し迷うのが、Codex CLIのトークン使用量とctx statsの値をどう比べるかです。

turn.completedusageはCodex側のトークン使用量です。一方、context-mode側は、元のツール出力をどの程度そのままコンテキストへ流さずに処理したかを示します。似た数字に見えても意味は同じではないため、単純に引き算して「何トークン減った」とは扱いません。

1回だけでは実行ごとのばらつきもあります。同じモデル、同じ推論設定、同じ依頼文で数回試し、極端な結果だけで判断しない方がよさそうです。

Step 9:今回想定している修正

今回の原因は、SQLで存在しない列名を参照していることです。

修正は1行で済みます。

 SELECT
     id,
     station_name,
-    observation_time,
+    observed_at,
     rain_mm
 FROM observations

修正後はpytestを実行します。

.venv/bin/python -m pytest -q

正常に直っていれば、次のようになります。

.                                                                        [100%]
1 passed

APIも起動してみます。

.venv/bin/uvicorn app.main:app --reload

別のターミナルからアクセスします。

curl http://127.0.0.1:8000/observations

JSONが返れば修正完了です。

比べるときに見たいこと

今回のエラーは小さいので、context-modeの有無で大きな差が出ない可能性があります。それでも、次の3点は見ておきたいところです。

ログを丸ごと読み込んでいないか

Uvicornやpytestのログが数十行なら、すべて読んでも大きな問題にはなりません。

ただし、Docker Compose、CI、PyInstallerなどのログは数千行になることがあります。context-modeを有効にしたとき、必要な箇所だけを取り出せているかは確認したいところです。

修正範囲が広がっていないか

SQLの列名を1つ直せば済む問題に対して、関係のないファイルまで変更されていないかをgit diffで見ます。

AIが修正したから正しいと考えるのではなく、差分を読む作業はこれまでどおり必要です。

テストまで終わっているか

原因を説明しただけで終わらず、修正後にpytestまで実行しているかも見ます。

今回はAGENTS.mdにテストコマンドを書いたため、毎回同じ条件を伝えやすくなりました。

この検証で見えてくること

今回の規模では、context-modeがなくてもエラーは直せます。むしろ、ログに候補のカラム名まで出ているため、通常のCodex CLIでもそれほど迷わないはずです。

それでも、小さな例から始めれば、比較の手順は整理しやすくなります。同じコミット、同じ依頼文、同じテストを使うところまでそろえておけば、次にもっと長いログを試すときにも流用できます。

context-modeの違いが分かりやすくなるのは、次のような場面だと思います。

  • Docker Composeの長い起動ログ
  • PyInstallerのビルドログ
  • 大量のpytest実行結果
  • 大きなリポジトリの横断調査
  • Git履歴や差分の集計
  • APIから取得した大きなJSONの解析

便利そうだから入れて終わりではなく、自分の作業でどの程度役立つかを記録しておく方が、あとで継続して使うか判断しやすくなります。

まとめ

今回は、WSL上にpyenvでPython 3.13環境を作り、FastAPIとDuckDBで小さなAPIを用意しました。SQLの列名をわざと間違え、500エラーとUvicornログを再現しています。

最初はブランチを切り替えて比較するつもりでしたが、修正内容が混ざるのを避けるため、Git worktreeで作業フォルダーを2つに分けました。少し手間は増えましたが、比較条件は分かりやすくなりました。

正直なところ、今回のエラーだけではcontext-modeの効果が大きく出ないかもしれません。ただ、同じ条件で調査を比べるための土台にはなります。

次はDocker Composeの起動ログやPyInstallerのビルドエラーなど、もう少し情報量の多い題材で試してみます。その方が、ログをすべて会話へ流さずに済む利点が見えやすそうです。

今回作成したフォルダー構成とファイルはZIPファイルでダウンロードできます。

参考リンク


0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?