【生成AI】Github Copilot Chat を活用したら開発速度が1.5倍速になった件 【真似してみよう】

## はじめに

皆さんは Github Copilot Chat というツールをご存知でしょうか？

Github Copilot Chat は、プログラミングと関係する質問に対して、対話ベースで回答をしてくれるツールです。
workspaceのコードを読み取り回答してくれるため、精度が高く実務でも非常に役立ちます。

今回は、この Github Copilot Chat を活用することで、開発スピードが 1.5 倍速になった活用術をご紹介いたします。




## 活用方法

さて、活用方法について実際に投げているプロンプトや活用術をご紹介いたします。

今回は、Vscodeで拡張機能のGithub Copilot chatをインストールして使用しています。

![スクリーンショット 2024-09-08 0.06.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/636995/7c635404-bd4a-3157-e3da-400db951e0df.png)

### 基本的な使い方
ここでは、GithubやVscodeのドキュメントにもある基本的な使い方をご紹介いたします。
プロンプトに「＠」と入力して、エージェント機能が使用することができます。


#### `@workspace`
>ワークスペース内のコードに関するコンテキストがあります。 Copilot でプロジェクトの構造、コードのさまざまな部分の相互作用、またはプロジェクト内の設計パターンを考慮する場合に @workspace を使用します。
github Copilot のドキュメントより

具体例でいくと「`@workspace` データベース接続はどこでどのように構成されていますか?」
といったプロンプトを投げることで、データベース接続のコードを教えてくれます。

実際に、適当なRailsプロジェクトで試すとこの通り。

<img width="450" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/636995/6592c1e5-4e92-3676-db43-bccc23b94169.png">


う〜ん、すごく便利、新しくプロジェクトにアサインされた時や
初めて使うフレームワークで活躍してくれそうですね。

#### `@vscode`
>Visual Studio Code コマンドと機能に関するコンテキストがあります。 Visual Studio Code に関するヘルプが必要な場合に @vscode を使用します。
github Copilot のドキュメントより

Vscodeエディタ自体のコマンドや機能を教ええてくれるコンテキストになります。



#### `@terminal`
> Visual Studio Codeターミナル シェルとその内容に関するコンテキストを持ちます。
 ターミナル コマンドの作成またはデバッグに関するヘルプが必要な場合に @terminal を使用します。
github Copilot のドキュメントより

「`@terminal` 直前のコミットを取り消したい」といったプロンプトを投げることで、
コミット名を変更するコマンドを教えてくれます。


---
以下は、@kim_t0814 がよく使用するプロンプトです。

### そのライブラリを全く知らない時

今回はすでに知っていますが、`flag_shih_tzu`という1 つの Gem について知らなかったとしましょう。

以下のようなプロンプトを投げると、概要と一次ソースを貰うことができます

```
@workspace

XXXのことが分かりません。
XXXの概要とそのリファレンスを教えてください。
```

![1d753aae-fa23-0e68-4ce8-849b2d5ea788.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/636995/85357bbb-d683-80c4-8089-0cda9f87fcc0.png)

リファレンス（特に英語のもの）を見ても何を言っているのかさっぱりな時ってありますよね。
そういう場合、概要を知った上でリファレンスを見ることで、無知の状態でも頭にスッと入って理解することができます。

### テストコードを書いて欲しい！

さて、Copilot Chat はテストコードも workspace のコードを読み取って作成してくれます。
今回は、去年作成した GraphQL API を叩いて電気料金を取得するアプリケーションの
メインロジックに対して、テストコードを作成してもらいます！(現在は、メインロジックはサービス層に移植済)

https://qiita.com/kim_t0814/items/d7346d19529ca2315fe3

https://github.com/kimkim0814/octopus-energy-line-bot

実際のコードは以下の通りになります。

