テクノロジー

同じ仕様書で3回作らせた――Opus 5とSonnet 5の差より、対話か自律かの差のほうが大きかった

議論参加:ナオ (生成AIの実力を取材する技術記者) / カエデ (テスト設計と品質保証のエンジニア) / ハルカ (ゲームAIと評価関数を扱う機械学習エンジニア) / ユウタ (AIでツールを量産する個人開発者) / ミサ (ユーザー体験と信頼性の研究者) / サトシ (雀荘歴の長いアマチュア雀士・ドメイン検証担当) / テツオ (事実確認を担う編集者)

前回、編集部はClaude Opus 5に日本のリーチ麻雀のスタンドアロンアプリを作らせ、全工程を記録した(テストは6回落ちて、6回とも実装は正しかった)。

今回は、**まったく同じ仕様書で、あと2回作らせた。**Sonnet 5に1回、そしてOpus 5にもう1回。ただし2回目のOpus 5は、前回のような対話ではなく、質問を一切させない自律エージェントとして走らせている。

この設計にしたのは、モデルの差を見ようとすると実行モードの差が混ざるからだ。3本揃えると、比較が2つの軸に分解できる。

対話モード自律エージェント
Opus 51本目(前回の記事)3本目(今回追加)
Sonnet 52本目

横に見れば同じ実行モードでのモデル差、縦に見れば同じモデルでの実行モード差になる。

そして結果は、前回の記事の主要な結論をひとつ覆した。

結論:3本すべてが同じ罠に落ちた。テスト手牌が意図せず役満と複合する、CPUのパラメータが評価関数に効いていない、難易度と実際の強さが一致しない——この3つは3本とも再現した。一方、前回「モデル差」と結論づけたウマ・返し点の実装漏れは、同じOpus 5でも自律モードでは起きていない。あれはモデル差ではなくハーネス差だった。そして完成度は、人間が介在した対話モードより自律モードのほうが高かった。

検証条件

技術記者のナオさんが設計を説明した。

「渡したプロンプトは202行、MD5まで一致する同一ファイルです。Phase 0からPhase 9まで、四人東南戦のフル実装、CPU 5体、初心者向けアドバイス、LAN対戦、牌のSVG自作、記録の運用ルールまで含みます」(ナオ)

「2本目と3本目に渡したエージェント指示も、作業ディレクトリのパス以外は一字一句同じにしました。『順番に、最後まで、止まらずに』『途中で質問をしない』『予算が厳しければ中核を優先して削ったものを明記せよ』まで揃えています」(ナオ)

汚染の確認もした。3本目のコードとDEVLOGに、先行2本のディレクトリを参照した形跡はない。アーキテクチャ、ファイル構成、牌の意匠、詰まった箇所の順序も独立している。

「ただし但し書きは残ります。この検証を実行して記事を書いているのが、比較対象であるOpus 5自身です。今回もOpus 5が負けている材料を優先して出します」(テツオ)

実測比較

3本のリポジトリで実際に測った数字。

Opus 5 / 対話Opus 5 / 自律Sonnet 5 / 自律
到達フェーズPhase 9Phase 9Phase 9
ソース行数7,2337,8208,607
テスト件数156117(+ Rust 7)81
テストファイル数91113
コミット数111314
失敗状態のコミット3本3本0本
タイトルの項目数256
配布ビルド未検証.dmg 成功.dmg 成功
ウマ・返し点の清算未実装(気づかず)実装済み(一発)未実装 → Phase 9 で自己発見・修正
tsc --noEmitエラー0エラー0エラー0

3本とも完走し、3本とも動くアプリになった。そもそも完走できるかどうかでは差がつかなかった。

3本すべてが落ちた罠

罠1:テスト手牌が、意図せず役満と複合する

前回の記事で、Opus 5(対話)が大四喜のテストを門前で書いたために四暗刻単騎が同時成立し、expected 4 to be 2で落ちたことを書いた。

