Radeon 780MでローカルLLMにブラウザゲームを作らせる:テストハーネスの記録
Minus Zeroでは、小さなローカルモデルに仕様書と実際のテストスイートを渡したら、正しいコードを書けるのかを試しました。8つの関数が組み込まれ、63個のテストが通りました。符号付きゼロの失敗が繰り返されたため、8Bモデルから、より遅い27Bモデルへ切り替える明確な理由も得られました。
ただ、もっと大きな疑問が残りました。6行の数値関数には狭い契約があります。ブラウザゲームにはレイアウト、入力、時間、アニメーション、状態遷移、モバイル向けの制約があり、ページを動かして初めて現れる不具合もあります。
クラウドモデルをループの外に置いたまま、ローカルモデルだけでゲーム全体を作って修正できるのかを見たくなりました。
題材にしたのはNeon Bastionというモバイル向けタワーディフェンスゲームです。index.html、styles.css、script.jsの3ファイルで構成しました。完成した成果物は、390×844と1280×720の両方で、ブラウザと静的チェック38個を通過しました。面白かったのは、ゲームを生成することより、生成コードの間違いを見抜く仕組みが育っていったことです。
役割を分ける
使ったのは、これまでローカルモデルの実験に使ってきたRadeon 780M搭載の小型PCです。パイプラインでは、各段階に合うモデルへ仕事を分けました。
gpt-oss-20b-16kは設計を立て、結果をレビューし、範囲を限定した修正案を出す。AntLing/Ling-3.0-tiny:q8_0は、1回の呼び出しでソースファイルを1つ完成させる。- ヘッドレスのMicrosoft Edgeはゲームを実行し、独立した証拠を返す。
この分担には、それまでのエージェント実験で見た理由があります。大きいローカルモデルは、編集を始めないまま数分考え続けたり、同じ不確かさを繰り返したり、ハーネスが必要とする構造化された回答を返す前に出力上限へ達したりしました。小さいモデルは最初のまとまった下書きを出すことに向いていましたが、自分の間違いを診断する力は弱めでした。
プランナーは受け入れ条件を文法制約付きJSONへ変換します。LingはHTML、CSS、JavaScriptを別々に書きます。静的チェックはファイルの欠落、外部依存、無効なJavaScriptを調べます。Edgeはゲームをスマートフォンとデスクトップの両方として読み込みます。ハーネスはスクリーンショット、ブラウザ例外、DOMの位置と大きさ、HUDの値、ゲームが公開する診断スナップショットを保存します。
失敗は、修正できそうなファイルだけへ戻します。パッチモデルは検索文字列と置換内容を指定した編集を返します。修正のたびにブラウザを再読み込みします。
「見た目が動いている」を実行可能にする
最初のタワーディフェンス用プロンプトには、4種類のタワー、4種類の敵、ウェーブ、ゴールド、ライフ、道、モバイルレイアウトが書かれていました。それでも、見た目はそれらしくても中身が足りない実装を許してしまいます。
最終的な契約では、次のような具体的な動作を検査しました。
- 8点以上の経路。横方向と縦方向の広がりも確認する。
- Gold、Lives、Wave、Scoreという読みやすいラベル。
- コストと効果が異なる、タップしやすい4つのタワー操作部。
- 体力と速度が異なり、fast役とtank役を含む4種類の敵。
- 敵が段階的に出現し、ウェーブが進むにつれて難しくなること。
- 経路上への配置と、ゴールド不足の購入を拒否すること。
- 経済、ダメージ、撃破数、スコア、ライフ、ゲームオーバー、再スタートが動くこと。
- どちらの対象ビューポートでもオーバーフローしないこと。
ゲームには読み取り専用のwindow.__gameDebug.snapshot()インターフェースも持たせました。正規化した経路座標、タワーと敵の定義、現在の状態、後半ウェーブの倍率予測を返します。これでゲームの挙動を調べられます。
スクリーンショットに、色の違う4つのタワーボタンを写すことはできます。タワーの仕組みまで証明することはできません。敵が画面に出ていても、移動するとは限りません。ゴールドの表示が減っても、違法な配置で所持金が守られたとは限りません。
バリデーターは、その境界を直接試しました。経路上への違法な配置と、地面への合法な配置を実行します。敵の位置が変わること、タワーが敵にダメージを与えること、ゴールド不足の購入が拒否されること、後半ウェーブの難度が上がることを確認します。
最初のゲームは説得力があって、間違っていた
最初に生成されたバージョンは、偽の安心感を作るには十分な見た目でした。ブラウザでの実行結果が不具合を見つけました。
モバイルではキャンバスが小さすぎて、大きな空白に挟まれていました。HUDは、契約が求める読みやすいラベルの代わりにアイコンを使っていました。デスクトップではキャンバスがビューポートからはみ出し、タワー選択欄を画面の下へ押し出しました。診断インターフェースから定義配列がいくつか抜けていたため、見た目の差を仕組みの差と結び付けられませんでした。敵の描画ではcreateRadialGradientの中に有限でない値が渡され、例外も発生しました。
ここで重要な結果が出ました。モデルの自己レビューや軽い目視確認より、測定機能を備えたブラウザのほうが弱点を見つけられました。
ハーネスはゲームの不具合を見つけました。その実行の中で、ハーネス自身の不具合も見つかりました。
修正ループが自分の対象を壊したとき
最初の修正機構は、検索文字列が現在のソースに一致すればパッチを受け入れていました。モデルが実在するコードを参照したことは確認できます。置換後のコードが安全だとは確認できません。
個別には適用できるJavaScriptの編集が、関数本体を重複させました。script.jsは21,148文字から38,086文字へ増え、構文解析に失敗しました。小さな修正を行うはずのシステムが、修正対象より大きな回帰を作っていました。
次のバージョンには、3つのガードを加えました。
- JavaScriptのパッチは、現行ファイルを置き換える前に構文チェックする。
- 小さなファイルへの例外を設けたうえで、通常は15%を超える不自然な増加を拒否する。
- 壊れた修正後スクリプトを
_harnessへ隔離し、保存していた初期実装を復元する。
38KBの無効なスクリプトは今も保存しています。失敗した成果物を残すと、ハーネスの調査がしやすくなり、履歴を書き換えずに実行を再開できます。
実行系の問題も見つかりました。あるgpt-ossの呼び出しは4,000トークンをすべて推論に使い、JSONを返しませんでした。現在のハーネスは、不正または不完全な返答に対して、回数を限定した再試行を行います。Goldラベルの失敗はHTMLのID、スタイル、JavaScriptにまたがっていましたが、最初の振り分けではCSSだけに送られていました。現在はカテゴリに応じて振り分けます。レビューがverdict: "pass"を返しながら重大な指摘を含む場合もあるため、重大度の高い指摘がその判定を上書きします。
改善の中心にあったのは、モデルの出力へ預ける信頼の量を減らすことでした。
780Mの中で動かす
780Mには、もう一つの調整課題がありました。Lingとgpt-ossを、必要なコンテキスト長のまま同時に安定して載せ続けることができませんでした。各呼び出しの前に、ハーネスがOllamaとローカルのllama.cppルーターを確認し、次の段階で使わないモデルをアンロードしました。
生成速度は実用範囲でしたが、各段階には計測できるだけの時間がかかりました。
| 段階 | モデル | 時間 | 速度 |
|---|---|---|---|
| アーキテクチャ設計 | gpt-oss 20B | 131.1秒 | 18.6 tok/s |
| 採用されたHTML | Ling Q8 | 22.8秒 | 37.0 tok/s |
| CSS | Ling Q8 | 65.9秒 | 35.6 tok/s |
| 初期JavaScript | Ling Q8 | 249.9秒 | 31.4 tok/s |
中程度の推論設定による修正呼び出しは4〜7分かかることがあり、パッチを返す前に予算を使い切る場合もありました。範囲を限定したパッチ作業を低い推論設定へ移すと、小さな修正は数十秒で終わるようになりました。設計と最終レビューには大きい推論予算を残しました。
速いパイプラインではありません。遅さを記録し、上限を設け、途中から再開できる形にしました。
最後の失敗
最終版のNeon Bastionには、保存したモデル応答ファイル45個、検証スナップショット13個、2つのビューポートのスクリーンショット、隔離した無効スクリプトが残っています。3ファイルの現行ソースは約31KBで、ブラウザエラーなしに38個すべてのチェックを通過します。
最後まで失敗していたチェックはpath-placement-rejectedでした。
生成されたisOnPath()ヘルパーは経路の各線分を正規化していました。そのあと、正規化後の長さを1と比較して、すべての線分を捨てていました。正規化された有効な線分が、検査対象として短すぎると扱われていたわけです。解像度の変更やCSSの修正、広い範囲を対象にしたモデルパッチでも、この幾何学的な判定には届きませんでした。
最後の失敗を手で調べ、しきい値を小さなイプシロンへ置き換えました。次の検証では38個中38個が通りました。
ここでの人間の介入も結果の一部です。ハーネスは、インタラクティブなゲーム全体を、原因を理解して修正できる、具体的で再現可能な1つの失敗へ縮めました。成功メッセージの裏に壊れたページを隠すより、扱いやすい失敗の形です。
Minus Zeroから変わったこと
Minus Zeroでは、既存の契約に対してモデルを検証しました。今回は、複数のモデルを囲む仕組みを検証しました。
前のループが尋ねたのは、小さいモデルが関数を実装できるか、そして失敗の形が繰り返されたときに大きいモデルが役に立つか、でした。タワーディフェンスのハーネスが尋ねたのは、独立した証拠を使って、ブラウザアプリ全体の設計、生成、修正、目視確認、復旧を導けるか、でした。
ローカルモデルは、驚くほど完成度の高い最初の下書きを出せます。大きいローカルモデルは、その下書きを診断して修正できます。大切なのはモデル呼び出し単体ではありません。明確な契約、実行可能な証拠、範囲を限定した編集、保存された成果物、失敗後に再開する方法まで含んだループです。
Minus Zeroでは符号付きゼロの算術に能力の壁が見えました。Neon Bastionでは、アプリケーション規模の作業にある信頼の境界が見えました。Neon Bastionそのものに意味があったわけではありません。ハーネスが実際に失敗し、修復するための題材だっただけです。