AI・機械学習
5000億トークン後:AIエージェントに一人称シューティングゲームの逆コンパイルをさせる
500B Tokens Later: Letting AI Agents Decompile a First-Person Shooter (momo5502.com)
要約
この記事は、AIエージェントを用いて一人称シューティングゲームの逆コンパイルを試みたプロジェクトについて詳述しています。プロジェクトでは、AIのオーケストレーション、インフラストラクチャの最適化、および正確なC++コード生成を目指しましたが、最終的にはAIが生成したコードのセマンティックな誤りを検出するための「オラクル」として、バイト単位での比較検証システムを導入する必要がありました。
全文翻訳
過去3ヶ月間、私は時間とトークンを費やして、ある人気の一人称シューティングゲームの逆コンパイルを行いました。目標は、単純な概念実証の状態に到達することではありませんでした。むしろ、ゲームの正確で安定した、機能完備の再現を目指しました。
私のブログの熱心な読者は、以前に書かれた2つの投稿が削除されたことに気づいたかもしれません。それ以外の読者は、私がどのゲームについて話しているのか疑問に思っているでしょう。どちらの読者にも、私はアメリカの企業が私たちの楽しみを台無しにしに来たとしか言えません。しかし、それは問題ありません。この投稿はゲームについてではありませんし、逆コンパイルのプロセスについてもあまり触れません。AIのオーケストレーションと、最適な結果を得るためにインフラストラクチャ、セットアップ、およびハーネスをどのように最適化するかについて、より多くを語ります。
このプロジェクトは、RektInator、Future、st0rm、そしてコミュニティの他のメンバーの助けを借りて行われました。彼ら全員に心から感謝します。
私たちの目標は何でしたか?
私たちは、ゲームをC++に正確に逆コンパイルすることを目指しました。明らかな意味論的な正確さに加えて、私たちはいくつかの追加要件を持っていました。
私たちは、読みやすく、コンパイル可能なC++ソースコードを求めていました。ゲームが古いことを考えると、セキュリティとバグ修正、そして移植性の向上も望んでいました。Linux、macOS、ブラウザなどでゲームを実行できれば素晴らしいでしょう。
後に、元の動作の再構築に完全に集中するために、最新化と移植性の追求は延期しました。
明らかに、全体的な目標は、数ヶ月にわたる自律AIエージェントの効果的なオーケストレーション方法を学ぶことでした。
初期セットアップ
私たちはClaude Max(20インスタンス)から始め、その後Codex Proを追加し、両方のサブスクリプションを同時に使用しました。モデルの選択は大きく変動しました。ほとんどの時間Sonnet 5を使用していましたが、Opus 5.5、Luna、Sol、Terraも多く使用しました。これについては後述します。
ClaudeエージェントはClaude Code CLIで実行され、CodexエージェントはCodex CLIで実行されました。他のエージェントハーネスも試しましたが、選択はほとんど影響がなかったので、デフォルト設定に留まりました。
進捗追跡
GitHub CLIを使用して、エージェントはGitHubのイシューを管理して進捗を追跡します。翻訳単位(.cppファイル)ごとに1つのイシューがあります。さらに、ラベルはイシューをグループ化し、優先順位付けするのに役立ちます。
コミュニケーション
エージェントはDiscordを介して通信します。すべてが1つのチャンネルにアクセスでき、そこで全てのメッセージを投稿および読み取ることができます。Discordは、エージェント間の通信だけでなく、人間とエージェント間の通信も可能にします。したがって、他の参加者はマシンアクセスを必要とせずに、エージェントと話すことができます。
GitHubのWebhookはCIの失敗を共有チャンネルに投稿するため、何かが壊れたときにエージェントは通知を受け取ります。
逆アセンブルと逆コンパイル
エージェントは、ほぼ全ての期間、Hex-Raysの公式ida-mcpを使用していました。これは非常にうまく機能します。非常に安定しており、ヘッドレスで、このプロジェクトに必要なすべてをサポートしています。強くお勧めします。
最初の1ヶ月
その時点で4つのエージェントを実行していました。3つのワーカーエージェントが逆コンパイルとコミットを行い、1つのレビューアエージェントが受動的に調整し、コミットをレビューしてバグをフラグ付けしました。
エージェントはゲームの約80%を逆コンパイルし、目に見える進捗がありました。ゲームは起動し、メインメニューが表示され、マップをロードできるようになりました。
これらの4週間の多くをセットアップの最適化に費やしました。
コンパクションを早期にトリガーすることで、トークン消費を削減しました。デフォルトのコンパクションしきい値はコンテキストの90%ですが、これを42%に削減しました。逆コンパイルは多くの揮発性情報を含みます。逆コンパイルされた関数は、もはや関連性がなくなり、コンテキストから削除される可能性があります。したがって、早期のコンパクションは、このような「ジャンク」をコンテキストから削除するのに役立ちます。
また、エージェントは時間の経過とともに集中力を失う傾向があることに気づきました。1回のコンパクションサイクル内でも、エージェントはドリフトして集中力を失う可能性があり、コンテキストが保持するデータが増えるほどその傾向は強まります。エージェントは、前の関数を完了する前に別の関数に移動することがありました。CIを監視しているにもかかわらず、失敗通知をDiscordで受信してもアイドル状態になることがありました。時折、作業が実際に完了したかどうかを徹底的に確認せずにイシューを閉じることがありました。
ターミナルで並んで作業している場合、エージェントを誘導してそれを防ぐことができますが、自律的に作業させている場合は誘導できません。
それを防ぐために、私たちは目標、エージェントの作業方法、回避すべきこと、および特定の状況の処理方法を定義したドキュメントを作成しました。
毎時実行されるcronジョブは、エージェントにこのドキュメントを再読み込みするように自動的に要求を注入し、指示をコンテキスト内で新鮮に保ちました。
これはエージェントに集中させるための理想的な方法ではないかもしれませんが、プロジェクトの終わりまで非常にうまく機能しました。
指示ドキュメントの洗練に多くの時間が費やされました。コンテンツはこのプロジェクトに非常に特化しているため、ここで共有する意味はあまりありません。
しかし…
セットアップとインフラストラクチャの最適化に全力を尽くしたにもかかわらず、作業の品質について話す必要があります。
継続的な進捗(ゲームの起動、メニューのレンダリング、マップのロード)により、逆コンパイルの品質は素晴らしいと信じていました。しかし、そうではありませんでした。
コードは非常に読みやすかったにもかかわらず、意味論的に間違っていました。エージェントは間違った関数シグネチャ、型、または構造体レイアウトを使用していました。彼らはロジックを発明したり、不要と判断したロジックを削除したりしました。
意味論的なエラーを超えて、エージェントは不要なアーキテクチャ変更も導入しました。例えば、ゲームにはグローバル変数を通じてアクセスされる特定の構成変数があります。エージェントは、この定数メモリアクセスを、桁違いにコストの高いルックアップを持つハッシュテーブルに変換していました。そして、これはうまくいかなかった多くのことのほんの一例です。
なぜこうなったのか?
レビューアはバグの発見に役立ちますが、それ以上のことにはうまく機能しません。アーキテクチャ上の決定は、目標に沿っている限り、疑問視されませんでした。
主な理由は、客観的な受け入れ基準がなかったことです。「正しさ」を適切に定義していませんでした。したがって、レビューアはどの変更が正しく、何が間違っているかを判断するのが困難でした。明らかにゲームをリファレンスとして持っていましたが、最新化と移植性もリストにあったため、特定の逸脱はバグとして扱われませんでした。
興味深いことに、コミットやコード内のコメントは、ワーカーエージェントが書き込んだどのような正当化の理由であれ、レビューアに逸脱を受け入れさせることになりました。ワーカーのコメントは、効果的に意図しないプロンプトインジェクションとして機能しました。レビューアは、逸脱を独自にチェックする代わりに、それらを正当化として受け入れました。
オラクル
私たちが求めていたのは、再構築された関数がオリジナルと一致するかどうかをエージェントに伝える自動チェックでした。それは同一の意味論を検証する必要があります。単純なPASSまたはFAILの信号で十分で、エージェントは自分で何が間違っているかを把握できます。
バイトマッチング逆コンパイル
これを達成する最も簡単な方法は、バイトマッチング逆コンパイルでした。私たちは、元のゲームのビルドに使用されたコンパイラに切り替え、比較を実行するスクリプトを作成しました。
スクリプトは、私たちの再構築されたOBJファイルとゲームのEXE/PDBを読み取ります(PDBがあるのは素晴らしいことで、プロセスを少し簡単にしますが、PDBがなくてもプロセスは同様に機能します)。次に、OBJとEXEから関数データを抽出し、全てのバイトを比較します。一致すれば関数は正確ですが、そうでなければ失敗し、エージェントは関数を再作業する必要があります。
他の関数やデータへの参照は、ターゲットがコンパイル済みバイナリのどこに配置されるかによってエンコード値が異なるため、バイト単位で一致するとは限りません。幸いなことに、OBJファイルはこれらの参照をリロケーションとして記録します。直接比較からリロケーションバイトを除外し、代わりに両方のバージョンが同じオフセットで同じシンボルを参照していることを検証できます。
スクリプトは、データと型についても同様のことを行います。
エージェントは、プッシュする前に作業を検証するために、このスクリプトを使用できます。
再構築された関数は、一連のテキストファイルに記録されます。CIはこれらのテキストファイルを使用して、記録された全ての関数を検証し、リグレッションが発生した場合は警告を発することができます。
チート
このスクリプトを導入したとき、エージェントが最初に行ったことはインラインアセンブリを書くことでした。これは明らかに目的を損ないます。そのため、特定の構文を禁止するように指示を洗練する必要がありました。