ChatGPT APIとGitHub ActionsでPull RequestのAI Code Reviewをつくってみた
モチベーション
巷で何かと話題な GhatGPT API で自分もなにか作ってみたいと思いました。
anc95/ChatGPT-CodeReviewなどもありますが、自分でプロンプトや仕組みを考えてみたかったので。
実用性はあまり考慮していません。
最初に結果
Pull Request を Open すると、コードレビューの結果をコメントに書いてくれます。
なお、今回は私が結果を確認しやすいように、レビュー対象を Ruby のコードに絞っています。
結果例 1
ARGV[0]
の入力チェックや、review_code
実行時のエラーハンドリングをするといいかもという旨のコメントをしてくれました。
対象ファイルと変更内容
+ # frozen_string_literal: true
+
+ require_relative '../lib/chatgpt_code_reviewer'
+
+ # Usage: git diff file_path | ruby cli/chatgpt_code_reviewer_cli.rb file_path
+ file_path = ARGV[0]
+ diff = $stdin.read
+
+ puts ChatGPTCodeReviewer.new.review_code(file_path, diff)
レビュー内容
Overall, the code changes look good. The new file `cli/chatgpt_code_reviewer_cli.rb` was created and contains the same code as the previous file, but with additional comments and formatting.
There are no major issues with the changes made in the diff. However, it would be better to add more error handling to the code. For example, if the user does not input a file path as an argument, the program should not crash and rather provide an error message to the user. This can be implemented by adding an `if` statement to check if the `ARGV[0]` is `nil` or not.
Additionally, it would be a good idea to handle any exceptions that may occur during the code review process. This can be done by wrapping the code in a `begin` and `rescue` block and printing the exception message if an exception occurs.
Overall, the code changes are fine and do not introduce any major issues, but it could be improved by adding more error handling.
結果例 2
N+1が発生するようなコードを書いてみました。ちゃんと指摘されていますね。
しかし、変更していない箇所についてもレビューされています。
class PostsController < ApplicationController
def index
@posts = Post.all.order(id: :desc)
render json: @posts
end
+ def show
+ @post = Post.find(params[:id])
+ @post.comments.each do |comment|
+ Rails.logger.info "comment likes: #{comment.likes.count}"
+ end
+ render json: @post
+ end
end
レビュー内容
Overall, the code looks good with no syntax errors or obvious logic issues. However, after reviewing the git diff, there are a couple of things to note:
1. app/controllers/posts_controller.rb:6
- The `index` method is missing a newline after the first line. It's always a good practice to have a newline after the class definition and after each method definition to improve code readability.
2. app/controllers/posts_controller.rb:8-16
- The `show` method is added with code to retrieve comments related to the post and print the number of likes for each comment using `Rails.logger`. This seems like a useful piece of information, but it would be better to move this logic into a separate method in the `Post` model, instead of clumping it into the controller. Additionally, to improve performance, we can preload the `likes` association to avoid hitting the database multiple times for each comment.
処理の流れ
- 開発者が Pull Request を Open する
- GitHub Actions の Workflow で Pull Request の変更ファイルのパスと git diff の結果を取得する
- Ruby のコードを実行する。このとき 2.で取得した結果を入力する
- Ruby のコード内で ChatGPT API にコードレビュー依頼のリクエストをして結果を受け取る
- レビュー内容が Pull Request のコメントに投稿される
手順
OpenAPI の API キーを作成する
以下から発行できます。
https://platform.openai.com/account/api-keys
ちなみに私は無料枠の期限が切れていたため、API の利用にはクレジットカードの登録が必要でした。
本記事を執筆している 2023 年 4 月時点の料金はgpt-3.5-turbo
モデルが$0.002 / 1K tokens
です。
詳しくは以下を参照
https://openai.com/pricing
ChatGPT API を呼び出す Ruby コードの作成
ちなみに Ruby で書いている理由は私が普段仕事で書くことが多いからです。
ChatGPT API を使うために Ruby OpenAI をインストールします。
gem "ruby-openai"
bundle install
次に ChatGPT API にコードレビュー依頼のリクエストを送り、結果を受け取るクラスを作成します。
# frozen_string_literal: true
require 'openai'
class ChatGPTCodeReviewer
def initialize
# NOTE: 作成したOPENAIのAPIキーを環境変数から取得
access_token = ENV.fetch('OPENAI_API_KEY')
@client = OpenAI::Client.new(access_token:)
end
# NOTE: レビューするファイルのパスと変更差分(git diffの結果)を入力
def review_code(file_path, diff)
file_content = File.read(file_path)
prompt = make_prompt(file_content, diff)
request_to_openai(prompt)
end
private
def request_to_openai(prompt)
response = @client.chat(
# TODO: 必要最低限のパラメータを指定している。見直しの余地がありそう
# https://platform.openai.com/docs/api-reference/chat/create
parameters: {
model: 'gpt-3.5-turbo',
messages: [{ role: 'user', content: prompt }]
}
)
response.dig('choices', 0, 'message', 'content')
end
# TODO: プロンプトも見直しの余地がありそう
def make_prompt(file_content, diff)
<<~PROMPT
I want you to act as an Ruby on Ruby, Ruby on Rails expert code reviewer.
I enter all the content for one rb file.
In addition, enter the git diff results of the file changes.
All contents are entered following "All contents:".
The git diff results are entered following "Diff:".
The format of the git diff is "+" at the beginning of added lines and "-" at the end of deleted lines.
After considering The "All Contents:", return the code review against the "Diff:"
Reviews for specific lines should begin with the format "directories/filename:row number" to indicate the line number of the file.
An example of the format is "app/models/hello.rb:2".
All contents:
```
#{file_content}
```
Diff:
```
#{diff}
```
PROMPT
end
end
GitHub Actions から実行するため、CLI 経由で実行できるようにします。
ここで git diff の結果はパイプラインで受け取るようにしています。
# frozen_string_literal: true
require_relative '../lib/chatgpt_code_reviewer'
# Usage: git diff file_path | ruby cli/chatgpt_code_reviewer_cli.rb file_path
file_path = ARGV[0]
diff = $stdin.read
puts ChatGPTCodeReviewer.new.review_code(file_path, diff)
プロンプトの説明
All contents:
以下にはレビュー対象のファイル全体の内容が入ります。
Diff:
以下にはそのファイルの変更差分として git diff の結果がはいります。
I want you to act as an Ruby on Ruby, Ruby on Rails expert code reviewer.
I enter all the content for one rb file.
In addition, enter the git diff results of the file changes.
All contents are entered following "All contents:".
The git diff results are entered following "Diff:".
The format of the git diff is "+" at the beginning of added lines and "-" at the end of deleted lines.
After considering The "All Contents:", return the code review against the "Diff:"
Reviews for specific lines should begin with the format "directories/filename:row number" to indicate the line number of the file.
An example of the format is "app/models/hello.rb:2".
All contents:
```
# frozen_string_literal: true
class PostsController < ApplicationController
def index
@posts = Post.all.order(id: :desc)
pp 'hoge'
render json: @posts
end
def show
@post = Post.find(params[:id])
render json: @post
end
end
```
Diff:
```
diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb
index cae4a20..b9e4be5 100644
--- a/app/controllers/posts_controller.rb
+++ b/app/controllers/posts_controller.rb
@@ -2,7 +2,13 @@
class PostsController < ApplicationController
def index
- @posts = Post.all.order(id: :desc)
+ @posts = Post.all
+ pp hoge
render json: @posts
end
+
+ def show
+ @post = Post.find(params[:id])
+ render json: @post
+ end
end
```
以下のように考えてプロンプトを組み立てています。
- Ruby, Rails のエキスパートとして振る舞うに指定
- 具体的な役割を与えると回答の精度が上がるという記事をみかけたので取り入れました
- 英語で依頼。日本語よりもレビュー結果の精度が高そう、かつ Token の消費量を抑えるため
- 英語で「最終的な結果は日本で返してください」と依頼するのも試したが、精度が落ちた印象を受けました
- ファイル全体と変更差分を入力
- 差分だけだと変更内容を正しく把握できず、レビューの精度が低いのではと考えました
- git diff の読み方を説明
- もしかしたらなくても問題なく解釈してくれるかもしれません
- どの行に対してのレビューかわかるように依頼
-
ファイルパス:行番号
のフォーマットでレビューを返すように依頼し、例示しています
-
GitHub Actions の Workflow 作成
事前準備としてOPENAI_API_KEY
という名前で、発行した OpenAI API のキーを Actions secrets を追加します。
以下の URL から追加できます。
https://github.com/[owner]/[repositoy]/settings/secrets/actions
name: ChatGPT Ruby Code Review
on:
pull_request:
# 修正をpushしたときにも実行するなら synchronize を追加する
types: [opened, ready_for_review]
paths:
- '**.rb'
jobs:
code-review:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
# HEADがマージ元ブランチの最新コミットになるようにしている
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.2.1
bundler-cache: true
# github.base_ref を利用して、マージ先のブランチとの差分を取得している
- name: review code
env:
OPENAI_API_KEY: ${{secrets.OPENAI_API_KEY}}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
git diff --name-only origin/${{ github.base_ref }}..HEAD | grep '\.rb$' | while read file; do
git diff origin/${{ github.base_ref }}..HEAD "${file}"
review_result=$(git diff origin/${{ github.base_ref }}..HEAD "${file}" | bundle exec ruby cli/chatgpt_code_reviewer_cli.rb "${file}")
gh pr comment ${{ github.event.pull_request.number }} --body "${review_result}"
done
注意事項
- Pull Request の変更差分が多いと Token の消費が多くなり、Workflow の実行時間も長くなるので注意が必要です。
- API で送れる token にも上限があり
pt-3.5-turbo
は上限4096 tokens
です。
- API で送れる token にも上限があり
- 今回作成したコードは全般的に想定外の入力を考慮していません。
- プロンプトもインジェクションできるかもしれません
つくってみた所感
-
プロンプトは改善の余地が多くありそう
- 前提条件を追加するとか
- 言語やライブラリのバージョンを教えてあげる
- レビュー観点を具体的にするとか
- セキュリティ、パフォーマンス、テストの網羅性、可読性など
- 複数ファイルの変更全てをまとめてレビュー依頼するとか
- 今回は実装を簡単にするため 1 ファイル単位で依頼している
- 前提条件を追加するとか
-
レビュー結果にはブレがある
- 全く同じ入力でも。結果にばらつきがある。
-
temperature
などの API パラメータで調整できそう
-
レビュワーがいない状況で利用できそう
- 周りに詳しい人がいなかったり、個人開発などのシーン。
- 結局、レビュー内容が適切か判断するのに知識が必要なので、気づきのきっかけくらいに捉えるのが良さそう
- AI なので良くも悪くもレビュー内容を気軽に採用・無視できる
-
Pull Request のファイル差分のインラインコメントとして投稿したらもっと良さそう
- 今回はやらなかった
- プロンプトを工夫して、
ファイルパス:行番号
のフォーマットを更にいい感じに強制すればできそう
参考
- https://openai.com/pricing
- https://platform.openai.com/docs/guides/chat
- https://dev.classmethod.jp/articles/chatgpt-summarizes-github-pr
- https://dev.classmethod.jp/articles/github-actions-get-diff-files-on-pr-events/
以上
Discussion