2023.01.16

GitHub Copilot にいいコードを書いてもらう方法

D.M.です。
AI とともにプログラミングをしてみた体験記です。

モチベーション

2022年11月、 ChatGPT が登場したことにより、今の学生は AI にレポートを書かせるというような話が普通に出るご時世になりました。 ChatGPT は例えばプログラミングのお題を投げると AI がかなり高精度なプログラムを書いてレスポンスしてくれたりします。この技術は将来的に Google 検索を脅かす存在になるのではというほどの注目を集めています。

ペアプログラマー Github Copilot

よりプログラミングに特化した AI サービスとして、 GitHub Copilot があります(ギットハブ コパイロットと読む)。
2022年6月に正式リリースされています。
このツールには以下のような特徴があります。

・ソースの流れやコメントに合わせて次に書くべきコードをサジェストしてくれる。
・VS Code などの IDE とプラグインで連携できる。
・ChatGPT と同じく裏側に GPT ベースの AI を利用( GitHub は MS 傘下で、この GitHub Copilot も OpenAI の技術を採用)。AIの学習の元ネタはGitHub上に公開されているソースコード。
・有料(月額10ドル)。無料トライアル60日あり。

GitHub Copilot は優秀なペアプログラマーのように位置づけられており、生産性の向上効果は高く喧伝されています。
公式サイトではある実装課題に対して2倍以上の速度改善効果があったと報告しています。

JSでWebサーバの処理を書いてもらう実験課題
        被験者数  課題完了率   完了時間
Copilotなし  50人    70%      2時間41分
Copilotあり  45人    78%      1時間11分
https://github.blog/jp/2022-09-15-research-quantifying-github-copilots-impact-on-developer-productivity-and-happiness/

GitHub Copilot の詳細についてはググると様々な記事がヒットします。
まだよくご存じない方は、まずは公式ページをご覧ください。
https://github.com/features/copilot

よし使ってみよう!となりました。

このブログでやりたいこと

「初めて NestJS を使いながらログイン画面のプログラミングをする。」

今回は GitHub Copilot を使いながら1つのシステムを構築してみて役に立った部分を書いていこうと思います。せっかくなので自分が知らない新しいフレームワークを勉強しながらやってみます。
NestJS を選んだ理由は、チームの他のプロジェクトで利用されていて私も理解を深めたいと感じていたので選定しました。

結論ファースト

・「コメント書け!」
文脈や仕様情報がなければ GitHub Copilot はサジェストできない。コメントによりサジェスト精度が大幅に向上する。
・テンプレ的な import 文や config の実装は超得意
GitHub Copilot にサジェストさせると一瞬で完了。
・GitHub Copilot からいいサジェストが来ないとき
まだコードの文脈が足りていない。または、コメントが悪い。ちゃんとロジックでやらせたい処理を一般的な技術用語で説明すること。
・まだ使いこなせなかった部分
とはいえ、どんなにコメント書いてもまだ完璧に100点は難しかった。文法エラーや意図に反したコードは出るので、最後のレビュアーは人間。
・楽しい
1人でググりながらサンプルを読んであれこれやるより、GitHub Copilot と一緒にプログラミングをしたほうがだいぶ早く実装が完了するし、何より楽しいです。

今回のルール

・VS Code で GitHub Copilot を使ってアプリケーションを実装する。
・ググりながら Web 上のサンプルコードを参照するが、コピペをしない。コードを手でタイプしていく。( AI によるサジェストが発生しやすい)

私の実装力

総合的に10段階中「6」ぐらいの実力です。
Java, PHP は業務的にテックリードの経験もありますが、今回の課題 NestJS ( Node.js, Typescript をベースにしたフレームワーク)はほとんど経験がありません。サンプルコードをかじった程度なのでこの言語ではレベル2だと思ってください。

何を作るのか

ユースケース

今回開発するのはログイン画面です。想定される使い方は以下のとおり。
1. ユーザがログイン画面を開く。
2. ログイン画面でメアドとパスワードを入力してボタンを押す。
3. サーバサイドでログイン判定をして完了画面へ遷移させる。

画面仕様

何の装飾もない簡素な画面を定義しました。
成功すると「ログイン成功」が表示されます。
初学者の課題としてはド定番と思います。

フレームワーク