Sonnet 5は大三元のテストで同じことをした。DEVLOGにこうある。

「テストの手牌の4組目を刻子にしていたため、大三元に加えて四暗刻単騎も同時に成立してしまい、コードは正しく基本点を8000×(1+2)=24000と計算していた」

そしてOpus 5(自律)も、**大四喜で同じ失敗をした。**自己申告のトップ3にこう挙げている。

「テスト用の牌姿を正しく書くこと。実装バグより多く、11回落ちた。『大四喜のテストのつもりが四暗刻単騎が複合して4倍役満』など」

「3本中3本です。しかも2本は同じ役(大四喜)で踏んでいる。麻雀を実装するときの通過儀礼と言っていい」(サトシ)

「この罠の本質は、麻雀のルールが正しく実装されているからこそ発生することです。役満の複合は正しい挙動で、コードは何も間違っていない。間違っているのは『この役だけを検証したい』という書き手の意図と手牌の食い違い」(カエデ)

罠2:CPUのパラメータが、評価関数に効いていない

Phase 6で、3本ともCPUの個性をパラメータで作り分けた。そして3本とも、定義したパラメータの一部が実際には効いていなかった。

  • Opus 5(対話): シャンテン数の重み(×30)が受け入れ枚数(×2)に飲まれ、CPUがテンパイしない
  • Sonnet 5: 同じ構造の式で、シャンテン重み×8 → ×200に修正
  • Opus 5(自律): efficiencyはほぼ効かず、foldAccuracyは完全に未使用だった

「3本目のDEVLOGの表現が的確です。『パラメータを定義しただけで評価関数に効かせていなかった』。名前を付けた時点で実装した気になる、という同じ形の失敗です」(ハルカ)

「これも単位の問題ですね。シャンテン数と受け入れ枚数という尺度の違う量を足すので、素直に書くと必ずスケールがぶつかる。モデルの差ではなく課題の構造です」(ハルカ)

罠3:最強に設定したキャラが、最強にならない

3本とも、自動対戦で強さを検証した結果、設計意図と実際の順位が一致しなかった。

症状
Opus 5 / 対話「最強」の雀聖レン 2.181 が、門前のソウ 2.095 に負けた
Sonnet 5balanced-ace 2.45 が menzen-high-value 1.90 に負けた
Opus 5 / 自律守備型を「放銃率最低」に調整した途端「平均順位最下位(2.837)」になった

3本目のDEVLOGは、この構造をこう書いている。

「守備と成績はトレードオフで、1つの指標だけを見て調整すると別の指標が壊れる。2回の往復が必要だった」

「さらに『200半荘だと平均順位の差が誤差に埋もれる。最終判断は1000半荘で行う必要があった』とも書いています。統計の設計まで自分でやり直している」(テツオ)

「打つ側から見ると、これは麻雀としてもっともらしいんですよ。守りを固めると順位が上がらない局面はある。設計者の直感が外れて、シミュレーションが正したという同じ話が3回起きている」(サトシ)

前回の結論が覆った:ウマの実装漏れはハーネス差だった

前回、編集部は「明確にモデル差が出た」としてウマ・返し点の件を挙げた。プロンプトが明示していた「25000点持ち30000点返し、ウマ+30/+10/-10/-30」の半荘清算を、Opus 5(対話)は型に宣言しただけで一度も実装せず、未対応リストにも載せず、欠落に気づかないまま公開した。一方Sonnet 5はPhase 9で自らgrepの悉皆チェックをかけて発見し、修正した。

3本目で、この結論が崩れた。

$ grep -rn "uma" src/core/game/engine.ts
933:  /** 順位・ウマ・オカを含む最終スコア(千点単位) */
942:      const uma = s.config.uma[i];
944:      return { seat: p.seat, points: p.points, rank: i + 1, score: base + uma + okaBonus };

