前回のあらすじ
前回の記事で、Claude Code(Opus 4.6)に4言語×5回 = 20回コードを書かせた。
結果:ほぼ全言語100%。 退屈なくらい動いた。
で、当然こう思うわけ:
「じゃあ安い方(Sonnet)でも同じ結果出るの?」
Opus 4.6 はClaude最上位モデル。Sonnet 4.6 はその下位互換。速くて安い。
同じプロンプト、同じテスト、同じ言語。モデルだけ変える。
やってみた。
検証条件:前回と完全に同一
| 条件 | 内容 |
|---|---|
| 言語 | TypeScript / Python / Rust / Ruby(4言語) |
| 回数 | 各5回(計20回 × 2モデル = 40回) |
| プロンプト | 同一の PROMPT.md
|
| テスト | 同一の test_api.sh(7項目) |
| 差分 | モデルだけ(Opus 4.6 vs Sonnet 4.6) |
変数はモデルだけ。これでモデル差が測れる。
結果:差、ほぼなし。
総合スコア
133 対 132。差はたった1テスト。
| モデル | 合計 | 合格率 |
|---|---|---|
| Opus 4.6 | 133/140 | 95.0% |
| Sonnet 4.6 | 132/140 | 94.3% |
正直、誤差じゃない?
言語別の内訳
| 言語 | Opus 4.6 | Sonnet 4.6 | 差 |
|---|---|---|---|
| TypeScript | 35/35(100%) | 35/35(100%) | ±0 |
| Python | 35/35(100%) | 32/35(91.4%) | -3 |
| Rust | 35/35(100%) | 35/35(100%) | ±0 |
| Ruby | 28/35(80%) | 30/35(85.7%) | +2 |
ちょっと待って。Ruby は Sonnet のほうがスコア高い?
そう、ここが面白いところ。
バグの「質」が全然違う
総合スコアは僅差。でも 失敗の仕方 が全然違った。
Opus 4.6 の失敗パターン
Opus は 1回だけ派手にコケた。Ruby Run-1 で rackup gem を Gemfile に入れ忘れてビルド失敗。残り19回は完璧。
失敗パターン:0か100か。 動くか動かないか。
Sonnet 4.6 の失敗パターン
Sonnet は 毎回ちょっとずつコケた。致命的じゃないけど、微妙なバグが散在。
失敗パターン:6/7 が通るけど1つだけ落ちる。 一見動くけど、実は問題がある。
Opus 4.6 Sonnet 4.6
失敗の仕方 ████████░░░░ ░░░░░░░░░░░░
1回 大失敗 8回 小失敗
ビルド失敗 1回 0回
テスト失敗 0テスト 8テスト
Opus は「動くか動かないか」。Sonnet は「動くけど微妙に壊れてる」。
実務だとどっちが怖い? 正直 Sonnet のほうが怖い。ビルド失敗は即気づくけど、6/7 通るバグは見逃す。
Sonnet のバグを深掘りする
バグ①:Flask の Unicode エスケープ(Python run-4)
Opus は5回とも FastAPI を選んだ。Sonnet は4回 FastAPI、1回だけ Flask を選んだ。
その1回が事故った。
# Flask のデフォルト動作
>>> from flask import jsonify
>>> jsonify({"title": "テスト買い物"})
# → {"title": "\u30c6\u30b9\u30c8\u8cb7\u3044\u7269"}
Flask は JSON_AS_ASCII=True がデフォルト。日本語が \uXXXX にエスケープされる。
テストスクリプトは grep "テスト買い物" で検証してるから、エスケープされた文字列はマッチしない。
# テスト結果
PASS: GET /todos returns 200 # ← ASCII、OK
PASS: GET /todos returns array # ← "[" を検索、OK
PASS: POST /todos returns 201 # ← ステータスコード、OK
FAIL: POST /todos returns todo with title # ← "テスト買い物" がない!
FAIL: GET /todos after POST has item # ← 同上
FAIL: PUT /todos/1 returns updated todo # ← "更新済み" がない!
PASS: DELETE /todos/1 returns deleted # ← "deleted" は ASCII、OK
HTTP ステータスは全部正しい。JSON の中身も正しい。でも日本語の表現方法が違う。
Flask の問題であって、Sonnet のコーディング能力の問題ではない。ただし「Flask を選んだ」のは Sonnet の判断。
教訓:AI のフレームワーク選択は安定してるようで、たまにブレる。そのブレが事故を生む。
バグ②:sqlite3 gem 2.0 の API 変更(Ruby 全5 run)
こっちのほうが深刻。全5回で同じバグ。
# Sonnet が書いたコード(全5 run 共通パターン)
DB.execute(
'UPDATE todos SET title = ?, completed = ? WHERE id = ?',
title, completed, id # ← 引数を個別に渡してる
)
ArgumentError: wrong number of arguments (given 4, expected 1..2)
sqlite3 gem 2.0 で API が変わった。
# sqlite3 gem 1.x(旧API)── 動く
DB.execute(sql, param1, param2, param3)
# sqlite3 gem 2.0(新API)── これが正解
DB.execute(sql, [param1, param2, param3])
引数を配列で渡す必要がある。Sonnet はこの Breaking Change を知らなかった。
面白いのは、INSERT と SELECT は動いてる こと。パラメータが1つなら execute(sql, param) で引数2つだから、新APIでもそのまま動く。3つ以上になった瞬間に壊れる。
だから GET、POST、DELETE は通って、PUT だけ落ちる。6/7 通る「微妙なバグ」の正体がこれ。
テスト結果(Ruby 全5 run 共通)
PASS: GET /todos returns 200 ← SELECT(1param)OK
PASS: GET /todos returns array ← SELECT OK
PASS: POST /todos returns 201 ← INSERT(1param)OK
PASS: POST /todos returns todo with title ← INSERT OK
PASS: GET /todos after POST has item ← SELECT OK
FAIL: PUT /todos/1 returns updated todo ← UPDATE(3params)エラー!
PASS: DELETE /todos/1 returns deleted ← DELETE(1param)OK
Sonnet は sqlite3 gem 2.0 の Breaking Change を知らなかった。Opus は知っていた(配列で渡していた)。
ここが 最上位モデルと高速モデルの差 として表れた。学習データの鮮度か、推論能力の差か。どちらにせよ、Ruby の最新 gem に対する知識で Opus に軍配。
Opus が Ruby で負けてるのにスコアが高い?
混乱するよね。整理する。
| Opus | Sonnet | |
|---|---|---|
| Ruby ビルド失敗 | 1回(Run-1、0/7) | 0回 |
| Ruby テスト失敗 | 0テスト(他4 run は 28/28) | 5テスト(5 run × 1テスト) |
| Ruby 合計 | 28/35 | 30/35 |
Ruby だけ見れば Sonnet の勝ち。 全部ビルドは通った。Opus は1回ビルドで死んだ。
でもトータルで見ると:
| Opus | Sonnet | |
|---|---|---|
| TypeScript | 35 | 35 |
| Python | 35 | 32 |
| Rust | 35 | 35 |
| Ruby | 28 | 30 |
| 合計 | 133 | 132 |
Python の3テスト差で Opus が逆転してる。
フレームワーク選択の比較
前回の記事で「AI が選ぶフレームワーク、面白いくらい偏ってる」と書いた。Sonnet でも検証。
| 言語 | Opus 4.6 | Sonnet 4.6 |
|---|---|---|
| TypeScript | Express 5/5 | Express 4/5, Fastify 1/5 |
| Python | FastAPI 5/5 | FastAPI 4/5, Flask 1/5 |
| Rust | actix-web 5/5 | actix-web 5/5 |
| Ruby | Sinatra 5/5 | Sinatra 5/5 |
Opus は「鉄板フレームワーク」から絶対ブレない。 Sonnet はたまに別の選択をする。
その「たまに」が事故の原因になったのが今回の Python Flask 事件。
安定性を取るなら Opus。多様性がほしいなら Sonnet。
……いや、多様性いらないか。テスト落ちたし。
じゃあ Sonnet で十分?
結論から言うと、タスクによる。
| ユースケース | おすすめ | 理由 |
|---|---|---|
| CRUD / プロトタイプ | Sonnet | 94% の成功率で十分。コスト1/5 |
| 本番コード生成 | Opus | フレームワーク選択が安定。最新API対応力が高い |
| 多言語ベンチマーク | Opus | Ruby の微妙なバグを避けたい |
| TypeScript / Rust | どちらでも | 両モデルとも100%。差なし |
| Python | Opus | Flask 事故のリスクを避ける |
| Ruby | 引き分け | Opus はビルド失敗、Sonnet はAPI不整合 |
CRUDなら Sonnet で十分
今回のベンチマーク(Todo API)レベルなら、Sonnet 4.6 で94.3%。
20回中19回は完動するコードが出てくる。残り1回も「PUT だけ落ちる」レベル。致命的じゃない。
でも「微妙なバグ」は怖い
6/7 テストが通って1つだけ落ちるバグ。これ、テストがなかったら 気づかない。
前回の記事でも書いたけど、テスト駆動が AI 時代にこそ効く。今回の Sonnet のバグは全部テストで検出できた。テストなかったら「動いてるっぽい」で本番に行くところだった。
まとめ
| 知見 | 一言 |
|---|---|
| たった1テスト差 | Opus 133/140 vs Sonnet 132/140 |
| TS / Rust は差なし | どちらも100%。安い方で十分 |
| バグの質が違う | Opus = 大失敗、Sonnet = 微妙なバグ |
| Flask 事故 | Sonnet がフレームワーク選択でブレた |
| sqlite3 gem 2.0 | Sonnet は最新API変更を知らなかった |
| フレームワーク安定性 | Opus は鉄板から絶対ブレない |
| テストが命綱 | Sonnet のバグは全部テストで検出 |
「高いモデルと安いモデル、どっち使うべき?」
「CRUDなら安い方でいい。ただしテストは絶対書け。」
これが40回ベンチマーク回した人間の結論。
前回 + 今回で計40回 AI にコード書かせて分かったのは、モデル選択より大事なのはテストの存在 だってこと。
Opus だろうが Sonnet だろうが、テストがなければバグは見逃す。テストがあれば、どっちのモデルでもバグを検出できる。
いいねとストックしてくれると次の検証のモチベになります。
検証コード:GitHub リポジトリ
前回の記事:TypeScript vs Python vs Rust vs Ruby ── AIに20回コード書かせて分かった「本当の差」