ChatGPTとClaudeの比較から学ぶ!数独ゲームの実装とAI活用事例

こちらの記事は、アソビュー! Advent Calendar 2024 の14日目(裏面)です。

こんにちは!

アソビューでウラカタチケットチームのエンジニアを担当しています、李と申します。

1. はじめに

アソビュー社内でも多くのエンジニアが生成系AIを日々の業務で活用しており、そのおかげで業務効率や利便性が着実に向上しています。ChatGPTの誕生以降、現在ではさまざまなAIプラットフォームが生まれていますが、その中でも特に強力な存在の一つが「Claude」です。

私自身、当初はChatGPTのみを使っていましたが、チーム内のメンバーからClaudeを勧められ、実際に試してみると、ChatGPTとは異なる独自の強みがあることに気づきました。今回は、日常業務での生成系AIの活用シーンを紹介しながら、二つのAIの特徴を比較してみたいと思います。 まず簡単に紹介しましょう。

2. ChatGPTとは

ChatGPTは、OpenAIが開発したAIチャットボットで、自然言語処理を用いて人間と対話するシステムです。多くの用途に対応しており、質問回答、文章作成、翻訳、要約、プログラミングサポートなど、幅広い分野で利用されています。


ChatGPTの主な特徴

  • 自然な会話:人間らしい自然な対話が可能です。
  • 多用途:教育、ビジネス、個人支援など、さまざまなシーンで役立ちます。
  • 高度な知識:最新の知識(カットオフ時点まで)に基づいて回答を生成します。
  • 多言語対応:複数の言語でコミュニケーションが可能です。

プランと提供されるサービス

プラン 内容 価格
無料プラン 基本的なChatGPTの機能を利用可能。応答速度やリクエスト回数に制限あり。 無料
ChatGPT Plus 高速な応答、より高いパフォーマンス、混雑時でも優先アクセスが可能。 月額20ドル

3. Claudeとは

Claudeは、Anthropicが開発したAIチャットボットおよび大規模言語モデル(LLM)です。名前の由来は、AI研究の歴史における功績者であるクロード・シャノン(Claude Shannon) から取られています。


Claudeの特徴:

  • 高い対話能力: 自然な会話を重視しており、長文理解や複雑な質問に対しても柔軟に対応します。指示理解の精度も高く、タスクの実行能力に優れています。
  • 安全性への配慮: Anthropicは「憲法AI(Constitutional AI)」というアプローチを採用し、安全性と倫理に配慮しながらAIをトレーニングしています。これにより、有害なコンテンツの生成を減らし、ユーザーにとって安全な体験を提供します。
  • ビジネスと個人の両方に対応: 単なる対話ツールとしてではなく、データ分析、コード生成、文章作成、アイデア出しなど、さまざまなビジネスシーンや個人利用にも役立ちます。

プランと提供されるサービス:

プラン 内容 価格
無料プラン 制限付きの無料アクセス
基本的な対話や短いタスクに対応
月間のリクエスト数に制限あり
無料
Claude Pro リクエストが優先的に処理されます
最大で数万トークンの入力と出力が可能
複雑なタスクや大規模なデータ処理にも対応
月額20ドル

4. プロジェクト作成

今回は、普段よく見られるシーンを想定し、Spring BootとReactを使用して「数独ゲーム」を作成する簡易プロジェクトのシミュレーションを行いました。

具体的には、ChatGPTとClaudeに対して、以下の要件を段階的に指示しながら、コード生成や改修を依頼する流れを試みました。


実現したい基本機能例:

  • 新規ゲーム開始・リセット・回答ボタン
  • 難易度選択
  • 盤面表示・入力機能
  • 回答チェック機能(誤答時はセルをハイライト)
  • UIの整形・枠線強調など

ChatGPTで挑戦

ChatGPTとのやり取りまとめ

  • フロントエンドUIの初期不備

    • 盤面が中央配置されない
    • 縦方向に余白が多い
    • 入力反応なし(クリック・キーボード不対応)
    • 新ゲーム開始・リセットボタンなし
    • 難易度選択なし
  • 修正要求を加えたが発生した問題

    • バックエンド処理が途中で無視され、React側でハードコーディングした状態に戻ってしまった
    • 太枠線が消えてしまうなど、一部修正による副作用が発生