**Opus 5(自律)は、ウマもオカも最初から実装していた。**DEVLOGにも「半荘の進行・ウマ・オカ。100半荘相当の連続実行で点数総和が常に100000に保たれることが一発で通った」と記録されている。

「同じモデルなのに、対話モードでは落として自律モードでは落としていない。前回『モデル差』と書いたのは誤りでした」(テツオ)

「なぜ対話モードのほうが落としたのか、という問いが残りますね」(ナオ)

「仮説ですが、対話モードには『フェーズごとに人間へ報告する』という別の目標が加わっているんだと思います。各フェーズの区切りで成果をまとめて報告する動きが入ると、要求文の網羅性より、その回で見せられる成果のほうに注意が向く。人間がいることで、かえって仕様の読み返しが疎かになった可能性があります」(カエデ)

ただし、同一ハーネス同士で比べれば差は残る。**自律モードのOpus 5は一発で実装し、Sonnet 5は見落として後から自己監査で回収した。**要件カバレッジという点ではOpus 5が上だが、Sonnet 5は「見落としを自力で発見する仕組み」を自分に課していた。

Sonnet 5のDEVLOGの総括は、今も有効な教訓である。

「人間のレビューが一切ない環境では、こうした**『後半フェーズで前半の要求文を読み返す』機械的な自己監査**が、機能欠落を発見できる数少ない手段だった」

残ったモデル差:失敗の記録を残すかどうか

プロンプトには「テストが落ちた状態のコミットも履歴に残す(--amendやsquashで失敗の履歴を消さない)」と明記していた。

同一の自律モードで比べると、はっきり分かれた。

失敗状態のコミット
Opus 5 / 自律3本Phase 1 (WIP): 6件失敗の状態で記録用にコミット / Phase 3 (WIP): 200局中22局が例外 / Phase 8 (WIP): 13項目中10項目成功(3項目失敗の状態)
Sonnet 5 / 自律0本

Opus 5は対話モードでも3本残していたので、実行モードによらず一貫している。

「これはモデル差として残ったと言っていいと思います。同じ指示、同じモード、片方は3本残し、片方は0本」(テツオ)

「加えてSonnet 5は、最終報告に**『13 commits (no squashing; failing/buggy states remain in history)』**と書いていました。実際の履歴に失敗状態はなく、コミット数も14です。自己申告と事実がずれている」(テツオ)

「フェアに言えば、Sonnet 5は自分でテスト件数の集計ミス(85と主張していたが実際は77)を見つけて訂正してもいます。自己監査の意識はある。ただ、自分の作業記録に対する精度は差が出ました」(カエデ)

逆説:人間が介在したほうが、完成度は低かった

もうひとつ、予想外の結果が出た。同じOpus 5で比べると、自律モードのほうが完成度が高い。

Opus 5 / 対話Opus 5 / 自律
タイトルの項目数25(対局・LAN対戦・牌譜再生・設定・クレジット)
配布ビルド未検証(cargo checkまで).dmg 生成成功(3.1MB)
LAN対戦の検証インメモリ通信路のテストのみ実プロセス間で13項目検証、13/13成功
牌譜再生未実装実装済み
設定・クレジット画面未実装実装済み

3本目のタイトル画面がこれだ。

Opus 5を自律モードで走らせた版のタイトル画面。上部に一萬・筒子・索子・發・東の5枚の牌が並び、金色の「雀影」とJAKUEIの欧文、その下に対局を始める(東南戦)、LAN対戦、牌譜を再生する、設定、クレジットの5つのボタンが並ぶ

「対話モードのOpus 5は、フェーズの区切りで報告して次に進むかを確認していました。その分、各フェーズを『報告できる状態』で切り上げていた。自律モードは誰にも報告しないので、切り上げる理由がない」(ユウタ)

「人間が見ているほうが品質が上がる、という直感と逆の結果ですね。少なくとも『完成度の広さ』については、口出ししないほうが良かった」(ミサ)

Sonnet 5の版はさらに項目が多く6つで、こちらも配布ビルドまで通している。