```rb
class OctopusEnergyBill

  GetBillDayQUERY = OctopusClient::Client.parse <<~'GRAPHQL'
    query(
      $accountNumber: String!
      $fromDatetime: DateTime
      $toDatetime: DateTime
    ) {
      account(accountNumber: $accountNumber) {
        properties {
          electricitySupplyPoints {
            agreements {
              validFrom
            }
            halfHourlyReadings(
              fromDatetime: $fromDatetime
              toDatetime: $toDatetime
            ) {
              startAt
              endAt
              value
              costEstimate
              consumptionStep
              consumptionRateBand
            }
          }
        }
      }
    }
  GRAPHQL


  GetBillMonthQUERY = OctopusClient::Client.parse <<~'GRAPHQL'
    query ($accountNumber: String!) {
      account(accountNumber: $accountNumber) {
        properties {
          electricitySupplyPoints {
            agreements {
              validFrom
            }
            intervalReadings {
              endAt
              startAt
              value
              costEstimate
            }
          }
        }
      }
    }
  GRAPHQL

  def notify_yesterday
    result = OctopusClient::Client.query(GetBillDayQUERY, variables: {
      accountNumber: ENV['OCTOPUS_ACCOUNT_NUMBER'],
      fromDatetime: Date.yesterday.beginning_of_day.iso8601,
      toDatetime: Date.yesterday.end_of_day.iso8601
      })


    electricity_supply_points = get_electricity_supply_points(result)
    half_hourly_readings = electricity_supply_points.first["halfHourlyReadings"]
    @kwh = half_hourly_readings.pluck("value").map(&:to_f).sum.round(2)
    @cost = half_hourly_readings.pluck("costEstimate").map(&:to_f).sum.round(1)
    text = "#{Date.yesterday.strftime('%Y年%m月%d日')}は#{@kwh}kWh消費して#{@cost}円かかったよ"

    send_message(text)
  end

  def notify_last_month
    result = OctopusClient::Client.query(GetBillMonthQUERY, variables: {
      accountNumber: ENV['OCTOPUS_ACCOUNT_NUMBER'],
      })

    electricity_supply_points = get_electricity_supply_points(result)
    monthly_readings = electricity_supply_points.first["intervalReadings"]

    @kwh = monthly_readings.last["value"].to_f.round(1)
    @cost = monthly_readings.last["costEstimate"].to_f.round(0)
    text = "#{Date.today.last_month.strftime('%Y年%m月')}は#{@kwh}kWh消費して#{@cost}円かかったよ"

    send_message(text)
  end

  private

  def get_electricity_supply_points(result_data)
    properties = result_data.original_hash.dig("data", "account", "properties")
    properties.first["electricitySupplyPoints"]
  end

  def send_message(text)
    message = {
      type: 'text',
      text: text
    }

    LineBotClient.new.client.broadcast(message)
  end
end
```

さて、上記のファイル内のコード上で右クリックして `Copilot>ファイルをチャット内` に追加をクリックした上で、
以下のようなプロンプトを投げると、単体テストコードを作成してくれます。

```
@workspace /tests
```

![スクリーンショット 2024-09-07 23.31.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/636995/2518e13b-1dca-5ec9-d01d-3140883d0905.png)

**実際に生成されたコードはこちら**

```rb
describe OctopusEnergyBill do
  describe '#notify_yesterday' do
    let(:mock_electricity_supply_points) { [{"halfHourlyReadings" => [{"value" => "1.0", "costEstimate" => "2.0"}]}] }

    before do
      allow(OctopusClient::Client).to receive(:query).and_return(mock_result)
      allow(octopus_energy_bill).to receive(:get_electricity_supply_points).and_return(mock_electricity_supply_points)
      allow(octopus_energy_bill).to receive(:send_message)
    end

    it 'queries the OctopusClient with correct parameters' do
      octopus_energy_bill.notify_yesterday
      expect(OctopusClient::Client).to have_received(:query).with(
        OctopusEnergyBill::GetBillDayQUERY,
        variables: {
          accountNumber: ENV['OCTOPUS_ACCOUNT_NUMBER'],
          fromDatetime: Date.yesterday.beginning_of_day.iso8601,
          toDatetime: Date.yesterday.end_of_day.iso8601
        }
      )
    end

    it 'sends a message with the correct text' do
      octopus_energy_bill.notify_yesterday
      expect(octopus_energy_bill).to have_received(:send_message).with("#{Date.yesterday.strftime('%Y年%m月%d日')}は1.0kWh消費して2.0円かかったよ")
    end
  end

  describe '#notify_last_month' do
    let(:mock_electricity_supply_points) { [{"intervalReadings" => [{"value" => "139.0", "costEstimate" => "3500.8"}]}] }

    before do
      allow(OctopusClient::Client).to receive(:query).and_return(mock_result)
      allow(octopus_energy_bill).to receive(:get_electricity_supply_points).and_return(mock_electricity_supply_points)
      allow(octopus_energy_bill).to receive(:send_message)
    end

    it 'queries the OctopusClient with correct parameters' do
      octopus_energy_bill.notify_last_month
      expect(OctopusClient::Client).to have_received(:query).with(
        OctopusEnergyBill::GetBillMonthQUERY,
        variables: { accountNumber: ENV['OCTOPUS_ACCOUNT_NUMBER'] }
      )
    end

    it 'sends a message with the correct text' do
      octopus_energy_bill.notify_last_month
      expect(octopus_energy_bill).to have_received(:send_message).with("#{Date.today.last_month.strftime('%Y年%m月')}は139.0kWh消費して3501円かかったよ")
    end
  end
end
```