イメージはこちらの通り

最終的なコード

SudokuController.java

import org.springframework.web.bind.annotation.*;
import org.springframework.http.ResponseEntity;

import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/api/sudoku")
public class SudokuController {

    @GetMapping("/new")
    public ResponseEntity<Map<String, int[][]>> generateSudoku(@RequestParam(defaultValue = "medium") String difficulty) {
        SudokuGenerator generator = new SudokuGenerator();
        int[][] puzzle = generator.generatePuzzle(difficulty); // 難易度を指定してパズルを生成
        int[][] solution = generator.getSolution(); // 解答を取得

        Map<String, int[][]> response = new HashMap<>();
        response.put("puzzle", puzzle);
        response.put("solution", solution);

        return ResponseEntity.ok(response);
    }
}

SudokuGenerator.java

import java.util.Random;

public class SudokuGenerator {
    private int[][] puzzle; // 問題
    private int[][] solution; // 解答
    private final Random random = new Random();

    public int[][] generatePuzzle(String difficulty) {
        // 完全解答の生成(バックトラッキングアルゴリズムなどを利用)
        solution = generateCompleteSolution();

        // 難易度に応じた空白セル数の決定
        int emptyCells;
        switch (difficulty.toLowerCase()) {
            case "easy":
                emptyCells = 30; // 初級: 30個の空白セル
                break;
            case "hard":
                emptyCells = 50; // 上級: 50個の空白セル
                break;
            case "medium":
            default:
                emptyCells = 40; // 中級: 40個の空白セル
                break;
        }

        // 空白セルを追加して問題を作成
        puzzle = createPuzzleWithEmptyCells(solution, emptyCells);

        return puzzle;
    }

    private int[][] generateCompleteSolution() {
        // 完全解答を生成するロジック(バックトラッキングの実装)
        // サンプルとして全て0のグリッドを返します(実際の実装は適宜補完してください)
        int[][] grid = new int[9][9];
        solveSudoku(grid);
        return grid;
    }

    private boolean solveSudoku(int[][] grid) {
        // 数独解答を生成するためのバックトラッキングアルゴリズム
        // 実装例: https://en.wikipedia.org/wiki/Sudoku_solving_algorithms
        for (int row = 0; row < 9; row++) {
            for (int col = 0; col < 9; col++) {
                if (grid[row][col] == 0) {
                    for (int num = 1; num <= 9; num++) {
                        if (isValid(grid, row, col, num)) {
                            grid[row][col] = num;
                            if (solveSudoku(grid)) {
                                return true;
                            }
                            grid[row][col] = 0;
                        }
                    }
                    return false;
                }
            }
        }
        return true;
    }

    private boolean isValid(int[][] grid, int row, int col, int num) {
        // 行・列・3x3の枠内で重複がないか確認
        for (int x = 0; x < 9; x++) {
            if (grid[row][x] == num || grid[x][col] == num || grid[row / 3 * 3 + x / 3][col / 3 * 3 + x % 3] == num) {
                return false;
            }
        }
        return true;
    }

    private int[][] createPuzzleWithEmptyCells(int[][] solution, int emptyCells) {
        // 解答をコピー
        int[][] puzzle = new int[9][9];
        for (int i = 0; i < 9; i++) {
            System.arraycopy(solution[i], 0, puzzle[i], 0, 9);
        }

        // 指定された数の空白セルをランダムに作成
        for (int i = 0; i < emptyCells; i++) {
            int row, col;
            do {
                row = random.nextInt(9);
                col = random.nextInt(9);
            } while (puzzle[row][col] == 0);
            puzzle[row][col] = 0;
        }

        return puzzle;
    }

    public int[][] getSolution() {
        return solution;
    }
}