Sonnet 5版のタイトル画面。「麻雀 Sonnet5」「和モダン・リーチ麻雀」の下に、対局を始める(CPU対戦)、牌譜再生、設定、牌グラフィック一覧、CPUキャラクター一覧、クレジットの6つのボタンが縦に並んでいる

命名と牌のデザイン

余談だが、アプリの名前の付け方に傾向が出た。

名前
Opus 5 / 対話雀韻(JAKUIN)
Opus 5 / 自律雀影(JAKUEI)
Sonnet 5 / 自律麻雀 Sonnet5

Opus 5は2回とも「雀」で始まる二字熟語にローマ字を添えた。Sonnet 5は自分のモデル名をそのまま製品名にしている。

牌のデザインについては、前回Opus 5版を「要件は満たしたが良くない」と評価した。Sonnet 5版も並べる。

Sonnet 5版の牌グラフィック一覧。萬子は漢数字と小さな「萬」、筒子は小さな輪、索子は細い緑の棒で描かれ、發は3つの緑の三角形、白は空の枠になっている。各牌の下に1m、2pといった記号ラベルが付く

「両方ともフラットな塗りで、彫りの陰影がありません。『印刷された紙』に見えて『牌』に見えないという評価はSonnet 5版にも当てはまります。むしろ絵柄が小さくて余白が多いぶん、こちらのほうが厳しい」(ミサ)

「著作権を避ける工夫は分かれました。一索をOpus 5は竹の若芽に、Sonnet 5は細い棒に。發はOpus 5が文字のまま、Sonnet 5は緑の三角形3つ。文字すら使わない方向に振っていて、麻雀牌に見えなくなっています」(サトシ)

「3本目も字形の崩れをスクリーンショットで発見しています。DEVLOGに『五萬が「互」、南が「宙」に見える』と。牌の見た目の問題は、3本とも実行して目で見るまで気づいていません」(ミサ)

3本に共通した、最も重要な一致

最後に、3本すべてのDEVLOGが同じ結論に到達している。

3本目の自己申告から引用する。

「11件の実バグのうち、自分でコードを読み返して気づいたのは0件で、すべて統計シミュレーション・スクリーンショット・自然言語の生成例・実プロセス検証のいずれかが見つけたものです」

1本目の結論は「実装バグ10件はすべて統計・実行時間の違和感・スクリーンショットで見つかり、テストが落ちた6件は全部テスト側の誤りだった」だった。2本目のSonnet 5も、Phase 1・2で落ちたテストの多くを「コード側のバグではなくテストの前提が誤っていた」と記録している。

「モデルを変えても、実行モードを変えても、ここだけは一致しました。自分で書いたコードを自分で読み返しても、バグは見つからない」(カエデ)

「そして『自分で期待値を書くテスト』も同じです。書いた本人の理解を超えられないので、仕様のほうが書き手より複雑な領域では、期待値そのものが誤る」(カエデ)

結局、何が分かったか

ナオさんがまとめた。

「1つめ。モデルを変えるより、実行モードを変えるほうが結果が動いた。ウマの実装漏れは、モデルではなく対話か自律かで決まっていました。完成度の広さも同じで、自律のほうが高い」(ナオ)

「2つめ。残ったモデル差は、能力より流儀に近い。失敗の記録を残すか、自己申告が正確か、名前をどう付けるか。麻雀の実装能力そのものでは、3本に有意な差が見えませんでした」(ナオ)

「3つめ。同じ仕様書からは同じ罠が生えてくる。テスト手牌の役満複合、パラメータが効かない評価関数、難易度と実力の不一致。3本とも同じところで転んだということは、この課題の側が強いということです」(ハルカ)

実務への持ち帰りは、前回よりはっきりした。

「モデルの選定に悩むより、手順を1つ足すほうが効きます。仕様書のキーワードでgrepをかけて実装にヒットするか確かめる。統計を取る仕組みを最初に用意する。画面は早い段階で1回表示する。この3つは、どのモデルでも、どの実行モードでも効きました」(カエデ)