mock を含めて、だいぶそれっぽいテストコードを生成してくれました。
後は、実際にコードが正常に動くか検証したり、自然言語でテストケースを追加して壁打ちをしていくことで
より良いテストコードを作成することができます。

### レビュー依頼を出す前に

これは、実際に実務中に自分はセルフレビューをするのですが、そのセルフレビューの一環として
Copilot Chat を使用しています。

流れとしては、

```console
実装
↓
セルフレビュー
↓
Copilot Chatにレビューさせる
```

という順で行うことによって、自分の目で見逃していた部分を補完することができます。

セルフレビューと Copilot Chat のダブルチェックによって、よりよい状態で
コードをレビュー依頼することができます。

これで、レビューの打ち返しを減らし、approve をもらうまでの時間を短縮することもできますし
レビュワーの負担も減らすことができます(~~まあ、Coplot Chat を使用せず完璧なコードを書ければそれがベストですが~~)。

さて、今回は分かりやすいように、分かりやすさを重視し、あえて悪いコードをレビューしてもらいます。
以下はミノ駆動本の 14 章のコードの悪いパターンを参照に ruby に書き換えたものです。

https://www.amazon.co.jp/%E8%89%AF%E3%81%84%E3%82%B3%E3%83%BC%E3%83%89%EF%BC%8F%E6%82%AA%E3%81%84%E3%82%B3%E3%83%BC%E3%83%89%E3%81%A7%E5%AD%A6%E3%81%B6%E8%A8%AD%E8%A8%88%E5%85%A5%E9%96%80%E2%80%95%E4%BF%9D%E5%AE%88%E3%81%97%E3%82%84%E3%81%99%E3%81%84-%E6%88%90%E9%95%B7%E3%81%97%E7%B6%9A%E3%81%91%E3%82%8B%E3%82%B3%E3%83%BC%E3%83%89%E3%81%AE%E6%9B%B8%E3%81%8D%E6%96%B9-%E4%BB%99%E5%A1%B2-%E5%A4%A7%E4%B9%9F-ebook/dp/B09Y1MWK9N/ref=sr_1_1?__mk_ja_JP=%E3%82%AB%E3%82%BF%E3%82%AB%E3%83%8A&crid=HRJ6RYGGRT5T&dib=eyJ2IjoiMSJ9.vQ7Cq6h_UiIKeb5BoKV8qduuxgPCOw4kEo4H2X_bJHXGjHj071QN20LucGBJIEps.vwD7zfE2BjiPp-6_YS3qNdOsMwTfGRLfpErAMZZGSjs&dib_tag=se&keywords=%E3%83%9F%E3%83%8E%E9%A7%86%E5%8B%95+%E6%9C%AC&qid=1725720673&sprefix=%E3%83%9F%E3%83%8E%E9%A7%86%E5%8B%95%E6%9C%AC%2Caps%2C223&sr=8-1