WebConfig.java

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig {
    @Bean
    public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurer() {

            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/api/**").allowedOrigins("http://localhost:3000");
            }
        };
    }
}

App.js

import React, { useState, useEffect } from "react";

const App = () => {
    const [sudoku, setSudoku] = useState([]);
    const [initialSudoku, setInitialSudoku] = useState([]);
    const [solution, setSolution] = useState([]);
    const [incorrectCells, setIncorrectCells] = useState([]);
    const [isLoading, setIsLoading] = useState(true);
    const [difficulty, setDifficulty] = useState("medium");

    const fetchSudoku = async () => {
        try {
            setIsLoading(true);
            const response = await fetch(`http://localhost:8080/api/sudoku/new?difficulty=${difficulty}`);
            const data = await response.json();
            setSudoku(data.puzzle);
            setSolution(data.solution);
            setInitialSudoku(data.puzzle);
            setIncorrectCells([]);
            setIsLoading(false);
        } catch (error) {
            console.error("数独データの取得中にエラーが発生しました:", error);
            setIsLoading(false);
        }
    };

    useEffect(() => {
        fetchSudoku();
    }, []);

    const handleChange = (row, col, value) => {
        if (value === "" || /^[1-9]$/.test(value)) {
            const updatedSudoku = sudoku.map((r, i) =>
                r.map((num, j) => (i === row && j === col ? (value === "" ? 0 : parseInt(value)) : num))
            );
            setSudoku(updatedSudoku);
            setIncorrectCells([]);
        }
    };

    const handleReset = () => {
        setSudoku(initialSudoku);
        setIncorrectCells([]);
    };

    const handleNewGame = () => {
        fetchSudoku();
    };

    const handleCheckAnswer = () => {
        const incorrect = [];
        sudoku.forEach((row, rowIndex) => {
            row.forEach((num, colIndex) => {
                if (num !== 0 && num !== solution[rowIndex][colIndex]) {
                    incorrect.push([rowIndex, colIndex]);
                }
            });
        });
        setIncorrectCells(incorrect);
        if (incorrect.length === 0) {
            alert("おめでとうございます!全て正解です!");
        }
    };

    const handleDifficultyChange = (event) => {
        setDifficulty(event.target.value);
    };

    const styles = {
        board: {
            display: "grid",
            gridTemplateColumns: "repeat(9, 40px)",
            gridTemplateRows: "repeat(9, 40px)",
            gap: "0",
            border: "3px solid black",
            margin: "20px auto",
            width: "fit-content",
        },
        cell: (rowIndex, colIndex) => ({
            width: "40px",
            height: "40px",
            textAlign: "center",
            lineHeight: "40px",
            fontSize: "18px",
            border: "1px solid #ccc",
            outline: "none",
            boxSizing: "border-box",
            backgroundColor:
                initialSudoku[rowIndex][colIndex] !== 0 ? "#f0f0f0" : "#fff",
            fontWeight: initialSudoku[rowIndex][colIndex] !== 0 ? "bold" : "normal",
            background: incorrectCells.some(
                ([i, j]) => i === rowIndex && j === colIndex
            )
                ? "#f8d7da"
                : initialSudoku[rowIndex][colIndex] !== 0
                    ? "#f0f0f0"
                    : "#fff",
            borderTop: rowIndex % 3 === 0 ? "2px solid black" : "1px solid #ccc",
            borderLeft: colIndex % 3 === 0 ? "2px solid black" : "1px solid #ccc",
            borderBottom: rowIndex === 8 ? "2px solid black" : "",
            borderRight: colIndex === 8 ? "2px solid black" : "",
        }),
        buttonContainer: {
            display: "flex",
            justifyContent: "center",
            gap: "10px",
            marginTop: "20px",
        },
        button: {
            padding: "10px 20px",
            fontSize: "16px",
            backgroundColor: "#007bff",
            color: "#fff",
            border: "none",
            borderRadius: "5px",
            cursor: "pointer",
        },
        difficultySelector: {
            margin: "10px 0",
            textAlign: "center",
        },
        loading: {
            fontSize: "18px",
            textAlign: "center",
            marginTop: "50px",
        },
    };

    if (isLoading) {
        return <div style={styles.loading}>Loading...</div>;
    }

    return (
        <div style={{ textAlign: "center", padding: "20px" }}>
            <h1>Sudoku Game</h1>
            <div style={styles.difficultySelector}>
                <label>
                    難易度:
                    <select value={difficulty} onChange={handleDifficultyChange}>
                        <option value="easy">Easy</option>
                        <option value="medium">Medium</option>
                        <option value="hard">Hard</option>
                    </select>
                </label>
                <button onClick={handleNewGame} style={{ ...styles.button, marginLeft: "10px" }}>
                    New Game
                </button>
            </div>
            <div style={styles.board}>
                {sudoku.map((row, rowIndex) =>
                    row.map((num, colIndex) => (
                        <input
                            key={`${rowIndex}-${colIndex}`}
                            type="text"
                            value={num === 0 ? "" : num}
                            onChange={(e) => handleChange(rowIndex, colIndex, e.target.value)}
                            readOnly={initialSudoku[rowIndex][colIndex] !== 0}
                            style={styles.cell(rowIndex, colIndex)}
                        />
                    ))
                )}
            </div>
            <div style={styles.buttonContainer}>
                <button onClick={handleReset} style={styles.button}>
                    Reset
                </button>
                <button onClick={handleCheckAnswer} style={styles.button}>
                    Check Answer
                </button>
            </div>
        </div>
    );
};

export default App;

完成品

感想

  • 得意な点:

    • 無料でも大半の機能を概ね実装可能
    • 明示的な指示に忠実に対応する
  • 不得意な点:

    • 一度行った修正を忘れる、または巻き戻してしまう場面がある
    • 要求を厳密に伝えないと想定外の実装をすることがある

Claudeで挑戦

Claudeとのやり取りまとめ

  • 初期コード生成と特徴
    • サービス層やController層など適切なアーキテクチャを提示
    • 想定機能(難易度調整・新ゲームボタンなど)をこちらから明示せずともある程度想定してコードに反映
  • 発生した問題
    • 存在しない依存関係(spring-boot-starter-cors)を追加しようとした
    • APIへの接続URLが不適切でデータ取得に失敗するケースあり
  • 不備への対応
    • 回答時の誤りを正すなど、問い合わせれば柔軟に対応
    • 最終的にはメモ機能追加など追加要件にも柔軟に対応

イメージはこちらの通り

最終的なコード

SudokuController.java

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/sudoku")
public class SudokuController {
    @Autowired
    private SudokuGeneratorService sudokuGeneratorService;

    @GetMapping("/generate")
    public SudokuDTO generateSudoku(@RequestParam(defaultValue = "2") int difficulty) {
        return new SudokuDTO(sudokuGeneratorService.generateSudokuPuzzle(difficulty));
    }
}

SudokuDTO.java

public class SudokuDTO {

    private int[][] grid;

    public SudokuDTO() {}

    public SudokuDTO(int[][] grid) {
        this.grid = grid;
    }

    public int[][] getGrid() {
        return grid;
    }

    public void setGrid(int[][] grid) {
        this.grid = grid;
    }
}

SudokuGeneratorService.java

import org.springframework.stereotype.Service;
import java.util.Random;

@Service
public class SudokuGeneratorService {
    private static final int GRID_SIZE = 9;

    public int[][] generateSudokuPuzzle(int difficulty) {
        int[][] grid = new int[GRID_SIZE][GRID_SIZE];
        fillDiagonalBoxes(grid);
        solveGrid(grid);
        removeNumbers(grid, difficulty);
        return grid;
    }

    private void fillDiagonalBoxes(int[][] grid) {
        for (int i = 0; i < GRID_SIZE; i += 3) {
            fillBox(grid, i, i);
        }
    }

    private void fillBox(int[][] grid, int row, int col) {
        Random random = new Random();
        for (int i = 0; i < 3; i++) {
            for (int j = 0; j < 3; j++) {
                while (true) {
                    int num = random.nextInt(9) + 1;
                    if (isValidBoxPlacement(grid, row, col, num)) {
                        grid[row + i][col + j] = num;
                        break;
                    }
                }
            }
        }
    }

    private boolean isValidBoxPlacement(int[][] grid, int boxStartRow, int boxStartCol, int num) {
        for (int i = 0; i < 3; i++) {
            for (int j = 0; j < 3; j++) {
                if (grid[boxStartRow + i][boxStartCol + j] == num) {
                    return false;
                }
            }
        }
        return true;
    }

    private boolean solveGrid(int[][] grid) {
        for (int row = 0; row < GRID_SIZE; row++) {
            for (int col = 0; col < GRID_SIZE; col++) {
                if (grid[row][col] == 0) {
                    for (int num = 1; num <= 9; num++) {
                        if (isValidPlacement(grid, row, col, num)) {
                            grid[row][col] = num;
                            if (solveGrid(grid)) {
                                return true;
                            }
                            grid[row][col] = 0;
                        }
                    }
                    return false;
                }
            }
        }
        return true;
    }

    private boolean isValidPlacement(int[][] grid, int row, int col, int num) {
        // Check row
        for (int i = 0; i < GRID_SIZE; i++) {
            if (grid[row][i] == num) {
                return false;
            }
        }

        // Check column
        for (int i = 0; i < GRID_SIZE; i++) {
            if (grid[i][col] == num) {
                return false;
            }
        }

        // Check 3x3 box
        int boxRowStart = row - row % 3;
        int boxColStart = col - col % 3;
        for (int i = boxRowStart; i < boxRowStart + 3; i++) {
            for (int j = boxColStart; j < boxColStart + 3; j++) {
                if (grid[i][j] == num) {
                    return false;
                }
            }
        }

        return true;
    }

    private void removeNumbers(int[][] grid, int difficulty) {
        int cellsToRemove = 0;
        switch (difficulty) {
            case 1: // Easy
                cellsToRemove = 20;
                break;
            case 2: // Medium
                cellsToRemove = 40;
                break;
            case 3: // Hard
                cellsToRemove = 60;
                break;
            default:
                cellsToRemove = 30;
        }

        Random random = new Random();
        while (cellsToRemove > 0) {
            int row = random.nextInt(GRID_SIZE);
            int col = random.nextInt(GRID_SIZE);
            if (grid[row][col] != 0) {
                grid[row][col] = 0;
                cellsToRemove--;
            }
        }
    }
}

WebConfig.java

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
                .allowedOrigins("http://localhost:3000")
                .allowedMethods("GET", "POST", "PUT", "DELETE")
                .allowedHeaders("*");
    }
}

App.js

import React, { useState, useEffect } from 'react';
import axios from 'axios';

const SudokuGame = () => {
  const [sudokuGrid, setSudokuGrid] = useState(Array(9).fill().map(() => Array(9).fill(0)));
  const [difficulty, setDifficulty] = useState(2);
  const [editableGrid, setEditableGrid] = useState(Array(9).fill().map(() => Array(9).fill(false)));

  useEffect(() => {
    generateNewPuzzle();
  }, [difficulty]);

  const generateNewPuzzle = async () => {
    try {
      const response = await axios.get('http://localhost:8080/api/sudoku/generate', {
        params: { difficulty }
      });
      setSudokuGrid(response.data.grid);
      setEditableGrid(response.data.grid.map(row => row.map(cell => cell === 0)));
    } catch (error) {
      console.error('Error generating sudoku', error);
    }
  };

  const handleCellChange = (rowIndex, colIndex, value) => {
    if (editableGrid[rowIndex][colIndex]) {
      const newGrid = [...sudokuGrid];
      newGrid[rowIndex][colIndex] = value === '' ? 0 : parseInt(value);
      setSudokuGrid(newGrid);
    }
  };

  const isValidInput = (value) => {
    return value === '' || (parseInt(value) >= 1 && parseInt(value) <= 9);
  };

  const resetGame = () => {
    generateNewPuzzle();
  };

  const checkSolution = () => {
    const solution = [...sudokuGrid];
    if (isSolutionValid(solution)) {
      alert('Congratulations! You solved the puzzle correctly.');
    } else {
      alert('Sorry, the solution is not correct. Please try again.');
    }
  };

  const isSolutionValid = (grid) => {
    for (let row = 0; row < 9; row++) {
      for (let col = 0; col < 9; col++) {
        if (!isValidPlacement(grid, row, col, grid[row][col])) {
          return false;
        }
      }
    }
    return true;
  };

  const isValidPlacement = (grid, row, col, num) => {
    // Check row
    for (let i = 0; i < 9; i++) {
      if (grid[row][i] === num && col !== i) {
        return false;
      }
    }

    // Check column
    for (let i = 0; i < 9; i++) {
      if (grid[i][col] === num && row !== i) {
        return false;
      }
    }

    // Check 3x3 box
    const boxRowStart = Math.floor(row / 3) * 3;
    const boxColStart = Math.floor(col / 3) * 3;
    for (let i = boxRowStart; i < boxRowStart + 3; i++) {
      for (let j = boxColStart; j < boxColStart + 3; j++) {
        if (grid[i][j] === num && (i !== row || j !== col)) {
          return false;
        }
      }
    }

    return true;
  };

  return (
      <div className="sudoku-container">
        <h1>数独ゲーム</h1>
        <div className="difficulty-selector">
          <label>難易度: </label>
          <select
              value={difficulty}
              onChange={(e) => setDifficulty(parseInt(e.target.value))}
          >
            <option value={1}>簡単</option>
            <option value={2}>普通</option>
            <option value={3}>難しい</option>
          </select>
          <button onClick={generateNewPuzzle}>新しいパズル</button>
          <button onClick={resetGame}>リセット</button>
          <button onClick={checkSolution}>回答</button>
        </div>
        <div className="sudoku-grid">
          {sudokuGrid.map((row, rowIndex) => (
              <div key={rowIndex} className="sudoku-row">
                {row.map((cell, colIndex) => (
                    <input
                        key={colIndex}
                        type="text"
                        maxLength="1"
                        value={cell === 0 ? '' : cell}
                        onChange={(e) => {
                          if (isValidInput(e.target.value)) {
                            handleCellChange(rowIndex, colIndex, e.target.value);
                          }
                        }}
                        className={`sudoku-cell 
                  ${editableGrid[rowIndex][colIndex] ? '' : 'pre-filled'}
                  ${Math.floor(rowIndex / 3) % 2 === Math.floor(colIndex / 3) % 2
                            ? 'cell-light'
                            : 'cell-dark'
                        }
                  ${(rowIndex + 1) % 3 === 0 ? 'border-bottom' : ''}
                  ${(colIndex + 1) % 3 === 0 ? 'border-right' : ''}
                `}
                        disabled={!editableGrid[rowIndex][colIndex]}
                    />
                ))}
              </div>
          ))}
        </div>
      </div>
  );
};

export default SudokuGame;

App.css

.sudoku-container {
  display: flex;
  flex-direction: column;
  align-items: center;
  font-family: Arial, sans-serif;
  padding: 20px;
}

.difficulty-selector {
  margin-bottom: 20px;
  display: flex;
  align-items: center;
  gap: 10px;
}

.difficulty-selector button {
  padding: 8px 16px;
  background-color: #007bff;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.difficulty-selector button:hover {
  background-color: #0056b3;
}

.difficulty-selector select {
  padding: 8px;
  border-radius: 4px;
  border: 1px solid #ccc;
}

.sudoku-grid {
  display: flex;
  flex-direction: column;
  border: 3px solid #333;
}

.sudoku-row {
  display: flex;
}

.sudoku-cell {
  width: 50px;
  height: 50px;
  text-align: center;
  font-size: 20px;
  border: 1px solid #999;
  background-color: #fff;
  cursor: pointer;
  padding: 0;
}

.cell-light {
  background-color: #f8f9fa;
}

.cell-dark {
  background-color: #e9ecef;
}

.pre-filled {
  background-color: #dee2e6;
  font-weight: bold;
  color: #495057;
}

.sudoku-cell:focus {
  outline: 2px solid #007bff;
  z-index: 1;
}

.border-bottom {
  border-bottom: 2px solid #333;
}

.border-right {
  border-right: 2px solid #333;
}

.sudoku-cell.invalid {
  background-color: #ffcdd2;
}

.sudoku-cell:disabled {
  cursor: not-allowed;
}

完成品

感想

  • 得意な点:
    • 要件が曖昧でも最適な機能を提案
    • より自然なプロダクト指向の実装
  • 不得意な点:
    • 無料プランでは会話の長さや対応の継続に制限があり
    • 前提知識がない初心者には敷居が高く、トラブルシューティングに時間がかかる

5. コードレビュー

普段AIを使用する際の非常に一般的なシーンとして、他のメンバーのコードをレビューするというものがあります。そこで、GPTとClaudeにお互いのコードをクロスレビューしてもらって、それぞれが異なる観点で改善点を指摘しました。

ChatGPT -> Claudeのコード

  • 入力・定数管理の明確化:難易度定義や入力値チェックを別ファイル化し、保守性を向上すべきと提案。
  • 回答一意性チェック:数独の解答が一意かどうかを判定する仕組みの強化を推奨。
  • コード共通化・再利用性:重複する処理を関数化し、メンテナンス性を改善。
  • セキュリティ面の考慮:CORSや入力バリデーションなど、APIの堅牢性を高める指摘。

Claude -> ChatGPTのコード

  • パラメータ検証の強化:不正な入力値や範囲外の難易度を受けた場合の適切なエラーハンドリングを推奨。
  • 例外処理・UI改善:エラー時の情報提供やモバイル対応など、ユーザビリティ向上の提案。
  • 機能拡張の示唆:キャッシュや統計、プロフィール管理など、付加価値ある機能を考慮。

総合的な所見

両者とも、現行コードを「より実務的に強固なもの」にする視点を提示しました。ChatGPTは内的品質(構造化、セキュリティ、再利用性)強化を、Claudeはユーザー体験や拡張性に焦点を当てました。相互の提案を組み合わせれば、より完成度が高い数独アプリケーションへと進化させることが可能です。

6. まとめ

日常業務でよく使われる「要件からのコード生成」および「コードレビュー」の観点から見た違い

ChatGPT

Good

  • プログラム全体の完成
    たとえ無料版であっても、プログラムのフレームワークを構築し、正常に動作させることが可能です。
  • 質問の長さに制限なし
    初心者でも与えられた指示に従うだけでプログラムを完成させられます。

More

  • 前の内容を忘れることがある
    既に完成した内容が再度修正されてしまうことがあり、指摘と修正に手間がかかることがあります。
  • 受動的な傾向
    ユーザーから明示的な指示がない限り、合理的・必須な機能でも実装されないことがあります。

Claude

Good

  • 高い理解力
    曖昧な要件でも意図を正確に理解し、合理的な機能を追加することで効率的に製品を完成へ導きます。

More

  • 会話の長さに制限
    無料プランでは1つの会話で製品を完成させるのが難しい場合があります。
  • ミスが発生する
    時間と労力をかけて修正する必要が出ることがあります。
  • 一定の知識が前提
    Claudeはユーザーに一定の知識があると仮定しているため、初心者にとってはプログラム全体の構築が難しいかもしれません。

まとめ

  • 有料プランを使用しない場合
    ChatGPT が最適な選択肢です。無料版でも十分な性能を発揮し、初心者でも安心して利用できます。
  • 基礎知識があり、効率的に要件を実現したい場合
    Claude が適しています。曖昧な要件でも高い理解力を発揮し、合理的な機能追加で効率を向上させます。

今回は軽く比較をしますが、今後とも、ChatGPTとClaudeの比較検証は続けていきたいと思いますので、興味ある方はぜひご覧いただけますと幸いです。

PR

アソビューではより良いプロダクトを素早く世の中に届けられるよう、様々な挑戦を続けています。 私達と一緒に働くエンジニアを募集していますので、興味のある方はぜひお気軽にエントリーください!