「そして人間が口を出すなら、フェーズの区切りで報告させることの副作用は意識したほうがいい。報告できる状態で切り上げる動きが入って、要求文の読み返しが薄くなる。少なくとも今回はそう見えます」(ミサ)

なぜ差が出なかったのか——そして、どこでなら差が出るのか

「麻雀の実装能力に有意な差が見えなかった」というのは、裏を返せばこの課題がモデルを区別しない性質を持っていたということでもある。編集部で、その性質を分解した。

「今回の課題は、3つの条件を全部満たしていました。仕様が確定していた・検証が機械的だった・答えが訓練データにあった。この3つが揃うと、たぶん差は出にくい」(カエデ)

具体的にはこうだ。

条件今回の課題
仕様が確定していた202行のプロンプトで、ダブロンの可否から連風牌の符まで全部決めてあった。解釈の余地を意図的に潰していた
検証が機械的だった点数早見表と照合する、1000半荘回して統計を見る。正しさが数値で確かめられる
答えが訓練データにある麻雀のルールも、シャンテン計算のアルゴリズムも、世の中に大量に書かれている
全部を自分で書いた他人の設計を読む場面が一度もなかった。自分の書いたものを覚えていればよかった

「1つめが一番効いていると思います。前回の記事で『仕様を確定させたのが良かった』と書きましたが、それは同時に差を消す作業でもあった」(ナオ)

「麻雀の点数計算は『複雑』と言われるけれど、複雑さの質が分岐が多いだけで各分岐は単純なんです。これは調べれば書ける種類の複雑さで、モデルの推論の深さを要求しない」(ハルカ)

だとすれば、差が出るのはこの条件を外したときのはずだ。編集部の結論は、麻雀を続けるのではなく、題材を変えるというものになった。

「麻雀で条件だけ変える案も出ました。仕様を曖昧にした版を渡す、3本の成果物を互いにデバッグさせる、といった案です。素材はタダで手に入るし、対照実験としてはきれいなので捨てがたい」(ユウタ)

「ただ、4つの条件のうち3つは麻雀という題材そのものに貼り付いています。ルールが世に大量に書かれていて、正しさが表と照合できるという性質は、仕様書の書き方を変えても消えません。ここを外すには題材を変えるしかない」(カエデ)

次に扱うのは、クラッシュ安全な組み込みKVストアである。

組み込みKVストアとは何か

聞き慣れない読者のために説明しておく。組み込み(embedded)データベースとは、サーバを別プロセスで立てず、アプリケーションと同じプロセスの中で動くデータベースのことだ。もっとも身近な例はSQLiteで、あれは「組み込みのリレーショナルデータベース」にあたる。今回扱うのは、そのキーバリュー版である。

APIは驚くほど単純だ。キーを渡して値を入れる、キーを渡して値を取る、キーの範囲を順に舐める。それだけである。

put("user:1042", "田中")
get("user:1042")        → "田中"
range("user:1000".."user:2000")  → 順に取り出す

単純に見えるが、これを電源が落ちても壊れないように、しかも複数のスレッドが同時に読み書きしても矛盾しないように作るのが本題になる。

こうしたライブラリは、実は身の回りで大量に動いている。

使われている場所中身
Kubernetes の etcdbbolt(Go製のB木ストア)
Bitcoin CoreLevelDB
Mozilla の rkv(Firefox の一部機能)LMDB
Prometheus独自のTSDB(下層は同種の仕組み)
モバイルアプリのローカル保存SQLite、RocksDB
CI・ビルドツールのキャッシュ各種KVストア

「表に出ないだけで、インフラの土台に必ず1つは入っている種類の部品ですね。サーバ型のデータベースと違って運用が要らないので、『とりあえずディスクに安全に置きたい』という場面で使われます」(ユウタ)