```rb
class PurchasePointPayment
  attr_reader :customer_id, :comic_id, :purchase_point, :local_date_time

  def initialize(customer,comic)
    if customer.enabled?
      @customer_id = customer.id
      if comic.enabled?
        @comic_id = comic.id
        if comic.current_purchase_point.amount <= customer_possession_point.amount
          @consumption_point = comic.current_purchase_point
          @payment_date_time = Time.current
        else
          fail '所持ポイントが不足しています。'
        end
      else
        fail ArgumentError, '現在取り扱いのできないコミックです。'
      end
    else
      fail ArgumentError, '有効な購入者ではありません。'
    end
  end
end
```

筆者は以下のようなプロンプトを投げます。

```
@workspace
あなたは、レビュワーです。
下記のコードレビューをする際に修正するとしたらどこを修正しますか?

また問題ない場合はLGTMを出してください。

以下、先ほどの悪いコードを貼り付ける or 選択して追加しておく
```

実際に生成されたコードとコメントはこちら

```rb
class PurchasePointPayment
  attr_reader :customer_id, :comic_id, :purchase_point, :payment_date_time

  ERROR_MESSAGES = {
    invalid_customer: '有効な購入者ではありません。',
    invalid_comic: '現在取り扱いのできないコミックです。',
    insufficient_points: '所持ポイントが不足しています。'
  }.freeze

  def initialize(customer, comic)
    validate_customer(customer)
    validate_comic(comic)
    validate_points(customer, comic)

    @customer_id = customer.id
    @comic_id = comic.id
    @purchase_point = comic.current_purchase_point.amount
    @payment_date_time = Time.current
  end

  private

  def validate_customer(customer)
    fail ArgumentError, ERROR_MESSAGES[:invalid_customer] unless customer.enabled?
  end

  def validate_comic(comic)
    fail ArgumentError, ERROR_MESSAGES[:invalid_comic] unless comic.enabled?
  end

  def validate_points(customer, comic)
    fail ERROR_MESSAGES[:insufficient_points] if comic.current_purchase_point.amount > customer.possession_point.amount
  end
end
```

![スクリーンショット 2024-09-07 23.56.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/636995/e2a897a4-8c3b-3e3d-93a9-58b849fcd0c4.png)

素晴らしい...😇

そのプロジェクトごとにコーディング規約はあるので、それに合わせる必要はありますが
理由も端的に説明してくれるので、コードを理解した上で修正することができます。

ただ、Copilot Chat が生成したコードが全て正しいとは限らないので、
そこはオーナーシップを持ってレビュー依頼をしてください(突っ込まれそうなところは回答できるように)。

### 資格の勉強時に
そのライブラリを全く知らない時と、似たような使用方法になりますが
Ruby Goldの勉強をしていた際にリファレンスだけ見た際に理解できなかった部分を
Copilot Chatと壁打ちをすることで理解を深めることができました。

ただし、プログラミングと離れている内容の場合は
Copilotの範囲外になります。

![スクリーンショット 2024-09-08 0.28.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/636995/428d0706-2397-02c6-b4dd-6bd1f620fb10.png)

ただし、以下のようなプロンプトのようにゴリ押しをしてプログラミングや
ソフトウェア開発と結びつけることで回答してくれはします...
(かなりネタ的な使い方になりますが)

![スクリーンショット 2024-09-08 0.30.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/636995/5fa0716d-1faf-759a-4216-99c1f7622855.png)

**Copilotさん!?**

このプロンプト、ドメインを考える時にもしかしたら役に立つことがあるかも知れませんね...!

## 終わりに
昨今、生成AIがどんどん流行りつつありますが、それを有効活用することが
手を動かすエンジニアにおいても重要なのかもしれませんね。

プロンプトを真似して投げてみることは、すぐ出来ると思うので
是非お試しください。

あ、実務で使用する場合はGithub Copilotにコードを学習させないことをお忘れなく！

## 参照
https://docs.github.com/ja/copilot/responsible-use-of-github-copilot-features/responsible-use-of-github-copilot-chat-in-your-ide

https://qiita.com/masakinihirota/items/c9df9de0c7326280bfae

https://code.visualstudio.com/docs/copilot/workspace-context