フレームワークは NestJS を使いました。
※今回は NestJS の詳細な解説は割愛させていただくため、もっと知りたいという方は公式ページをご確認ください。
NestJS 公式ページ
https://docs.nestjs.com/
日本語訳
https://zenn.dev/kisihara_c/books/nest-officialdoc-jp

結果的にこんなファイル構成になりました。
src/
├ main.ts
├ app.module.ts
├ app.controller.ts
├ app.service.ts
├ user/
│  ├ user.module.ts
│  ├ user.controller.ts
│  ├ user.service.ts
│  ├ user.entity.ts
│  └ dto/
│      └ user.post-login.dto.ts
views/
└ user/
   ├ login.hbs
   └ login-result.hbs
各ソースの役割をざっくり説明します。
main.ts はフレームワーク的に最初に呼ばれる処理です。
app.*.ts はHelloWorld時に生成されたベースアプリケーションです。Module 呼び出し部分の一部のみ使っています。
user.*.ts がログイン画面の処理となっています。今回、私はこのuser.*.tsを GitHub Copilotと一緒に実装しました。

NestJS 上のクラスとしての役割

user.*.ts について、このクラスは何を担当してるの?というのが初見の方でもすぐわかるように少し説明しておきます。
NestJS 上のクラスとしての役割は以下のようになっています。

・Controller (user.controller.ts)
コントローラ。リクエストを受け付けて返します。今回のURLは1つで、 http://localhost:3000/user/login です。また、リクエストパラメータのバリデーションを行います。複雑な処理はここでは書きません。後続の Service 処理を呼びます。

・DTO (Data Transfer Object)(user.post-login.dto.ts)
リクエストパラメータを入れるオブジェクトです。今回は画面からEメールアドレスとパスワードを受け取ります。

・Service (user.service.ts)
サービスは具体的な機能の処理を担当します。今回の処理は1つで、リクエストパラメータを DB へ問い合わせてログイン可能か判定します。

・Entity (user.entity.ts)
データを保持するオブジェクト。データベースのテーブル定義どおりにパラメータを持ちます。今回は User テーブル1つの Entity を作成しました。ログイン処理時の Select 結果を保持します。

・Module (user.module.ts)
サブシステムをまとめたものはモジュールと呼ばれます。上記のControllerやServiceなどが一式入ります。NestJS がアプリケーションを構成するために使用されます。

・View (login.hbs, login-result.hbs)
画面。 Handlebars テンプレートエンジン(hbs)を使って描画します。

作っていく

ここからがメインコンテンツです。
開発の順序的には、DB接続設定、 Entity, Service, Controller, View の順に作成しました。

実装手順

第1ステップ ベースプログラムをGenerateする
第2ステップ DB接続コンフィグを作成する
第3ステップ EntityクラスでDBのテーブルのデータを定義する
第4ステップ ServiceクラスでDBのテーブルをSelectする
第5ステップ ContorllerクラスでブラウザからURLへのリクエストを受け付ける
第6ステップ View の hbs で画面を表示する

第1ステップ ベースプログラムをGenerateする

NestJS ではコマンドによる Generate で Class の枠自体は一発生成できます。
まず nest new project-name で Hello World のベースアプリケーションができます。
$ npm i -g @nestjs/cli
$ nest new project-name
次にログイン画面の関連のモジュールの塊として user を生成します。
$ nest generate module user
CREATE src/user/user.module.ts (82 bytes)
UPDATE src/app.module.ts (386 bytes)
$ nest generate service user
...
$ nest generate controller user
...
ここまではノーコーディングで完了。おかげで AI の出番はありませんでした。

第2ステップ DB接続コンフィグを作成する

ここから実装に入っていきますが、正直言ってしばらく失敗が続きました。(後半にうまくいきはじめます)

ここではDB接続のライブラリ TypeORM をインストールして、Configを作成します。
MySQL 8 はローカルにインストールしてあります。
接続ドライバーには mysql2 を使いました。
% npm install @nestjs/typeorm typeorm mysql2
TypeORM では DB 接続情報を ormconfig.json ファイルに書きます。
ここからVS Code上で実装します。

まず、結果的に仕上がったファイルはこちらです。
{
    "type": "mysql",
    "host": "127.0.0.1",
    "username": "root",
    "password": "root",
    "port": 3306,
    "database": "login",
    "entities": [
        "src/entity/**/*.ts"
    ],
    "synchronize": true
}

