GitHub Copilotでのより良いプロンプトの書き方

こんばんは Esperna - Qiita です。 今年の4月から業務で GitHub Copilot を使い始めて2ヶ月以上経ちます。ほんと便利ですね。 この2ヶ月ゴリゴリ単体テストやコードを書いたり、デバッグすることが多かったのですが、 コードを書くとどんどん補完されていくのが気持ちよくて、もう後戻りできないです。 補完機能だけでも、1ヶ月あたり数時間は入力時間が短縮されているのではないでしょうか。

一方で、捉え方次第でもっと GitHub Copilot を活用できるのではと思い、GitHub Copilot でのプロンプトの書き方について調べてみました。 想定読者は

  • Copilotを使っているけどイマイチ使いこなせてない感があり、もっと上手く活用したいと思っている人
  • Copilotを社内で導入したいと思っている人

で、そのような人たちの役に立てればと思っています。

より良いプロンプトの書き方について

次の3つがあると思います。

  1. コメントには create/generate/rewrite といったプロンプトによる指示ではなく関数の説明そのものを書く
  2. 宣言的プログラミングを行う
  3. 既知のアルゴリズムやデータ構造、デザインパターンに適用する

なお、本ブログで登場するプロンプトの例はVSCode環境で実行したもので、注意すべき点があります。

  • 出力例は内容の妥当性を保証したものではないのでご注意ください。
  • また実際のコメントには「//<=ここで Ctrl+Enter(※)」は含んでいません。

GitHub Copilotはプロンプトを記載した上でCtrl+Enterを押すと、複数の補完候補を表示してくれます

1. コメントには create/generate/rewrite などを使わずに関数の説明そのものを書く

GitHubのgenerating-code-suggestions-from-commentsに「You can describe something you want to do using natural language within a comment」と書かれており、 当たり前とは言えば当たり前な気もしますが、色々検討し1周回って元に戻ってきた例を1つ紹介します。

下記はjsonファイルの中身をjqでフィルタリングするGo言語のプログラムの出力を目指したプロンプトの例です。 自然言語の説明は少なく、主な条件をshellから推測してもらうことを期待していました。

/*
cat test.txt | jq -r  'select(.vulnerability.modules[0].packages[0].callstacks[0] !=null).vulnerability | {descrip
tion:.osv.details, "check_name":"govulncheck" ,  fingerprint:.osv.aliases[0] , "severity": "HIGH", location : { path : .modules[0].packages[0].callstacks[0].frames[0].position.filename , lines 
:{begin:.modules[0].packages[0].callstacks[0].frames[0].position.line } } } '| jq -s .
*/

// q: Please rewrite above shell by golang
// a: //<=ここでctrl+Enter

これは同僚に言われて気がついたのですが、Copilot はChatGPTと違って「指示する」系のプロンプトはなんとなく合わない印象があります。 なぜなら

  • 生成後に指示文を削除するなどの辻褄を合わせる作業が必要なくなる / コメントがそのままコメントとして機能するから
  • もともと コメント + ソースコード を学習に利用しているはずで、そこには copilot への指示文は含まれないはずだから

よって、コメントには create/generate/rewriteと書かず、関数の説明そのものを書いてあげると良さそうです。

// print out "govulncheck" results like following `jq` script
// cat test.txt | jq -r 'select(.vulnerability.modules[0].packages[0].callstacks[0] !=null).vulnerability | {description:.osv.details, "check_name":"govulncheck" ,  fingerprint:.osv.aliases[0] , "severity": "HIGH", location : { path : .modules[0].packages[0].callstacks[0].frames[0].position.filename , lines :{begin:.modules[0].packages[0].callstacks[0].frames[0].position.line } } }'
// shall not use ioutil because it is deprecated//<=ここでctrl+Enter

2. 宣言的プログラミングを行う

手続き的プログラミングはコードの実行手順を記述していくようなプログラミングを指すのに対して、 宣言的プログラミングとは得たい結果を記述するようなプログラミングを指します。

例としては前者が c言語で for 文や if 文でコンピュータにやってほしい手続き(例えばバブルソート等を考えてもらうとイメージしやすいと思います)を書くのに対して、 後者はSQLなどで「create table if not exists xxx」と書きます。

以下は単純なモータを駆動するプログラムを出力するプロンプトの一例で、 状態に応じた start の振る舞いをコメントで記述しています。

class Motor {
    constructor() {
        this.isActive = false;
    }
    //when motor state is not active, start calls motorDev.start()
    //when motor state is active, start does nothing
    start()//<=ここでctrl+Enter
}

出力例

start() {
        if (!this.isActive) {
            this.isActive = true;
            motorDev.start();
        }
    }

上記の記載に stop の振る舞いの記載も追加すると

