GPT-4 で Minecraft を自動プレイする Voyager を動かしてみた

# 概要
GPT-4 に全自動で Minecraft をプレイさせる論文 ["Voyager: An Open-Ended Embodied Agent with Large Language Models"](https://voyager.minedojo.org/) を紹介します。

**Voyager** は、継続的・段階的に複雑なタスクを学習し続けることができ、マップ開拓や新アイテム獲得の能力で既存手法に勝ると主張されています。

![progress](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142066/347e0b0a-89a3-efde-b304-cfca070bb322.png)


# 既存手法との違い
LLM にツールや外部 API を与えて自律的に計画・行動させるアルゴリズムと言うと、[ReAct](https://ai.googleblog.com/2022/11/react-synergizing-reasoning-and-acting.html), [Reflexion](https://arxiv.org/abs/2303.11366), [Auto-GPT](https://github.com/Significant-Gravitas/Auto-GPT) などが特に有名です。

これらと Voyager の一番の差別化部分は、**Iterative Prompting Mechanism** および **Skill Library** と呼ばれるコンポーネントです。
Voyager はボットを操作するために [Mineflayer](https://github.com/PrismarineJS/mineflayer) という JavaScript API を利用しており、「ゾンビと戦う」などのタスクを実行するコードを GPT-4 に生成させて eval します。

![components](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142066/c8e895b9-969e-51bb-19e8-c27b714aba17.png)

実行結果やエラーのフィードバックを受けて自動でコードを修正し、最終的にタスクに成功した場合のみコードが Skill Library に保存されます。この時にコードの説明文をベクトル埋め込みして保存し、必要に応じて学習済みのスキルを再度実行できるようになっています。

# 実行結果
## コスト
Open AI API の使用料が **1 時間あたり大体 5 ドル**程度かかります。

## 録画
次の動画は、Voyager を実行している様子をリアルタイムかつノーカットで録画したものです。

<iframe width="560" height="315" src="https://www.youtube.com/embed/qQZElCRuaqU" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>

ほとんどの時間静止したままですが、これはバグではありません。
GPT のレスポンスを待つ間ボットは何もできないので、Minecraft サーバーを一時停止する処理がスクリプトに組み込まれています。

[ReplayMod](https://www.replaymod.com/) を使って、後半部分ボットが動いている間だけ抽出したのが以下の動画です。12:07 あたりでは金を採掘することに成功しています。

<iframe width="560" height="315" src="https://www.youtube.com/embed/yOjhNXWyQgM" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>

## ボットが詰まった部分
動画の最後でインベントリが一杯になってチェストにアイテムを保管しようとしていますが、妙に狭い場所にいるためか上手くチェストに触れることができずスタックしています。
この時 GPT-4 が生成したコードは以下の通りで、特におかしな点は見当たりません。おそらく、[プリミティブとして使われている JS コード](https://github.com/MineDojo/Voyager/blob/9fc787e5f4d027bbb34bee56e07c659e3a2dc2ad/voyager/control_primitives/useChest.js#L31-L59)の方が足を引っ張っていそうです。

**6 月 2 日追記**：ボットのチェスト管理周りのコードにバグがあったようで、無事修正されました。 ([GitHub issue](https://github.com/MineDojo/Voyager/issues/56))

```javascript
async function placeAndDepositUselessItems(bot) {
  // Find a suitable position to place the chest
  const chestPosition = bot.entity.position.offset(1, -1, 0);
  const chestBlock = bot.blockAt(chestPosition);
  if (chestBlock && chestBlock.name === "chest") {
    bot.chat("Chest already exists at the position.");
  } else {
    // Find an empty position to place the chest
    const emptyPosition = bot.entity.position.offset(1, -1, 1);
    const emptyBlock = bot.blockAt(emptyPosition);
    if (emptyBlock && emptyBlock.name === "air") {
      // Place the chest at the empty position
      await placeItem(bot, "chest", emptyPosition);
      bot.chat("Chest placed.");
    } else {
      bot.chat("No suitable position found to place the chest.");
      return;
    }
  }

  // Update the chest position after placing it
  const updatedChestPosition = bot.findBlock({
    matching: mcData.blocksByName.chest.id,
    maxDistance: 32
  }).position;

  // Define a list of useless items to deposit
  const uselessItems = {
    andesite: bot.inventory.count(mcData.itemsByName.andesite.id),
    dirt: bot.inventory.count(mcData.itemsByName.dirt.id),
    cobblestone: bot.inventory.count(mcData.itemsByName.cobblestone.id),
    wooden_pickaxe: bot.inventory.count(mcData.itemsByName.wooden_pickaxe.id)     
  };

  // Deposit the useless items into the chest
  await depositItemIntoChest(bot, updatedChestPosition, uselessItems);
  bot.chat("Useless items deposited.");

  // Check the number of occupied inventory slots after depositing
  const occupiedSlots = bot.inventory.used();
  if (occupiedSlots <= 20) {
    bot.chat(`Success! Inventory has ${occupiedSlots} occupied slots.`);
  } else {
    bot.chat(`Failed! Inventory still has ${occupiedSlots} occupied slots.`);     
  }
}
```

## チェックポイント
Voyager を実行するとプロジェクトルート直下に `/ckpt` ディレクトリが作成され、イベント履歴などが保存されます。
特に `/ckpt/skill/code` 以下には Skill Library に追加された JavaScript コードが置かれるため、どんなタスクを達成したのか詳しく確認することができます。

今回生成された `/ckpt` のファイルと、GPT への全リクエスト履歴を以下の GitHub レポジトリで公開しています。

https://github.com/swen128/Voyager_checkpoint

例として、金の採掘に成功した際のコードは以下の通りです。

```javascript:ckpt/skill/code/mineGoldOreAndSmelt.js
async function mineGoldOreAndSmelt(bot) {
  // Check if the bot has an iron pickaxe or better in its inventory
  const ironPickaxe = bot.inventory.findInventoryItem(mcData.itemsByName.iron_pickaxe.id);
  const diamondPickaxe = bot.inventory.findInventoryItem(mcData.itemsByName.diamond_pickaxe.id);

  // If not, craft an iron pickaxe
  if (!ironPickaxe && !diamondPickaxe) {
    await craftItem(bot, "iron_pickaxe", 1);
    bot.chat("Iron pickaxe crafted.");
  }

  // Explore in a random direction until a gold ore is found
  const randomDirection = new Vec3(Math.random() > 0.5 ? 1 : -1, 0, Math.random() > 0.5 ? 1 : -1);
  const goldOre = await exploreUntil(bot, randomDirection, 300, () => {
    const foundGoldOre = bot.findBlock({
      matching: mcData.blocksByName.gold_ore.id,
      maxDistance: 32
    });
    return foundGoldOre;
  });
  if (goldOre) {
    // Mine the gold ore
    await mineBlock(bot, "gold_ore", 1);
    bot.chat("Gold ore mined.");

    // Place the furnace if not already placed
    const furnaceBlock = bot.findBlock({
      matching: mcData.blocksByName.furnace.id,
      maxDistance: 32
    });
    if (!furnaceBlock) {
      const furnacePosition = bot.entity.position.offset(1, -1, 1);
      await placeItem(bot, "furnace", furnacePosition);
      bot.chat("Furnace placed.");
    }

    // Smelt the gold ore in the furnace to obtain a gold ingot
    await smeltItem(bot, "gold_ore", "coal", 1);
    bot.chat("Gold ore smelted into gold ingot.");
  } else {
    bot.chat("Gold ore not found within exploration time.");
  }
}
```



# 手元で動かす方法
## セットアップ
GitHub レポジトリの README に詳しい手順が記載されています。

https://github.com/MineDojo/Voyager

- Python 3.11 だと依存パッケージインストール時にエラーが起きます。3.9 か 3.10 を使いましょう
- ローカルに Minecraft がインストールされている前提で諸々ハードコードされているので、Docker コンテナなどを使おうとすると面倒です
-  `LAN に公開` する時は `ゲームモード：サバイバル` のままにしてください。クリエイティブモードだとインベントリが無いためボットがアイテムを拾ったか判定できず、同じタスクでスタックします


## ボットを観察するには
- 実行時間の大半はポーズ状態なので、[ReplayMod](https://www.replaymod.com/) などを使って録画しておくことをお勧めします
- リアルタイムに監視する際には Minecraft の [spectate コマンド](https://minecraft.fandom.com/ja/wiki/%E3%82%B3%E3%83%9E%E3%83%B3%E3%83%89/spectate)が便利です (`/spectate bot`)
- ボットは松明無しに地下へ潜りがちなため、自キャラに[暗視効果](https://minecraft.fandom.com/ja/wiki/%E6%9A%97%E8%A6%96)をつけておくと良いでしょう (`/effect give @s minecraft:night_vision`)
- タスクを 1 つ達成するたびにボットがサーバーを出入りするため、毎回 spectate が解除されてしまいます。[コードのこの部分](https://github.com/MineDojo/Voyager/blob/edeee83/voyager/env/mineflayer/index.js#L52)に以下のようなコマンドを仕込んでおくと楽です

```diff:voyager/env/mineflayer/index.js
    bot.once("spawn", async () => {
+       bot.chat("spectate @s @p)
        bot.removeListener("error", onConnectionFailed);
```