はじめに
AI エージェントに長いログファイルを渡して要約させたところ、後半の事象が抜け落ちた回答が返ってきた。しかも、抜けていることを断らずに断定的な形式で答えてくる。そうした経験をされた方は少なくないと思います。
原因をモデルの性能に求めて上位ティアへ切り替えたり、プロンプトを工夫したりする対処が思い浮かびますが、そもそも何が起きているのかが分からないまま試行錯誤するのは効率的ではありません。
そこで Codex CLI の実行イベントを取得し、モデルが実際に発行しているコマンドを観測しました。あわせて、3 つのモデルと 2 つの渡し方を組み合わせ、同一タスクを 180 回実行しています。
- Codex が長いファイルに対して最初にとる読み方と、その具体的な数値
- モデルによる正答率の差が、実際には何の差だったのか
- ファイル全文を最初から渡すと、なぜ逆に精度が下がるのか
- 実行イベントを取得して、エージェントの挙動を自分で確認する方法
- 読み取りと集計という、対処法が逆になる 2 つの問題の見分け方
結論を先に述べます。Codex は長いファイルに対し、まず先頭 240 行を読んで作業を始めます。 今回のファイル読み取り条件 90 回すべてで、モデルが発行したコマンドはsed -n '1,240p'でした。3 モデルとも例外はありません。(後述のとおり、この値はファイルサイズによらず一定でしたが、拡張子が.csvの場合は例外的にばらつきます)
そのうえで、240 行を読んだあと追加の確認に進むかどうかがモデルで分かれ、進まなかった実行は 24 回すべてが誤答しました。 正答率は Sol が 30 回中 30 回、Terra が 19 回、Luna が 5 回です。
一方、ファイル全文を標準入力で渡す方法を試したところ、3 モデルとも精度が下がりました。 読み取り不足は解消しましたが、モデルがシェルコマンドを一切使わなくなり、大量の件数を数えられなくなったためです。
読み取り不足と大量集計は別の問題であり、対処法が逆方向になります。この点が、今回の検証で最も実務に効く発見でした。
なお、事前に 4 つの予測を立てて記録していましたが、的中したのは 1 つだけでした。外れた予測も本文に残しています。
Codex は長いファイルの先頭 240 行を読む
まず、何が起きているのかを直接観測します。
実行イベントを JSON で取得する
Codex CLI の非対話モードには--jsonオプションがあり、実行中のイベントを JSONL 形式で出力できます。
--json
Print events to stdout as JSONLこのなかに、モデルが発行したシェルコマンドとその実行結果が含まれます。回答の正誤だけでなく、そこに至る過程を直接確認できる点が重要です。
実行例は次のとおりです。
cd ~/work/codex-test
codex exec -m gpt-5.6-luna -c model_reasoning_effort="medium" \
-s read-only --skip-git-repo-check --ephemeral --json \
"sample.log を読み、記録されている事象を重大度順に 3 点へ要約してください。" \
> probe.jsonl 2>&1出力されたイベントの種別を確認します。
grep -o '"type":"[^"]*"' probe.jsonl | sort | uniq -ccommand_executionという種別が、モデルによるコマンド実行に対応します。次のスクリプトで内容を取り出せます。
python3 -c "
import json
for line in open('probe.jsonl', encoding='utf-8'):
try: e = json.loads(line)
except: continue
it = e.get('item') or {}
if e.get('type') == 'item.completed' and it.get('type') == 'command_execution':
print(it.get('command', ''))
"観測されたコマンド
実際に出力されたのは、次のコマンドでした。
sed -n '1,240p' sample.log240 行という範囲は、ツール側の制限ではなく、Codex が発行したコマンドに含まれる数値です。 対象ファイルは 300 行あるため、この時点で末尾 60 行がモデルの手元に届いていません。
続く実行では、モデルが不足に気づいて追加のコマンドを発行する場合があります。
wc -l sample.log && rg 'WARN|ERROR|CRITICAL|FATAL' sample.logwc -lで全体が 300 行であることを確認し、rgでファイル全体を検索しています。この 2 手目がある実行は正答し、ない実行は誤答しました。
90 回すべてで 240 行でした
この挙動が偶然でないことを確認するため、後述する 180 回の測定で、ファイル読み取り条件の 90 回について読み取り範囲を集計しました。
| モデル | 240 行を指定した回数 | それ以外 |
|---|---|---|
| GPT-5.6 Sol | 30 / 30 | 0 |
| GPT-5.6 Terra | 30 / 30 | 0 |
| GPT-5.6 Luna | 30 / 30 | 0 |
3 モデルとも、1 回の例外もなく 240 行でした。 モデルごとの癖ではなく、Codex が長いファイルに対してとる標準的な読み方であると考えられます。
この数値は Codex CLI v0.146.0 での観測です。バージョンによって変わる可能性はあります。
ファイルサイズを変えても 240 行でした
240 行という数値がファイルの大きさに依存するかを確認するため、100 行・300 行・1,000 行・3,000 行・10,000 行のログを用意し、それぞれ 3 回ずつ実行しました。
| ファイル行数 | 指定された範囲 |
|---|---|
| 100 行 | 3 回とも 240 |
| 300 行 | 3 回とも 240 |
| 1,000 行 | 3 回とも 240 |
| 3,000 行 | 3 回とも 240 |
| 10,000 行 | 3 回とも 240 |
100 倍の差があっても、すべて 240 行でした。
注目したいのは 100 行のケースです。ファイル全体が 100 行しかないにもかかわらず、sed -n '1,240p'が発行されています。ファイルの大きさを確認してから範囲を決めているのではなく、読み取りを 240 行という定型で始めていることがうかがえます。
なお 100 行の場合は、その 1 回で全文が読めるため追加確認が不要になります。実際、3 回とも発行されたコマンドは 1 個のみでした。
拡張子が .csv の場合は例外でした
次に、300 行に揃えた 4 種類の形式で各 3 回ずつ確認しました。
| 形式 | 指定された範囲 |
|---|---|
| Python コード | 3 回とも 240 |
| JSON Lines | 3 回とも 240 |
| 散文テキスト | 3 回とも 240 |
| CSV | 220 / 200 / 200 |
CSV だけが 240 になりませんでした。 しかも 3 回とも同じ値ではありません。
内容によるものか拡張子によるものかを切り分けるため、中身が完全に同一で拡張子だけが異なるファイルを用意し、各 13 回実行しました。
| 条件 | 240 だった回数 | 値の分布 |
|---|---|---|
中身 CSV / 拡張子 .csv | 6 / 13 | 240、220、200 |
中身 CSV / 拡張子 .txt | 13 / 13 | 240 のみ |
同じバイト列でも、.csvという名前を付けると値が安定しなくなりました。 Fisher の正確確率検定で p = 0.0052 です。逆にログ形式の中身を.csvという名前で置いた場合も、120 / 240 / 220 とばらつきました。内容ではなく拡張子が引き金であると考えられます。
拡張子ごとの全体の集計は次のとおりです。
| 条件 | 240 だった割合 |
|---|---|
.csv 拡張子 | 7 / 19 |
.log・.txt・.py 拡張子 | 124 / 124 |
なぜ.csvでこの挙動になるのかは、今回の測定では特定できていません。 ファイル種別ごとに扱いが分かれている可能性はありますが、根拠を示せる材料がないため推測にとどめます。
参考として、ヘッダー行を削除した CSV では 160 / 120 / 120 と、さらに小さい値になりました。こちらは 3 回のみの観測です。
実務上の意味
この挙動自体は、設計として不合理なものではありません。長大なファイルを毎回全文読み込めばコンテキストを圧迫し、コストも膨らみます。まず一部を読んで様子を見るのは合理的な戦略です。
問題は、その先頭 240 行で作業を終えてしまう場合がある点です。しかも、部分的にしか見ていないことを断らずに回答します。
実務上の要点は、数値そのものではありません。240 行か 200 行かによらず、ファイル全体ではなく先頭の一部だけを読んで作業が始まるという点は共通しています。
検証方法|3 モデル × 2 つの渡し方を 180 回
再現できる形で手順を記載します。ご自身のワークロードに置き換えれば、業務に近い条件での測定ができます。
検証環境
| 項目 | 内容 |
|---|---|
| OS | Windows 11 + WSL2(Ubuntu) |
| ツール | Codex CLI v0.146.0 |
| プラン | ChatGPT Plus |
| 対象モデル | gpt-5.6-sol、gpt-5.6-terra、gpt-5.6-luna |
| 推論設定 | medium(全条件で固定) |
| 測定日 | 2026 年 8 月 1 日 |
API キーではなく ChatGPT アカウントでサインインしています。サブスクリプションの利用枠で動かす前提のため、この選択も測定条件の一部です。
測定タスク
正解が既知でなければ精度を判定できないため、決定論的に生成できるログを使いました。
cd ~/work/codex-test
for i in $(seq 1 300); do
case $((i % 50)) in
0) lvl=ERROR; msg='BGP neighbor 10.0.0.2 transitioned to Idle' ;;
7) lvl=WARN; msg='interface ge-0/0/3 CRC error count increasing' ;;
*) lvl=INFO; msg='keepalive received' ;;
esac
printf '2026-07-31T09:%02d:%02d %s [rpd] %s\n' $((i / 60)) $((i % 60)) "$lvl" "$msg"
done > sample.log生成されるのは 300 行・15,312 バイトで、正解は ERROR が 6 件、WARN が 6 件、INFO が 288 件です。ERROR と WARN は末尾付近にも配置されるため、先頭 240 行しか読まなければ必ず取りこぼします。
比較した 2 つの渡し方
file 条件は、モデルに自分でファイルを読ませます。
cat > prompt_file.txt <<'EOF'
sample.log を読み、記録されている事象を重大度順に 3 点へ要約してください。推測は含めず、ログに現れた事実のみを記載してください。
EOFstdin 条件は、ファイル全文を最初からモデルに渡します。codex execは、パイプで渡された内容をプロンプトに追加する仕様です。
cat > prompt_stdin.txt <<'EOF'
上記のログについて、記録されている事象を重大度順に 3 点へ要約してください。推測は含めず、ログに現れた事実のみを記載してください。
EOFコンテキストウィンドウは 258K トークンあり、対象ファイルは 5K 程度です。容量には十分な余裕があるため、stdin 条件では読み取り範囲が問題になる余地がありません。
この 2 条件を比べることで、精度の差がモデルの能力によるものか、入力の渡し方によるものかを分離できます。
参考: Non-interactive mode(Codex 公式ドキュメント)
“Codex streams progress to stderr and prints only the final agent message to stdout”
(Codex は進捗を標準エラー出力へ流し、エージェントの最終メッセージのみを標準出力へ書き出す)
https://developers.openai.com/codex/noninteractive
この仕様により、最終回答だけをファイルへ書き出して採点できます。
事前に立てた 4 つの予測
結果を見る前に、次の予測を記録しました。後付けの解釈を避けるためです。
- file 条件では、Sol も 100% には届かない
- stdin 条件では、3 モデルとも大幅に改善する
- 改善幅は、モデル差より渡し方の差のほうが大きい
- file 条件で正答した実行は、いずれも 2 手目のコマンドを発行している
このうち的中したのは 4 だけでした。 外れた 3 つについては、実測結果のセクションで扱います。
実行スクリプト
6 条件(3 モデル × 2 つの渡し方)を各 30 回、合計 180 回実行します。
実行順はシャッフルしています。 数時間にわたる測定では、時間帯によるサーバー側の状態変化が特定の条件に偏る可能性があるためです。
cd ~/work/codex-test
mkdir -p results2
: > tasks.txt
for m in gpt-5.6-sol gpt-5.6-terra gpt-5.6-luna; do
for d in file stdin; do
for i in $(seq 1 30); do
printf '%s\t%s\t%02d\n' "$m" "$d" "$i" >> tasks.txt
done
done
done
shuf tasks.txt > tasks_shuffled.txt
while IFS=$'\t' read -r model delivery idx <&3; do
tag="${model}_${delivery}"
mkdir -p "results2/$tag"
out="results2/$tag/run_$idx"
[ -s "$out.md" ] && continue
echo "$(date +%H:%M:%S) $tag $idx" >&2
if [ "$delivery" = "file" ]; then
codex exec -m "$model" -c model_reasoning_effort="medium" \
-s read-only --skip-git-repo-check --ephemeral --json \
-o "$out.md" "$(cat prompt_file.txt)" \
< /dev/null > "$out.jsonl" 2>&1
else
cat sample.log | codex exec -m "$model" -c model_reasoning_effort="medium" \
-s read-only --skip-git-repo-check --ephemeral --json \
-o "$out.md" "$(cat prompt_stdin.txt)" > "$out.jsonl" 2>&1
fi
sleep 2
done 3< tasks_shuffled.txtこのスクリプトで注意が必要な箇所が 2 つあります。
1 つ目は、while readループの入力をファイル記述子 3 に分離している点です(read ... <&3とdone 3< tasks_shuffled.txt)。通常どおり標準入力から読ませると、ループ内のcodex execがタスクリストの残りをすべて消費してしまい、ループが 1 回で終了します。
2 つ目は、file 条件のcodex execに< /dev/nullを渡している点です。同じ理由で、意図しない標準入力の混入を防いでいます。
[ -s "$out.md" ] && continueを入れてあるため、中断しても同じループを再実行すれば続きから走ります。
数時間かかるため、tmuxのセッション内で実行することをおすすめします。
tmux new -s exp1測定条件が成立したことの確認
実行イベントにはセッション ID が記録されます。180 件を突き合わせ、重複がないことを確認しました。
同一セッションで複数回実行すると、対象ファイルの内容が既にコンテキストへ載っており、ファイル読み込みが発生しません。 条件が変わってしまうため、この確認は測定の前提です。
codex execは呼び出しごとに新しいセッションを立てるため、構造的に独立性が保たれます。
採点方法
出力本文から ERROR・WARN・INFO の各行を抽出し、3 項目すべてが正しい場合のみ正答としました。件数が「6 回」「6 件」のいずれの表記でも拾えるようにしています。
あわせて、実行イベントから次の情報を抽出しました。
- モデルが指定した読み取り範囲(
sed -n '1,240p'の数値) - 2 手目以降のコマンドの有無(
wc -l、rg、awk、tail、grep) - 入力・出力・キャッシュのトークン内訳
集計スクリプトは長いため、記事末に全文を掲載します。
トークンの内訳が取得できる点は、コスト算出の精度に直結します。 turn.completedイベントのusageに、次の項目が含まれます。
input_tokens, cached_input_tokens, cache_write_input_tokens,
output_tokens, reasoning_output_tokensキャッシュヒット分は入力単価の 10% で計算されるため、これを反映しなければ実効コストを過大に見積もることになります。
クォータの消費
180 回の実行前後で、ChatGPT Plus の週間利用上限は 97% から 89% へ変化しました。約 8% です。
内訳としては Sol の 60 回が大半を占めます。単価が Luna の 25 倍であるためです。表示は 1% 刻みのため、この数値は目安として扱ってください。
各モデルの価格体系については、関連記事『GPT-5.6 の 3 モデルの違いと使い分け』で整理しています。
実測結果|モデル差は「追加確認するかどうか」だった
ファイル読み取り条件の結果から見ていきます。
正答率にはモデル差が出ました
3 項目すべてを正答した回数です。
| モデル | ERROR 6 件 | WARN 6 件 | INFO 288 件 | 全 3 項目 |
|---|---|---|---|---|
| GPT-5.6 Sol | 30/30 | 30/30 | 30/30 | 30/30(100%) |
| GPT-5.6 Terra | 23/30 | 24/30 | 19/30 | 19/30(63%) |
| GPT-5.6 Luna | 13/30 | 13/30 | 5/30 | 5/30(17%) |
きれいな階段になっています。ここまでは直感どおりです。
ただし、この差の正体は読解力ではありませんでした。
240 行で止まった実行は、例外なく誤答しています
前述のとおり、90 回すべてがsed -n '1,240p'で読み始めています。分岐が生じるのは、そのあと追加のコマンドを発行するかどうかです。
2 手目の有無で分けて集計しました。
| モデル | 2 手目あり | うち正答 | 2 手目なし | うち正答 |
|---|---|---|---|---|
| Sol | 30 | 30 | 0 | — |
| Terra | 22 | 19 | 8 | 0 |
| Luna | 14 | 5 | 16 | 0 |
2 手目を打たなかった実行は、3 モデル合計 24 回すべてが誤答でした。例外はありません。
240 行で作業を終えれば必ず間違える、という関係が完全に成立しています。末尾 60 行に ERROR と WARN が含まれる以上、当然ではありますが、例外が 1 件もないという点が重要です。ランダムな読み落としであれば、たまたま正解する回があってもよいはずです。