以下でどんな感じでAIが動いたかを書いていきます。

ついに初の AI サジェスト!
ormconfig.json を書き始めて3行目でついに初めて GitHub Copilot によりサジェストされました。が、若干 ormconfig.json の正しい書き方と違うものが表示されてしまいました。(薄いグレーの箇所がサジェスト。上述の最終結果ファイルと比べると間違っていることがわかります)

気を取り直して一部を書き換えてゆっくり進めていくと次のサジェストが出てきます(同じくグレーの箇所。サジェストはタブを押すと確定されます)。今度はいいかんじですが、サジェストが数行一気に出てこず1行ずつだったのでAIもちょっと迷いながら出してきてるような印象でした。

ローカルの設定なのでDB名やパスワードなど違いはあるのですが、 typeorm ならこう書くでしょというセオリーがある程度出てくる感じではありました。

AIの精度が良くないようなことを書いていますが、あとから思い直すと、この段階で私はまだAIを完全に使いこなしているとは言えなかったと思います。

第3ステップ EntityクラスでDBのテーブルのデータを定義する

テーブル定義どおりのオブジェクト Entity を作ります。今回必要な user テーブルはログインのためにメアドをパスワードを保持する簡単なテーブルです。元の定義自体は私が考案しました。

user テーブル定義
user_id int auto_increment
email varchar 255
password varchar 255
created_at datetime
updated_at datetime

これをTypescriptで実装していきます。

まず結論として仕上がったファイルはこちらです。
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export class User {
    @PrimaryGeneratedColumn()
    user_id: number;
    
    @Column()
    email: string;
    
    @Column()
    password: string;

    @Column()
    created_at: Date;

    @Column()
    updated_at: Date;
}
書きあがるまでにどんな順序でサジェストされたかを書いていきます。
まず User の Entity ならこれでしょ!というカラムがサジェストされました。(グレーの箇所)
まだ私はクラス名ぐらいしか情報を教えていないのに。これはすごい。

ただ私は作成日 created_at 、更新日 updated_at を必ず用意するタイプなので、手で created_at を実装します。すると、 GitHub Copilot が updated_at をサジェストしてくれました。

ここまで書いてちょっとわかってきたのですが、コードの書き出し時にはAIのヒントとなる他のコードがないため雑なサジェストしかできないようです。ですが、コードが増えるにつれてその文脈をヒントにして精度の高いサジェストが出せるようになってきます。

第4ステップ ServiceクラスでDBのテーブルをSelectする

前半は失敗が続いていましたが、このあたりからようやく成功しはじめます。
ここでは Service クラスで DB アクセスしてユーザのデータを取得します。

結果的に書いたコードです。
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { FindOneOptions, Repository, TreeRepositoryUtils } from 'typeorm';
import { User } from './user.entity';
import { bcrypt } from 'bcrypt';

@Injectable()
export class UserService {
    constructor(
        @InjectRepository(User)
        private userRepository: Repository<User>,
    ) {}

    //Emailとパスワードを受け取り、ログイン処理を行う
    async login(email: string, password: string): Promise<boolean> {
        //Emailからユーザーを検索
        let user = await this.userRepository.findOne({
            where: {    
                email: email
            }
        });
        
        //ユーザーが存在しない場合はfalseを返す
        if( !user ){
 
            return false;
        } 
        //パスワードハッシュ比較   
        if( await bcrypt.compare(password, user.password) ){
            return true;
        }

        return false;
    }
}
この完了までにどんなサジェストをもらえたか書いていきます。

まず、初期処理の Constructor です。NestJS では DB アクセスはフレームワークの Repository クラスが自動的にやってくれます。で、その設定を記述する Constructor のあたりはほぼ定型文なので GitHub Copilot がさらっと実装してくれました。ありがとう!

ですが、 User テーブルへのアクセスするためのコードで、なかなか意図したものをサジェストしてくれませんでした。(私は以下の findOne の箇所のパラメータを where: { email: email } としたいが、そうなってくれない)
文脈的に email と password がパラメータとして流れてくる箇所なので、この2つのパラメータを使って Select をしようとする GitHub Copilot の提案はそんなにズレているわけでもないんですが、違うには違います。結局 fineOne の部分は私が自分で実装しました。
実はこの問題は次のセクションで解決します。

