Claudeは最強のLLMになったのか？～無料チャット系AI比較～

## Claudeは最強のLLMになったのか？～無料チャット系AI比較～

### この記事について
先日AnthropicがClaude3を公開し、類似のAIサービスの精度を上回ったとかIQ100を超えたとかで話題になりました。

[Anthropic.com](https://www.anthropic.com/news/claude-3-family)

[参考：Anthropicが次世代AIモデル「Claude3」公開、GPT-4やGeminiを性能で上回る](https://xtech.nikkei.com/atcl/nxt/news/24/00347/)


しかしそのようなニュースを目にしても、多くの人は「けっきょく何がどうすごいの？」と思うことでしょう。私もそのひとりです。

ということで、最近話題にあがるチャット系AIはどれくらいすごいのか、共通の課題を与えて比較してみました。この記事ではその結果をまとめています。

注意点として、各サービスやモデルの優劣を決定づける意図はないこと、またLLMの性質上検証に再現性が担保されないことをご留意ください。

### 課題とルール

今回の検証では、AIサービスに共通の課題を与えます。課題の選定は私の好みで、「2048」というゲームを作成してもらうことにしました。

[2048 公式サイト](https://play2048.co/)

こちらのゲームは2013年ごろに世界中で流行ったパズルゲームです。以下のような特徴を持ちます。

- 公式のソースコードが、Git Hub 上にオープンソースとして公開されている
- 単一ページで遊べる上に全体的なアルゴリズムがシンプルである
- 機能やデザインの拡張性が高い

そのため今回の検証に適していると考えました。嘘です青春のゲームなので題材にしたかったんです。

各種チャット系AIサービスに命令を打ち込み、この「2048」を作らせます。それにあたり次のようにルールを定めました。

- AIサービスはすべて無料のものを使う
- プロンプトには日本語のみ入力する
- HTML形式で出力させる（スクリプトやスタイルのファイルを分けるのは可）
- 生成させたコードはコピペでのみ反映させ、自力で直接編集しない
- AIに対してコードそのものの具体的な不備や修正案を出さず、ユーザー視点で要求を伝える

つまり、専門技術を持たない日本語話者が裸一貫でゲーム制作に挑戦するようなロールプレイとなっています。

また、評価基準については感覚的なものとなってます。具体的に性能の序列をつけることが目的でなく、使用感を具体的に知ることが目的だからです。それに則り、各サービスに共通タスクを同様に与えていき、どの程度で手詰まりっぽくなったかをまとめるにとどめています。

### 使用した チャット系AIサービス

本検証では以下の 5 つのサービスを使用しました。前述した通り、すべて無料範囲での使用となります。

#### Chat-GPT (GPT-3.5)
https://chat.openai.com/

OpenAIが提供する、言わずと知れた覇権サービスです。今回はGPT3.5を使用しましたが、有料版ではすでにGPT-4turboまで公開されています。


#### Gemini (Gemini pro)
https://gemini.google.com/app

こちらはGoogleが提供しているサービスです。改名前のBardという名前のほうが馴染みあるかもしれません。

#### Bing Chat (Copilot)
https://www.bing.com/chat

Microsoftが提供するサービスです。OpenAIとの提携によりモデルはGPTが採用されています。Bing検索と連携しているなど、Chat-GPTには無い個性があるといえます。

#### ELYZA-japanese-Llama-2-70b
https://huggingface.co/spaces/elyza/ELYZA-japanese-Llama-2-13b-instruct-demo

株式会社ELYZAが開発した、日本語運用に特化したLLMです。無料体験版のチャットボットを使用しました。

#### Claude3 Haiku
https://www.anthropic.com/news/claude-3-family

冒頭でも触れた話題のサービスです。最高スペックのモデルはOpusですが、今回の検証では無料のHaikuを使用します。


### 検証Step1 骨組みを作る

ここから検証結果となります。基本的には入力内容と出力コードの実行結果をペアで載せていきます。また、すべての応答結果ではなく掻い摘んでいます。

最初に入力する質問はすべてのサービスに共通して、
```「2048」というゲームを知っていますか？```
です。どのサービスも卒なく回答します。原初の開発者に触れたり脳トレとして推奨したり、ちょっとずつ回答に個性が垣間見えて面白いです。

ゲーム「2048」の説明を引き出した後は共通して
```このゲームを作りたいです。HTML上で動作するようにコードを書いてください```
と入力します。するとこの時点でかなりの差が出ました。

まずはELYZAの出力を見てみましょう。
![image1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2730345/6999a78d-9fdd-27c5-2a2f-7aa944e81caa.png)

1段目の数字nに対応した2のn乗の表になってます。ちなみにスクリプトは記述されておらず操作は一切不能です。2048というゲームをうまく理解・表現できていないようです。最初の質問に回答した段階では4×4マスのゲームであると言及していましたが、ここから修正していくのは難儀しそうですね。

次にBing Chatを見てみます。
![image2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2730345/364304d5-de54-2ec5-50e3-6ff0205a9318.png)


見栄えは悪いものの、4×4マスの上に2や4のタイルをランダムポップさせるソースコードを出してくれました。現時点で操作は不能です。Claudeも同じような完成度で、あちらは初期のランダム配置に加えて左側への操作だけ有効でした。

偶然かもしれませんがChat-GPTがこの段階でとても個性的で優等生でした。
![image3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2730345/94e0ac88-c5cb-9709-e7b7-d435fe3858de.png)


このようにある程度見やすいタイルをデザインし、しかも4方向すべて操作が可能で合体も問題ない、つまり最低限2048を遊べるコードが一発で生成されました。
と、思っていたら
![image4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2730345/8b0aef0d-9ff5-6fed-e059-3a33e448edd7.png)


このように、タイルの改行設定が無かったので、ウィンドウサイズに合わせて絶望的なビジュアルになりました。要修正です。

一方Geminiは、タイルが常に横一列に並んだものを生成しました。2か4がランダムに出現する点はいいんですが、4×4に表示するのが難しいのかもしれません。

### 検証Step2 最低限の機能を実装する
次に、最初に生成したコードにて明らかに不足している、2048と呼ぶための最低限の機能を持たせてもらいます。表示を4×4にそろえ、上下左右にタイルを動かして数字を大きくしていくのが目標ラインです。
Step1の生成物を受けて、```表示をそろえてほしい```、```各方向への操作を実装してほしい```、などのようにプロンプトを入力しました。

まずここでELYZAとGeminiが脱落します。最初の生成物から改善されないのです。コードの何が問題なのかを自力で点検するのが難しいのかもしれません。

うまくいった例を見ます。下のものはChat-GPTです。
![image5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2730345/3be3d1bc-d775-46b5-c542-19af52e3053d.png)


色が単一なためプレイに難はありますが、機能的にはまず問題ないと言っていいでしょう。
同様にBing Chat、Claudeも、この段階でゲームとしてプレイが可能なコードを出力してくれました。

### 検証Step3 追加機能を要求する

ここから欲張っていきます。スコアの表示やリセットボタンの追加、ゲームオーバー判定、色彩調整などを試みます。

Chat-GPTの例がこちらです。
![image6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2730345/37e92fed-ee2d-2a6c-29f7-41010cc2ad30.png)


一見するとうまくいっていますが、キーボード操作を拾うスクリプトがなくなって、ゲームとして動かなくなってしまいました。その後修正を要求しても、操作に正しく応答することはなくなりました。コードが長くなったことで全機能を管理できなくなったようです。

続いてBing Chatをみます。
![image7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2730345/9747aa2e-61dc-be45-ab86-00f1b24e957f.png)


色がとても見やすくていい感じですね。
しかしここで手詰まりとなりました。この回答の末尾で同じような文を繰り返し続けて回答を終了しなくなり、様子を見ていたらリロードが入って会話のログが消えてしまったのです。Bing Chatは回答精度が高く参照したURLも表示してくれるのですが、Bingのリロードに伴ってログが流れてしまう欠点があります。

Claudeはどうでしょうか。
![image8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2730345/b0f97b17-541f-3552-a47d-22bef8884ba7.png)


色も追加機能も完璧です。
![image9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2730345/824e9a2d-3f97-64f7-7a05-ca479e9b6acf.png)


ゲームオーバーのポップアップも作ってくれました。素晴らしい出来栄えといえます。

さらに、ここまでの比較でClaudeの良さが光ってきました。Claude3 Haikuですが、とにかく回答が速いのです。文脈も常に理解していて、全体的にストレスフリーで使える印象でした。なんなら余力を感じます。

### 発展課題を与えてみたら
さて、余力を感じたのでもう少し発展的なことをClaudeに挑戦させてみます。

私はClaudeに、この2048をベースに新しいゲームをつくることを依頼しました。仕様として、タイルを3の累乗、マスを5×5に変更した上で、2つのタイルが合体するシステムから3つのタイルが合体するシステムへの改修を目指します。
完成したものがこちらです。
![image10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2730345/271be61e-ca29-c515-3821-af9bc7254eb8.png)

実はこのゲーム、かつてAppStoreでリリースされていた「729」というゲームなのです。「2048」が流行っていたときはこのような派生ゲームが多くみられ、数年のうちに消えていきました。その中でも私のお気に入りだったものを再現してもらいました。

画像は最終的に成功したアプリのものですが、ここに至るのは大変でした。2048と比べてオープンソースでないうえにアルゴリズムが複雑なので、思い通りの挙動がなかなか得られませんでした。
具体的には、過剰にタイルが消える、左は良いのに右は動かない、右に動かすとタイルの位置が左右反転する、などの問題です。
何より大変だったのが無料枠の上限です。長文のソースコードを出力させるのはトークン使用量が大きいらしく、わりとあっという間に上限に達してしまうのです。とはいえ半日程度時間を置くことで使用量が回復し、会話の履歴も保持されるため、時間さえかければ遂行できたのが不幸中の幸いでした。

ちなみにこれが仕上がるのに丸3日かかりました。

### 検証を終えた感想

雑感として、Claudeの使い勝手は頭一つ抜けているように思います。

本検証における性能面でいえばBing ChatとChat-GPTが次いでいましたが、Bingは常に返答に時間がかかり、Chat-GPTは回答が途中で切れます。Claudeは回答生成時間が極めて短いことに加え、回答が長くなるのを見越してか、コードの変更箇所だけを抜粋して回答することが多かったように思われます。

一方で、これはClaudeに限らないと思われますが、どうしても不便な点として

- デバッグが雰囲気で為され、平然と無意味なコード修正をして有意義そうに提出する
- 機能追加によって過去の機能が壊れることがある
- 変わったアルゴリズムは自力で書けない
- コーディング規則に完全な一貫性は持たせられない

などが挙げられます。とあるバグの原因が変数名の打ち間違い（hasMoved → hasMo**w**ed）によるフラグ管理失敗だったときは、AIもtypoするんかいっ！とツッコミを入れたくなりました。このミスもけっきょくClaudeが自力で発見して修正したのですが、同時に他の正常な関数も書き換えていてひやひやしました。

この検証で得た教訓として

- 仕様や不具合は具体的に伝える
- コードチェックと修正は短い範囲で行わせる
- 日を跨ぐ際は特にしっかりバックアップをとる

と学びました。これって人間のコーディングと同じなんじゃ......

なお、今回はシンプルな課題での検証となってますが、APIでの呼び出しやRAG機能の実装など、生成AIに持たせたい役割は多岐にわたり、それによってサービスの得手不得手も変わってきます。BingのWeb検索機能との連携などはわかりやすいものですね。

Claude3も、Amazon BedrockにてSonnetとHaikuが連携可能となってます。(参考：https://aws.amazon.com/jp/blogs/news/anthropics-claude-3-haiku-model-is-now-available-in-amazon-bedrock/)
使用目的に合わせてサービスやプラットフォームを選択していくことが重要であり、GPT一強と思われた頃よりも選択肢が増えたのは何よりうれしいことです。

### まとめ

各種チャット系AIサービスを使って、シンプルなゲーム作成タスクを通した比較検証を行ないました。
Claude3 Haikuはこのタスクにおいて目立って有用であり、個人的には高い評価を与えています。

一方で生成AIゆえに避けて通れない欠点や、サービスごとの個性も存在しているため、目的に合わせて慎重にサービスを選択することが重要といえるでしょう。


### おまけ
Claudeの検証にて得られた「729」のコードを配布します。私の青春のゲームをお手元で楽しんでみてください。
<details><summary>「729」のサンプルコード</summary>

```
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>729 Game</title>
  <style>
    #game-board {
      border: 1px solid #ccc;
      display: inline-grid;
      grid-template-columns: repeat(5, 80px);
      grid-template-rows: repeat(5, 80px);
      background-color: #cdc1b5;
      padding: 10px;
    }
    .tile {
      background-color: 1px solid #776e65;
      text-align: center;
      font-size: 40px;
      font-weight: bold;
      line-height: 80px;
      margin: 2px;
    }
    #score-display {
      font-size: 24px;
      margin-top: 10px;
    }
  </style>
</head>
<body>
  <div id="game-board"></div>
  <div id="score-display">Score: 0</div>
  <button id="reset-button">Reset</button>
  <script>
    const gameBoard = document.getElementById('game-board');
    const scoreDisplay = document.getElementById('score-display');
    const resetButton = document.getElementById('reset-button');
    const board = [
      [0, 0, 0, 0, 0],
      [0, 0, 0, 0, 0],
      [0, 0, 0, 0, 0],
      [0, 0, 0, 0, 0],
      [0, 0, 0, 0, 0]
    ];
    let score = 0;

    function createTile(value) {
      const tile = document.createElement('div');
      tile.className = 'tile';
      tile.textContent = value === 0 ? '' : value;
       // タイルの色を設定
       const tileColors = {
        3: '#eee4da',
        9: '#ede0c8',
        27: '#f2b179',
        81: '#f59563',
        243: '#f67c5f',
        729: '#f65e3b',
        2187: '#edcf72',
        6561: '#edcc61'
      };
      const color = tileColors[value] || '#776e65';
      tile.style.backgroundColor = color;
      tile.style.color = value > 4 ? '#f9f6f2' : '#776e65';

      return tile;
    }

    function drawBoard() {
      gameBoard.innerHTML = '';
      for (let i = 0; i < 5; i++) {
        for (let j = 0; j < 5; j++) {
          gameBoard.appendChild(createTile(board[i][j]));
        }
      }
    }
    function addRandomTile() {
      let emptyTiles = [];
      for (let i = 0; i < 5; i++) {
        for (let j = 0; j < 5; j++) {
          if (board[i][j] === 0) {
            emptyTiles.push({ x: j, y: i });
          }
        }
      }
      if (emptyTiles.length > 0) {
        const randomIndex = Math.floor(Math.random() * emptyTiles.length);
        const { x, y } = emptyTiles[randomIndex];
        board[y][x] = Math.random() < 0.9 ? 3 : 9;
      }
    }
    function moveLeft() {
  let hasMoved = false;
  for (let i = 0; i < 5; i++) {
    let row = board[i].filter(value => value !== 0);
    let merged = [];
    for (let j = 0; j < row.length; j++) {
      if (row[j] === row[j + 1] && row[j + 1] === row[j + 2]) {
        merged.push(row[j] * 3);
        score += row[j] * 3;
        j += 2;
      } else {
        merged.push(row[j]);
      }
    }
    merged = merged.concat(Array(5 - merged.length).fill(0));
    if (!hasMoved && JSON.stringify(board[i]) !== JSON.stringify(merged)) {
      hasMoved = true;
    }
    board[i] = merged;
  }
  if (hasMoved) {
    addRandomTile();
  }
}
function moveRight() {
  let hasMoved = false;
  for (let i = 0; i < 5; i++) {
    let row = board[i].filter(value => value !== 0);
    let merged = [];
    for (let j = row.length - 1; j >= 0; j--) {
      if (j >= 2 && row[j] === row[j - 1] && row[j - 1] === row[j - 2]) {
        merged.push(row[j] * 3);
        score += row[j] * 3;
        j -= 2;
      } else {
        merged.push(row[j]);
      }
    }
    merged = merged.concat(Array(5 - merged.length).fill(0));
    merged.reverse(); // ここで merged 配列を反転させる
    if (!hasMoved && JSON.stringify(board[i]) !== JSON.stringify(merged)) {
      hasMoved = true;
    }
    board[i] = merged;
  }
  if (hasMoved) {
    addRandomTile();
  }
}
function moveUp() {
  let hasMoved = false;
  for (let j = 0; j < 5; j++) {
    let column = board.map(row => row[j]).filter(value => value !== 0);
    let merged = [];
    for (let i = 0; i < column.length; i++) {
      if (column[i] === column[i + 1] && column[i + 1] === column[i + 2]) {
        merged.push(column[i] * 3);
        score += column[i] * 3;
        i += 2;
      } else {
        merged.push(column[i]);
      }
    }
    merged = merged.concat(Array(5 - merged.length).fill(0));
    if (!hasMoved) {
      const previousColumn = board.map(row => row[j]);
      if (JSON.stringify(previousColumn) !== JSON.stringify(merged)) {
        hasMoved = true;
      }
    }
    for (let i = 0; i < 5; i++) {
      board[i][j] = merged[i] || 0;
    }
  }
  if (hasMoved) {
    addRandomTile();
  }
}
function moveDown() {
  let hasMoved = false;
  for (let j = 0; j < 5; j++) {
    let column = board.map(row => row[j]).filter(value => value !== 0);
    let merged = [];
    for (let i = column.length - 1; i >= 0; i--) {
      if (i >= 2 && column[i] === column[i - 1] && column[i - 1] === column[i - 2]) {
        merged.push(column[i] * 3);
        score += column[i] * 3;
        i -= 2;
      } else {
        merged.push(column[i]);
      }
    }
    merged = merged.concat(Array(5 - merged.length).fill(0));
    merged.reverse();
    if (!hasMoved) {
      const previousColumn = board.map(row => row[j]);
      if (JSON.stringify(previousColumn) !== JSON.stringify(merged)) {
        hasMoved = true;
      }
    }
    for (let i = 0; i < 5; i++) {
      board[i][j] = merged[i] || 0;
    }
  }
  if (hasMoved) {
    addRandomTile();
  }
}
    addRandomTile();
    addRandomTile();
    drawBoard();
    updateScoreDisplay();

    document.addEventListener('keydown', event => {
      switch (event.key) {
        case 'ArrowLeft':
          if (!isGameOver()) {
            moveLeft();
            drawBoard();
            updateScoreDisplay();
          } else {
            handleGameOver();
          }
          break;
        case 'ArrowRight':
          if (!isGameOver()) {
            moveRight();
            drawBoard();
            updateScoreDisplay();
          } else {
            handleGameOver();
          }
          break;
        case 'ArrowUp':
          if (!isGameOver()) {
            moveUp();
            drawBoard();
            updateScoreDisplay();
          } else {
            handleGameOver();
          }
          break;
        case 'ArrowDown':
          if (!isGameOver()) {
            moveDown();
            drawBoard();
            updateScoreDisplay();
          } else {
            handleGameOver();
          }
          break;
      }
    });

    function updateScoreDisplay() {
      scoreDisplay.textContent = `Score: ${score}`;
    }

    resetButton.addEventListener('click', resetGame);
    function resetGame() {
      for (let i = 0; i < 5; i++) {
        for (let j = 0; j < 5; j++) {
          board[i][j] = 0;
        }
      }
      score = 0;
      addRandomTile();
      addRandomTile();
      drawBoard();
      updateScoreDisplay();
    }
    function handleGameOver() {
  if (isGameOver()) {
    const finalScore = score;
    const gameOverConfirm = confirm(`ゲームオーバー\n最終スコア: ${finalScore}\nゲームをリセットしますか?`);
    if (gameOverConfirm) {
      resetGame();
    }
  }
}
function isGameOver() {
  // 空きマスがあるか確認
  for (let i = 0; i < 5; i++) {
    for (let j = 0; j < 5; j++) {
      if (board[i][j] === 0) {
        return false;
      }
    }
  }
  // 3つのタイルが連続しているか確認
  for (let i = 0; i < 5; i++) {
    for (let j = 0; j < 3; j++) {
      if (board[i][j] === board[i][j + 1] && board[i][j + 1] === board[i][j + 2]) {
        return false;
      }
    }
  }
  for (let j = 0; j < 5; j++) {
    for (let i = 0; i < 3; i++) {
      if (board[i][j] === board[i + 1][j] && board[i + 1][j] === board[i + 2][j]) {
        return false;
      }
    }
  }
  return true;
}
  </script>
</body>
</html>
</details>