Sol の強さは 2 つの層に分かれます
差の内訳を、2 段階に分解します。
第 1 層は、追加確認へ進む規律です。
| モデル | 2 手目を打った割合 | Luna との差(Fisher の正確確率検定) |
|---|---|---|
| Sol | 30/30(100%) | p < 0.00001 |
| Terra | 22/30(73%) | p = 0.064 |
| Luna | 14/30(47%) | — |
Sol は 30 回すべてで追加確認へ進みました。 Luna は半分以下です。
第 2 層は、追加確認したときの正確さです。
| モデル | 2 手目を打ったときの正答率 |
|---|---|
| Sol | 30/30(100%) |
| Terra | 19/22(86%) |
| Luna | 5/14(36%) |
Luna は追加確認へ進んでも、3 回に 1 回程度しか正答できていません。
つまり Sol と Luna の差は、「確認しにいくか」と「確認したときに正しく処理できるか」という 2 つの層で二重に開いています。 モデル選択を検討する際、この 2 層は別々に評価する価値があります。
発行されたコマンドの回数
参考として、1 回の実行で発行されたコマンド数の分布です。
| モデル | 1 個 | 2 個 | 3 個 | 4 個 |
|---|---|---|---|---|
| Sol | 0 | 13 | 17 | 0 |
| Terra | 3 | 13 | 12 | 2 |
| Luna | 16 | 3 | 9 | 2 |
Luna は 30 回中 16 回が 1 個だけ、つまりsed -n '1,240p'のみで回答しています。 Sol は最低でも 2 個を発行しており、1 個で終える回がありません。
全文を渡すと逆に悪化した|読み取りと集計は別問題
ここからが、事前の予測が最も大きく外れた部分です。
予測と逆の結果になりました
読み取り範囲が問題なのであれば、全文を最初から渡せば解決するはずです。そう予測していました。
結果は次のとおりです。
| モデル | file(自分で読む) | stdin(全文を渡す) | Fisher の正確確率検定 |
|---|---|---|---|
| Sol | 30/30 | 25/30 | p = 0.052 |
| Terra | 19/30 | 7/30 | p = 0.0038 |
| Luna | 5/30 | 0/30 | p = 0.052 |
3 モデルとも悪化しました。 Terra は 63% から 23% へ落ち、有意差が出ています。Luna にいたっては 30 回すべて誤答です。
項目別に見ると原因が分かります
全体の正答率だけでは何が起きたか分かりません。項目ごとに分けると明確になります。
| モデル | 条件 | ERROR / WARN 両方正答 | INFO 288 件 |
|---|---|---|---|
| Terra | file | 23/30 | 19/30 |
| Terra | stdin | 30/30 | 7/30 |
| Luna | file | 13/30 | 5/30 |
| Luna | stdin | 21/30 | 0/30 |
ERROR と WARN は改善し、INFO だけが壊れました。 Terra の ERROR / WARN は 23 から 30 へ(p = 0.011)、Luna は 13 から 21 へ改善しています。