最大の山場、パスワードハッシュ比較

Service の login ファンクションでは、 DB アクセスしてユーザのデータを取ったあと、ユーザが入力したパスワードが正しいかを検証します。
User のテーブル内のデータはあらかじめ用意してあり、パスワードはあらかじめハッシュ化して保存してあります。
ここでは何らかのロジックでパスワードのハッシュ比較をしたいなあと思いながら実装を始めました。

唐突に書き始めると、よくありがちなコードがサジェストで出てきましたが、これは違います。これはただの文字列比較です。ログイン処理らしくハッシュ化文字列を比較する実装を提案してほしいのに。

~ ここでしばらくググる ~

bcrypt というライブラリでハッシュ化する例がいくつかあったので、これでハッシュ化パスワードの比較を行おうと思います。

ここで初めて私はコメントを書きます。
import { bcrypt } from ‘bcrypt’; を追加し、「パスワードハッシュ比較」というコメントを書きました。
すると。。
かなりそれっぽいコードが出てきました!import文+コメントを書くことで仕様が明示され、まともな返しができるようになったのだと思います。そしてこれはほぼそのまま動作します。GitHub Copilot 優秀!

GitHub Copilot にいいコードを書かせようと思ったら、コメントでも前後のコードでもファイル名でもとにかく多くの仕様情報を伝えていく必要があることがわかってきました。

ひとつ前のパートで where: { email: email } がうまくサジェストされなかった問題がありましたが、これもコメント不足が原因でした。あとあとコメントを書き直して実装したらちゃんとサジェストされました!

1行1コメント以上を徹底することですべてのコードを GitHub Copilot にサジェストさせることができる可能性があると感じています。AIとどうコミュニケーションするかを理解し実践していく時代が本格的にはじまったと言えるのではないでしょうか。

第5ステップ ContorllerクラスでブラウザからURLへのリクエストを受け付ける

ここでは2つのソースを作ります。1つ目はController で URL を制御します。2つ目はそのURLから送られるパラメータを入れる DTO を定義します。

まず、画面からSubmitされたパラメータを入れる DTO の最終形はこうなりました。
import { IsEmail, IsNotEmpty } from 'class-validator';

export class PostLoginDto {
  @IsNotEmpty()
  @IsEmail()
  email: string;

  @IsNotEmpty()
  password: string;
}

以下 AI が提案してくれたコードです。
DTO は、ほぼほぼ GitHub Copilot が書いてくれました。
DTO にはバリデーションも含めることができ、リクエストパラメータのバリデーションチェック処理はデコレータで作れます(Javaでいうアノテーション)。
私はこのデコレータの使い方をあまり理解しないまま書き始めていましたが、GitHub Copilot がそれっぽいコードをサジェストしてくれたので、採用しました。(グレーの箇所がサジェスト)

次に Controller クラスは結果的にこうなりました。
getLogin() : ログイン画面の初期表示。
postlogin() : ログイン処理。Eメールとパスワードのパラメータを受け取って、Serviceにおくってログイン判定をします。
import { Controller, Get, Post, Render, UsePipes } from '@nestjs/common';
import { ValidationPipe } from '@nestjs/common';
import { Body } from '@nestjs/common';
import { PostLoginDto } from './dto/user.post-login.dto';
import { UserService } from './user.service';

@Controller('user')
export class UserController {

    constructor (private readonly userService: UserService) {}

    @Get("login")
    @Render('user/login')
    getLogin() {
        return {message: 'ログイン画面'};
    }

    @Post("login")
    @UsePipes(new ValidationPipe({whitelist: true, forbidNonWhitelisted: true}))
    @Render('user/login-result')
    async postLogin(@Body() postLoginDto: PostLoginDto) {
        let result = await this.userService.login(postLoginDto.email, postLoginDto.password);
        //Trueならログイン成功とレスポンスする
        if( result ){
            return {message: 'ログイン成功'};
        }
        return {message: 'ログイン失敗'};
    }

}

AIの提案ですが、Service を呼ぶところの引数は GitHub Copilot がいい感じに提案してくれました。(グレーの箇所がサジェスト)

最後の Result を返す処理は、コメントを書いたら GitHub Copilot が期待通りに実装してくれました。

第6ステップ View の hbs で画面を表示する

View ではシンプルにメールアドレスとパスワードのパラメータを渡すForm画面を定義します。