class Motor {
    constructor() {
        this.isActive = false;
    }
    //when motor state is not active, start calls motorDev.start()
    //when motor state is active, start does nothing
    start() {
        if (!this.isActive) {
            motorDev.start();
            this.isActive = true;
        }
    }

    //when motor state is active, stop calls motorDev.stop()
    //when motor state is not active, stop does nothing//<=ここでctrl+Enter

startstop の対称性も維持したコードを出力してくれます。

出力例

class Motor {
    constructor() {
        this.isActive = false;
    }
    //when motor state is not active, start calls motorDev.start()
    //when motor state is active, start does nothing
    start() {
        if (!this.isActive) {
            motorDev.start();
            this.isActive = true;
        }
    }

    //when motor state is active, stop calls motorDev.stop()
    //when motor state is not active, stop does nothing
    stop() {
        if (this.isActive) {
            motorDev.stop();
            this.isActive = false;
        }
    }

個人的に宣言的プログラミングで書くときは割とテスト駆動・テストファーストで書くときの感覚に近いと思っています。 宣言的にコメントを書くとCopilotが行間を読んで、コードを出力してくれますし、 暗黙のうちに対称性や規則性を考慮してコードを出力してくれるので、 Copilotはテスト駆動・テストファーストでコードを書く際の効率を上げてくれると思っています。

また、関数・メソッドに関する入出力の説明を増やしたり、 データの型に関する情報を増やすとより精度の高い出力が得られると思います。 これは「.d.ts をピン止めするとOpenFiles としてプロンプトの一部になって補完精度あがるよ」というmizchiさんという方のtwitter投稿と 同じことではないかと思っていて、型のような静的な情報が増えるほど、出力が一意に定まりやすいのではないかと思います。

3.既知のアルゴリズムやデータ構造、デザインパターンに適用する

例えばquick sortのようなアルゴリズムをバグを埋め込まずに実装するには時間がかかります。 既知のアルゴリズムを実装するといった車輪の再発明をする時間を減らして、 ドメインに特化した設計(データ構造・アルゴリズムの選択、クラスの責務分担、状態設計、非機能とのトレードオフの選択など)に 時間をかけられると効率が良いのではと思っています。 大抵の設計・実装課題というのは過去に先人が通ってきた道であり、 特定のアルゴリズムやデータ構造、あるいはデザインパターンで解ける内容である場合は 前述の宣言的プログラミングを駆使して精度の高い出力を得られるのではと思っています。 ただし、前提として出力されるコードが正しいかどうかを理解できる、 つまり、入出力コードをチェックするテストコードが書けることが前提になります。

//use quickSort algorithm
//input: Number array
//output: Number array
function quickSort(array) //<=ここでctrl+Enter

出力例

function quickSort(array){

    //if array is empty or has 1 element, return it
    if(array.length <= 1){
        return array;
    }

    //choose pivot, take first element
    var pivot = array[0];

    //create left and right arrays
    var left = [];
    var right = [];

    //iterate through array, put elements bigger than pivot in right array, smaller in left array
    for(var i = 1; i < array.length; i++){
        if(array[i] < pivot){
            left.push(array[i]);
        }else{
            right.push(array[i]);
        }
    }

    //recursively call quickSort on left and right arrays
    return quickSort(left).concat(pivot, quickSort(right));

}

注意点

Copilotはとても便利ですが注意点があります。

  • 前提としてGitHub Copilotで下記が設定されていること
    • Suggestions matching public codeがBlockになっていること
    • Allow GitHub to use my code snippets for product improvementsにチェックが入ってないこと
  • Copilotから出力されるコードが妥当かセルフチェック、テスト、レビューを必ず行うこと
    • そのまま使えることもあるが、大抵は加筆・修正が必要
    • 上述の設定をしていてもCopilotが提案するコードにライセンス違反のコードが含まれる可能性があるのでチェックすること

まとめと所感(Copilotをチーム全体で使う意義について)

次の3つを意識すると精度の高い出力を得やすく、車輪の再発明を防げると考えます。

  1. コメントには create/generate/rewrite といったプロンプトによる指示ではなく関数の説明そのものを書く
  2. 宣言的プログラミングを行う(なるべく静的な情報を増やす)
  3. 既知のアルゴリズムやデータ構造、デザインパターンに適用する

Copilotをチーム全体で使う意義については、単にコードの入力時間の短縮に止まらず、 前述のような宣言的なコードのコメントを増やすことで、 テストファースト・テスト駆動的な開発がチームでできるとGoDocやJSDoc的にAPI仕様(設計仕様)を残すことにも繋がり、 チーム内に一定の規律や共通見解を育み易く、属人性を下げることにもつながるのではないかと思いました。

参考

docs.github.com www.educative.io


株式会社フォトシンスでは、一緒にプロダクトを成長させる様々なレイヤのエンジニアを募集しています。 hrmos.co

Akerun Proの購入はこちらから akerun.com