Codex CLI + context-modeの効果を実プロジェクトで検証する:ログ解析からコード修正まで
前回の記事では、WSL上にCodex CLIとcontext-modeを導入し、MCPとして使えるところまで試しました。
導入自体はできましたが、実際の開発でどのくらい役に立つのかは、まだよく分かっていません。
そこで今回は、FastAPIとDuckDBで簡単なAPIを作り、わざと500エラーを発生させます。そのうえで、通常のCodex CLIだけで調べた場合と、context-modeを有効にした場合で、ログの扱い方や修正までの流れに違いが出るかを見ていきます。
今回用意するエラーは、SQLのカラム名を1か所間違えるだけの単純なものです。正直なところ、ログを数行読めば人でも原因に気付けます。最初から複雑な題材にすると何を比べているのか分かりにくくなるため、まずは小さな例から始めることにしました。
今回やること
見るのは、単純なトークン数だけではありません。
- どのファイルを調べたか
- 長いログをそのまま読み込んでいないか
- 修正したファイルが必要以上に増えていないか
- テストまで実行したか
- 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
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
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パッケージを読み込めるようにします。
[tool.pytest.ini_options]
pythonpath = ["."]
testpaths = ["tests"]
2.4 DuckDBの初期化処理
"""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 レスポンスモデル
"""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ですが、この時点では直しません。
"""雨量観測値を返す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のエントリーポイント
"""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 テストを作る
"""雨量観測値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の実行を毎回手作業で行うと条件がずれやすいため、簡単なスクリプトにしました。
#!/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を置きます。
# 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
.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まで有効になっているとは限りません。プラグインの有効状態、hooksとplugin_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 doctorとctx 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.completedのusage
|
context-modeの有無で極端な差があるか |
| キャッシュ済み入力 |
turn.completedのusage
|
キャッシュの影響が混ざっていないか |
| コマンド実行数 | JSONLのitem.type
|
調査が遠回りしていないか |
| MCPツール呼び出し | JSONLのitem.type
|
context-modeが実際に使われたか |
| 修正ファイル | git diff --stat |
必要以上に変更していないか |
| 修正内容 | git diff |
原因に沿った修正になっているか |
| テスト結果 | pytest | 修正後に正常終了したか |
| context-mode側の統計 | ctx stats |
大きな出力をどの程度絞ったか |
ここで少し迷うのが、Codex CLIのトークン使用量とctx statsの値をどう比べるかです。
turn.completedのusageは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ファイルでダウンロードできます。
参考リンク
- 前回の記事:WSLでCodex CLI + context-modeを使う
- pyenv
- Python 3.13.14
- Codex CLI
- Codex CLI Plugins
- Codex CLIのAGENTS.md
- Codex CLIのNon-interactive mode
- context-mode
- DuckDB Python API
- FastAPI