結果的に出来上がったファイル
login.hbs
{{message}}
<form action="http://localhost:3000/user/login" method="post">
  email<br/>
  <input type="text" name="email" /><br/>
  password<br/>
  <input type="password" name="password" /><br/>
  <input type="submit" value="Login" />
</form>

唐突に書き始めました(もはや GitHub Copilot にとっては困る書き方)が、それっぽい FORM の提案をしてくれました。


Resultを表示するほうのページも載せておきます。(ド単純なのでサジェストはありませんでした。)
login-result.hbs
Result<br/>
{{message}}

まとめ GitHub Copilot と仲良くやる方法

結論
「コメント書け!」
・テンプレ的な import 文や config の実装は GitHub Copilot にサジェストさせると一瞬で完了。
・複雑なビジネスロジック実装では、まずコメント書け!するとAIはサジェスト精度が大幅に向上。
・いいサジェストが来ないときは、コメントが悪い。書いてほしい処理を一般的な技術用語で説明すること。
・どんなにコメントを頑張ってもまだ完璧に100点は難しい。結構イージーな文法エラーがあったりする。
・1人でググりながらサンプルを読んであれこれやるより、AIのサジェストを活用したほうがだいぶ早く実装が完了する。まずやりたいことをコメントで書くと AI がとりあえず実装してくれる。それをベースにググると効率が良かった。

GitHub Copilot は良きプログラミングパートナーと言えるか?

答えはYESです。GitHub Copilot は非常に有用でした。
実際のコーディングでは上で紹介した内容の2倍以上がサジェストされています。
体感的に以下の割合ぐらいのコードがサジェストされたと思っています。
Entity 80%
Service 80%
DTO 90%
Controller 80%
View 90%

実装において定番の箇所は非常に多くあり、GitHub Copilot は定型文のサジェスト力は凄まじいです。実装初期段階でもこれ絶対 import するでしょっていうのを適切にサジェストしてくれます。独自ロジックの実装がいかに少ないかを改めて実感した次第です。ググる→コピペ→ちょっと修正、という従来の初学者の開発フローと比べても、気持ち的に全然早いと思いました。

AIコミュニケーションの時代に突入

とにかく一番大事なことはコメントをちゃんと書くことです。これによりサジェストの精度が大きく向上します。

使いはじめは正直よくわからんコードばかりがサジェストされて混乱し、なんでこんなことやってるのというのはありました。私が勉強不足なのか、そもそもGitHub Copilotがテキトーなのかと悩みましたが、コメントを書くことを覚えてからはこの問題はかなり少なくなったと感じています。問題はAIとのコミュニケーションのほうにあったわけです。

結果的に想像していた以上に楽しくプログラミングができました。今回 GitHub Copilot と一緒に書くことが結構楽しかったです。フレームワークに詳しくない分、こんな風に書けばいいんだというのをサジェストされるのは良い発見となりましたし、孤独な学習のツラさを忘れさせてくれるような心理的メリットもありました。

ただ、そもそもどういうコードを書くべきかという元のアイディアは、人間自身が持っている必要があると考えています。コードが正しいかどうかをレビューして最終的に判断するのは、現段階では人間だと思います。(これも覆される時代がおそらく来る)

懸念

現状の GitHub Copilot は業務利用における懸念もあるので、以下をメモ的に書いておきます。
・実装したコードは必ず GitHub に送られるので、機密情報を含む場合はリスクを伴う。 AI 学習対象データから外すチェックボックスがあるので必ず OFF にするべき。
・元ネタの学習データは GitHub にあるコード。そのためサジェスト時にはオープンソースライセンスの規約にのっとり引用元を明示するなどが必要だが、現状 GitHub Copilot にはできない(つまりライセンス違反の可能性がある)。公式では、2023年に、どのコードをもとにしたのか出す機能を提供予定としている。

最後に宣伝です!

GMOインターネットグループの次世代システム研究室では、最新のテクノロジーを調査・検証しながらインターネットの新規サービスの開発を行うエンジニアを募集しています。募集職種一覧 からご応募をお待ちしています。

  • Twitter
  • Facebook
  • はてなブックマークに追加

グループ研究開発本部の最新情報をTwitterで配信中です。ぜひフォローください。

 
  • AI研究開発室
  • 大阪研究開発グループ

関連記事