ChatGPTで仕様に沿ったコードレビューをしてくれる GPTを作成してみた

この記事はレコチョク [Advent Calendar 2023](https://qiita.com/advent-calendar/2023/recochoku)の5日目の記事となります。

# はじめに
株式会社レコチョクでエンジニアをしている山本です。
主にサーバーサイドを担当しております。
好きなバンドは凛として時雨、好きなゲームはApex Legendsです。
よろしくお願いいたします！

弊社では、今年度より生成AIの積極的な活用による「with AI プロジェクト」を発足しております。
[詳細はこちらから](https://recochoku.jp/corporate/news/20230630-withai-project/)

AIを積極的に使うようになってから、何か効率化できないかということを日々考えながら過ごしております。

今回はChatGPTに先月11月に追加された機能「GPTs」について触れていきたいと思います。

# GPTsとは
GPTsとはChatGPTのカスタム版をノーコードで作成できる機能です。
作りたい題材を決め、ChatGPTと会話をするだけで自分だけのGPTを作れるようになっております。
GPTsには独自データを学習させる機能が備わっており、独自データを学習させることでよりオリジナルなGPTの作成もできます。
作成したGPTは公開/非公開の設定ができ、公開するとほかのユーザも使うことができ、一部の人だけに公開することも可能です。

# 今回作成するGPTについて
今回はTODOアプリAPIのコードレビューを行ってくれるGPTを作成していきます。
こちらのGPTでは、API仕様書とDBのデータ定義書をあらかじめ決めておき、これらの情報をChatGPTに学習させておきます。
このようにすることで、ただコードのレビューを行ってくれるだけでなく、仕様通りの設計になっているかまで確認を行ってくれるようになります。

# 今回作成するTODOアプリAPIについて
今回はGPTsがどのようにコードレビューを行ってくれるのかを確認したいだけのため、全APIを作らず簡単なタスク一覧の取得APIのみを作成します。
言語はPHP、フレームワークはLaravelを使用して作成していきます。

# 仕様決め
まずはAPI仕様書とDBのデータ定義書を作成していきます。
以下が今回作成する仕様の内容となります。
※.yamlや.ddlファイルだとChatGPT側で学習されなかったためtxtファイルで記載しております。

API仕様書(api.txt)
```
openapi: 3.0.0
info:
  title: TODO App API
  version: 1.0.0
paths:
  /api/tasks:
    get:
      summary: タスク一覧の取得
      description: タスク一覧を取得します。
      parameters:
        - in: query
          name: title
          schema:
            type: string
          description: タイトル
        - in: query
          name: is_completed
          schema:
            type: boolean
          description: 完了かどうか
        - in: query
          name: sort_by
          schema:
            type: string
            enum: [due_date, is_completed]
          description: ソートするカラム
        - in: query
          name: sort_direction
          schema:
            type: string
            enum: [asc, desc]
          description: ソートの方向
      responses:
        '200':
          description: 成功
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: integer
                  title:
                    type: string
                  description:
                    type: string
                  is_completed:
                    type: boolean
                  due_date:
                    type: string
                    format: date
        '422':
          description: バリデーションエラー
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
        '500':
          description: サーバーエラー
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
```
データ定義書(ddl.txt)
```
CREATE TABLE tasks (
    id INT AUTO_INCREMENT PRIMARY KEY,
    title VARCHAR(255) NOT NULL,
    description TEXT,
    is_completed BOOLEAN NOT NULL DEFAULT FALSE,
    due_date DATE
);
```

# 実際にGPTを作成していく
ChatGPTのこちらの画面から作成を行っていきます。
左側でChatGPTと会話しながら作成する内容を決めていき、右側で実際に作成されたGPTを試すことができます。
![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3626109/68ce1420-698d-0a07-89f7-9c278532113d.png)

初めに以下のように作成する内容をChatGPTに投げます。
今回はChatGPTに追加で仕様書を学習させたいので、上記で作成したファイルを添付しています。
![2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3626109/1b22d6a0-cf6d-e778-d7e8-780dd60403dc.png)

するとChatGPT側で作成する内容に沿った GPT名とプロフ画像を作成してくれます。
そこから細かい調整をChatGPTと会話を進めながら作成していきます。
基本的にはどこに重点を置くか、ユーザにどのようなアプローチで接するかという点などの質問がくるのでそれに答えていく形になります。
![3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3626109/0fd6c39e-325f-863a-146a-c63f8848b5c5.png)

これで GPTの作成が完了しました。
追加で何か考慮して欲しいことがあればさらにChatGPT側に投げかけることで追加できます。
このようにノーコードで簡単に作成を行うことができます。

# 作成したGPTを試してみる
実際に作成した GPTを試してみます。
まずLaravelで作成したTODOアプリAPIの内容をプロンプトに記載します。
仕様書に沿っていなければ指摘してくれるか試すために、テーブルのカラム(description)とリクエストパラメータ(is_completed)とエラーハンドリングを欠落した内容にします。
プロンプト内容は以下になっております。
```
タスク一覧APIのコードレビューをお願いします。

# /routes/api.php
<?php

use App\Http\Controllers\Api\TaskController;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

Route::get('/tasks', [TaskController::class, 'index']);

# /database/migrations/2023_12_01_000000_create_tasks_table.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('tasks', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->date('due_date')->nullable();
            $table->boolean('is_completed')->default(false);
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('tasks');
    }
};

# /app/Models/Task.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Task extends Model
{
    use HasFactory;

    protected $fillable = ['title', 'is_completed', 'due_date'];
}

# /app/Http/Controllers/Api/TaskController.php
<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Models\Task;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;

class TaskController extends Controller
{
    public function index(Request $request)
    {
        try {
            $validator = Validator::make($request->all(), [
                'title' => 'string|max:255|nullable',
                'sort_by' => 'in:due_date,is_completed',
                'sort_direction' => 'in:asc,desc',
            ]);

            $query = Task::query();

            $query->when($request->has('title'), function ($q) use ($request) {
                $q->where('title', 'like', '%' . $request->input('title') . '%');
            })
            ->when($request->has('sort_by'), function ($q) use ($request) {
                $q->orderBy($request->input('sort_by'), $request->input('sort_direction', 'asc'));
            });

            return response()->json($query->get());
        } catch (\Exception $e) {
            return response()->json(['error' => 'Internal Server Error'], 500);
        }
    }
}
```
上記のプロンプトを投げた結果が以下となります。
![4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3626109/63fb34cf-5d2e-afc7-a466-a661f6533fe7.png)

内容を確認すると欠落した内容を指摘してくれており、改善点が詳細に書かれております。
こちらの内容をもとに修正を行ってみます。
修正した内容をプロンプトに記載しました。
```
修正しました。

# /routes/api.php
<?php

use App\Http\Controllers\Api\TaskController;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

Route::get('/tasks', [TaskController::class, 'index']);

# /database/migrations/2023_12_01_000000_create_tasks_table.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('tasks', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->text('description')->nullable();
            $table->date('due_date')->nullable();
            $table->boolean('is_completed')->default(false);
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('tasks');
    }
};

# /app/Models/Task.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Task extends Model
{
    use HasFactory;

    protected $fillable = ['title', 'description', 'is_completed', 'due_date'];
}

# /app/Http/Controllers/Api/TaskController.php
<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Models\Task;
use Illuminate\Http\Request;
use Illuminate\Database\QueryException;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;

class TaskController extends Controller
{
    public function index(Request $request)
    {
        try {
            $validator = Validator::make($request->all(), [
                'title' => 'string|max:255|nullable',
                'is_completed' => 'boolean|nullable',
                'sort_by' => 'in:due_date,is_completed',
                'sort_direction' => 'in:asc,desc',
            ]);

            if ($validator->fails()) {
                throw new ValidationException($validator);
            }

            $query = Task::query();

            $query->when($request->has('title'), function ($q) use ($request) {
                $q->where('title', 'like', '%' . $request->input('title') . '%');
            })
            ->when($request->has('is_completed'), function ($q) use ($request) {
                $q->where('is_completed', $request->input('is_completed'));
            })
            ->when($request->has('sort_by'), function ($q) use ($request) {
                $q->orderBy($request->input('sort_by'), $request->input('sort_direction', 'asc'));
            });

            return response()->json($query->get());
        } catch (ValidationException $e) {
            Log::error($e->errors());
            return response()->json(['error' => $e->getMessage()], 422);
        } catch (QueryException $e) {
            Log::error($e->getMessage());
            return response()->json(['error' => 'DB Error'], 500);
        } catch (\Exception $e) {
            Log::error($e->getMessage());
            return response()->json(['error' => 'Internal Server Error'], 500);
        }
    }
}
```
上記の修正を行ったプロンプトを投げた結果が以下となります。
![5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3626109/67474190-d41e-bca4-9f0d-a996091a357d.png)

適切にコーディングを行った際は、良い評価をしてくれていることが確認ができるかと思います。
今回はプロンプトにコードを記載しましたが、phpファイル自体を添付してレビューも行うことが可能です。

# まとめ
ChatGPTで仕様に沿ったコードレビューをしてくれる GPTの作成を行ってみました。
ChatGPTに仕様を学習させることで、一人でも仕様通りの適切なコーディングが行えるようになりました。
今回作成したGPTは私個人の利用のみに留まりましたが、 GPTはチーム内にも共有することができ、例えば以下のようなことにも活用できる気がしています。
・仕様書や要件定義書などの資料を学習させた GPTをチーム内で共有を行い、コーディングの補佐を行う。
・新人研修の課題を GPTに学習させ共有を行い、一次レビューはChatGPTを使ってもらうことで指導係の負担を減らす。
このような使い方をすることで業務の効率化になるのではと思っております。

学習させる内容や GPTの細かな調整などは模索中となっておりますので、色々試していければと思います。

最後まで読んでいただきありがとうございました。

明日のレコチョク   [Advent Calendar 2023](https://qiita.com/advent-calendar/2023/recochoku)は6日目 「FastAPI + Assistants API で英単語一問一答チャットボットを作ってみた」となります。お楽しみに！

***
この記事はレコチョクのエンジニアブログの記事を転載したものとなります。

https://techblog.recochoku.jp/9856