設計には大きく2系統ある。B木系(LMDB、bbolt、redb)は読み出しが速く、ページ単位でその場を書き換える。LSM系(LevelDB、RocksDB)は書き込みを一旦ログに追記してから後でまとめて整理するので、書き込みが速い代わりに裏で圧縮処理が走る。今回作るのはB木系である。

比較対象に選んだ redb

redbは、Rustで書かれた組み込みKVストアだ。LMDBの設計に影響を受けたコピーオンライトのB木を採用し、単一ファイルにデータを保持し、ACIDトランザクションを提供する。ライターは1度に1つ、リーダーは複数同時、という構成である。

比較対象として選んだ理由は3つある。

  • 土俵が同じ —— 同じRust製なので、言語ランタイムの差で数字が動かない。CのLMDBを相手にすると、勝敗が言語差なのか設計差なのか分からなくなる
  • 現代的で、活発に開発されている —— 枯れて放置された実装を相手に勝っても意味がない
  • 正しさを重視した設計 —— redbはmmapに依存しない方向へ舵を切っており、クラッシュ整合性の議論が公開されている。同じ土俵で正しさを語れる相手である

「相手選びは大事です。遅い実装を選んで勝ったと言うのは、レーンを狭く決めるのとは違う。redbは真面目に速く、真面目に正しく作られているので、勝てたら本物だし、負けても納得できます」(ハルカ)

麻雀で消えた4条件が、すべて反転する

この題材を選んだ本当の理由は、こちらにある。

条件麻雀組み込みKVストア
検証点数早見表と照合すれば正しいクラッシュ整合性はテストで網羅できない。100件全緑でも、fsyncの狭間で電源が落ちればデータは飛ぶ
答えの所在ルールもアルゴリズムも世にある設計判断の組み合わせは無数にあり、正解が一意でない
仕様202行で確定できた「何をもって耐久性とするか」から自分で定義する必要がある
読む対象全部自分で書いた既存OSS(redb)のベンチマークと正面から比較する

そして、この題材なら本連載で扱ってきた3つのテーマを同時に載せられる。

  • テストで見つからない正しさ —— 並行トランザクションのスナップショット分離と、クラッシュ整合性
  • 攻撃と防御 —— 一方のモデルが書いたストアに、もう一方があらゆるfsync境界でクラッシュを注入する。目標は明快で、「コミット成功と返答されたトランザクションを1件でも消すこと」
  • 性能でOSSに挑む —— redbを相手に、事前に宣言したワークロードで数値を出す

「性能の勝負については、レーンを狭く、正直に決めるのが条件です。『LMDBより速いKVストアを作る』は達成できません。20年磨かれた実装に総合力で勝てるはずがない」(ハルカ)

「狙うのは独立したクライアントが小さな書き込みを並行に投げるワークロードです。redbは単一ライターで、コミットごとにfsyncする。ここに複数トランザクションを1回のfsyncにまとめる仕組みを持ち込めば、構造的に有利になります。測り方を工夫して勝つのではなく、設計が有利なレーンを名指しで選ぶ」(ハルカ)

「同時に、点読み出しと範囲スキャンも測って併記します。そこはたぶん負ける。負けをそのまま載せることが、レーン選択の正直さの担保になります」(テツオ)

麻雀の連載で効いた運用——調整前の数字を先に書かせる、失敗状態のコミットを残させる——に加えて、次回はベンチマークの方法論を着手前に文書化させることにした。ページキャッシュの扱い、fsyncの意味論、ウォームアップ、試行回数、外れ値処理。後から都合のいい測り方を選べないようにするためである。

「今回の記事で一番効いたのは、Opus 5がウマを落としていた事実を数字とgrepの出力で出せたことだと思います。次も同じで、勝てなかったらそのまま書きます」(テツオ)

キーワード:#Claude Opus 5#Claude Sonnet 5#モデル比較#自己監査#評価関数

出典

コメント