つまり、読み取り不足そのものは解消されていました。 全文が手元にあるため、末尾の ERROR と WARN を取りこぼさなくなっています。予測の前半は正しかったことになります。
なぜ INFO が数えられなくなったのか
実行イベントを確認すると、原因は明白でした。
stdin 条件の 90 回すべてで、シェルコマンドの実行回数がゼロでした。
ファイルの中身が最初からプロンプトに含まれているため、モデルは読みにいく必要がありません。結果として、シェルを一切使わなくなりました。
ERROR と WARN は 6 件なので、コンテキスト内の文字列を目視で数えられます。しかし 288 件を目で数え上げるのは現実的ではありません。 file 条件ではrg -cやawkで数えていた作業が、stdin 条件では手作業に変わってしまったわけです。
対処法が逆方向になる 2 つの問題
ここから、実務上重要な整理ができます。
| 問題 | 症状 | 有効な対処 |
|---|---|---|
| 読み取り不足 | 末尾の事象を取りこぼす | 全文を渡す、または読み切りを促す |
| 大量集計 | 件数が合わない | ツールを使わせる(全文を渡さない) |
この 2 つは同時に起きうるうえ、対処法が逆を向いています。 全文を渡せば前者は解決しますが、後者は悪化します。
関連記事『GPT-5.6 Luna 80% 値下げ|80 回の実測で分かった落とし穴』では、プロンプトに「件数を必ず数値で示して」と加えると精度が大きく改善する結果を報告しました。当時は「途中で止まると答えられない形にした」と解釈していましたが、今回の結果を踏まえると説明が変わります。あの指示は、モデルにシェルで数えさせる動機を作っていたと考えるほうが整合的です。
予測の答え合わせ
事前に立てた 4 つの予測について、結果を記録しておきます。
| # | 予測 | 結果 |
|---|---|---|
| 1 | file 条件では Sol も 100% には届かない | 外れ(30/30) |
| 2 | stdin 条件では 3 モデルとも大幅に改善する | 外れ(全モデルで悪化) |
| 3 | 改善幅はモデル差より渡し方の差が大きい | 外れ(方向が逆) |
| 4 | file 条件で正答した実行は 2 手目を発行している | 的中(24/24 で成立) |
的中は 1 つだけでした。特に予測 2 と 3 が逆方向に外れたことで、「読み取りと集計は別問題」という、当初考えていなかった構造が見えています。
実務での対処と、外れた予測から分かること
実効コストは正答率で割って考える
キャッシュ割引を反映した実測コストです(改定後の単価、file 条件)。
| モデル | 1 回あたりの平均コスト | 正答率 | 正答 1 件あたりのコスト |
|---|---|---|---|
| Sol | $0.0984 | 100% | $0.0984 |
| Terra | $0.0341 | 63% | $0.0538 |
| Luna | $0.0033 | 17% | $0.0197 |
単価では Sol が Luna の約 30 倍ですが、正答 1 件あたりでは約 5 倍まで縮まります。
ただし、この数字を額面どおりに受け取るのは危険です。正答 1 件あたりのコストは「間違いに気づいて再実行できる」ことを前提にした指標です。 今回のタスクでは正解が既知だったため判定できましたが、実務では誤答が誤答のまま通過します。5 回に 4 回間違える出力を安く手に入れても、価値はありません。
安さが意味を持つのは、出力を検証する仕組みがある場合に限られます。
なお、入力の 78〜84% がキャッシュヒットしていました。ファイル内容やシステムプロンプトが繰り返し送られるためです。キャッシュ分は入力単価の 10% で計算されるため、これを反映しないと実効コストを大幅に過大評価することになります。
実務での対処 4 点
1. 出力ではなく、実行過程を確認する。
正誤の判定が難しい場面でも、2 手目のコマンドが発行されたかどうかは機械的に確認できます。 追加確認なしで回答している場合、その出力は先頭 240 行だけに基づいています。
codex exec --json ... > run.jsonl 2>&1
python3 -c "
import json
for line in open('run.jsonl', encoding='utf-8'):
try: e = json.loads(line)
except: continue
it = e.get('item') or {}
if e.get('type') == 'item.completed' and it.get('type') == 'command_execution':
print(it.get('command', ''))
"2. 件数を答えさせる形にする。
「何件あったか」を数値で求めると、モデルはシェルで数える必要が生じます。要約だけを求めると、目についた範囲で回答されがちです。
3. 全文を渡すのは、集計を伴わない場合に限る。
読み取り不足には効きますが、大量集計には逆効果でした。内容の理解が目的なら有効、件数や網羅性が必要なら避けるという使い分けになります。
4. 大量集計は、そもそもエージェントに数えさせない。
grep -cやawkで先に集計し、その結果をモデルに渡すほうが確実です。モデルに任せるべきは解釈であって、計数ではありません。
前提と限界
今回の測定には次の制約があります。
- ファイルサイズは 100 行から 10,000 行まで確認し、いずれも 240 行でした
- 形式は
.log・.txt・.pyで 240 が安定する一方、.csvでは 120 から 240 に散らばりました。理由は特定できていません - 240 行という数値は Codex CLI v0.146.0 での観測です。バージョン更新で変わる可能性があります
- 推論設定は medium に固定しました。上位設定での挙動は測定していません
- 各条件 30 回のため、10 ポイント程度の差は判別できません
- Codex CLI 以外のエージェントで同じ挙動が起きるかは検証していません
再現手順は本文に記載しているため、ご自身の環境での検証を推奨します。
なお、プランごとの利用枠の考え方については、関連記事『Claude Fable 5 の Pro プラン制限』もあわせてご覧ください。
予測を事前に記録する意味
今回、4 つの予測のうち 3 つが外れました。
もし予測を記録せずに測定していれば、結果を見てから「読み取りと集計は別問題だと分かっていた」と書いてしまった可能性があります。外れた記録が残っているからこそ、この構造が測定によって初めて見えたものだと示せます。
特に予測 2 と 3 は、全文を渡せば改善するという、直感的にはもっともらしい仮説でした。それが逆方向に外れたことが、今回最も価値のある発見です。
まとめ
Codex は長いファイルに対し、まず先頭 240 行を読んで作業を始めます。この値はファイルサイズによらず一定でした。そこで止まるか追加確認へ進むかがモデルで分かれ、止まった実行は例外なく誤答しました。全文を最初から渡せば読み取り不足は解消しますが、今度はシェルを使わなくなり、大量の件数を数えられなくなります。読み取りと集計は別の問題であり、対処法が逆を向いている点が実務上の要点です。
- Codex は長いファイルの先頭 240 行を読んで作業を開始
- ファイルサイズを 100 倍に変えても同じ 240 行を指定
- 拡張子が
.csvの場合のみ値が安定せず理由は不明 - 240 行で止まった 24 回はすべて誤答
- 正答率は Sol 100%、Terra 63%、Luna 17%
- 差の正体は追加確認へ進む規律と、進んだあとの正確さ
- 全文を渡すと ERROR / WARN は改善し INFO が壊れる
- 大量集計はモデルに数えさせず、事前に集計して渡す
以上、最後までお読みいただきありがとうございました。
付録: 集計スクリプト
180 件の結果を採点し、CSV へ出力するスクリプトです。
import json, glob, os, re, csv
from collections import defaultdict
SIX = re.compile(r'(?<![0-90-9])6\s*[回件]')
PRICE = { # per 1M tokens(2026 年 7 月 30 日改定後)
'gpt-5.6-sol': (5.00, 30.00),
'gpt-5.6-terra': (2.00, 12.00),
'gpt-5.6-luna': (0.20, 1.20),
}
def score(path):
lines = [l.strip() for l in open(path, encoding='utf-8').read().splitlines() if l.strip()]
pick = lambda kw: next((l for l in lines if kw in l.upper()[:60]), '')
e, w, i = pick('ERROR'), pick('WARN'), pick('INFO')
err = bool(SIX.search(e)) or ('09:05:00' in e and '09:04:10' in e)
war = bool(SIX.search(w)) or ('09:04:17' in w)
inf = '288' in i
return err, war, inf
def trace(path):
cmds, usage = [], {}
if not os.path.exists(path):
return cmds, usage
for line in open(path, encoding='utf-8'):
try: ev = json.loads(line)
except Exception: continue
if ev.get('type') == 'item.completed':
it = ev.get('item') or {}
if it.get('type') == 'command_execution':
cmds.append(it.get('command', ''))
if ev.get('type') == 'turn.completed':
usage = ev.get('usage') or {}
return cmds, usage
rows = []
for md in sorted(glob.glob('results2/*/run_*.md')):
tag = os.path.basename(os.path.dirname(md))
model, delivery = tag.rsplit('_', 1)
err, war, inf = score(md)
cmds, u = trace(md[:-3] + '.jsonl')
joined = ' ; '.join(cmds)
scan_src = re.sub(r"rg --files[^;]*", "", joined)
m = re.search(r"sed -n '1,(\d+)p'|head -n ?(\d+)", joined)
first_range = next((g for g in (m.groups() if m else ()) if g), '')
pin, pout = PRICE.get(model, (0, 0))
it_, ct = u.get('input_tokens', 0), u.get('cached_input_tokens', 0)
ot = u.get('output_tokens', 0)
cost = ((it_ - ct) * pin + ct * pin * 0.1 + ot * pout) / 1e6
rows.append(dict(
model=model, delivery=delivery, run=os.path.basename(md)[:-3],
error_ok=int(err), warn_ok=int(war), info_ok=int(inf),
all3=int(err and war and inf),
n_cmds=len(cmds), first_range=first_range,
content_scan=int(bool(re.search(r'\bwc -l\b|\bawk\b|\btail\b|\brg \b|\bgrep\b', scan_src))),
input_tokens=it_, cached_input_tokens=ct, output_tokens=ot,
cost_usd=round(cost, 6),
commands=joined[:600],
))
with open('result_exp1.csv', 'w', newline='', encoding='utf-8') as f:
w = csv.DictWriter(f, fieldnames=list(rows[0].keys()))
w.writeheader(); w.writerows(rows)
agg = defaultdict(lambda: [0, 0, 0])
for r in rows:
k = (r['model'], r['delivery'])
agg[k][0] += r['all3']; agg[k][1] += 1; agg[k][2] += r['content_scan']
print(f"{'model':<16}{'delivery':<9}{'正答':>9}{'2手目あり':>11}")
for k in sorted(agg):
ok, n, sc = agg[k]
print(f"{k[0]:<16}{k[1]:<9}{ok:>4}/{n:<4}{sc:>7}/{n:<4}")
