Pull requestの概要の作成とコードの改善を提案するツールを作ってみた

2024.7.9

1. はじめに

はじめまして、Sreake事業部でインターンをしている村山です。

今回は、PR Guardianというツールの開発と検証をしました。PR GuardianはPull Requestの概要の作成、コードの改善提案をするツールです。

2. PR用のツールについて

PR用のツールとして、CodiumAIのPR-AgentやGithub Copilot pull requestなどがあります。PR-Agentについては、以前、PR-Agentとその類似システムの解説にて検証を行っています。PR-Agentは現在、以下の機能が提供されています。

  • PR Description (/describe): タイトル、タイプ、概要、コードウォークスルー、ラベルを自動生成します。
  • PR Review (/review): PRに関する調整可能なフィードバック、問題点、セキュリティ懸念、レビューの労力などを提供します。
  • Code Suggestions (/improve): PRの改善のためのコード提案を行います。
  • Question Answering (/ask …): PRや特定のコード行に関する自由形式の質問に回答します。
  • Update Changelog (/update_changelog): PRの変更内容を基にCHANGELOG.mdファイルを自動更新します。
  • Find Similar Issue (/similar_issue): 類似の問題を自動的に検索し提示します。
  • Help (/help): すべての利用可能なツールのリストを提供し、対話的にトリガーを可能にします。

これらに加えてPR-Agent Proでは、以下の機能が提供されています。

  • Add Documentation (/add_docs): PRで変更されたメソッド、関数、クラスのドキュメントを生成します。
  • Generate Custom Labels (/generate_labels): ユーザーが定義したガイドラインに基づき、PRのカスタムラベルを生成します。
  • Analyze (/analyze): PRで変更されたコードコンポーネントを特定し、各コンポーネントに対して対話的にテスト、ドキュメント、コード提案を生成します。
  • Custom Prompt (/custom_prompt): ユーザーが定義したガイドラインに基づき、PRコードの改善に関するカスタム提案を自動生成します。
  • Generate Tests (/test component_name): PRコードの変更に基づき、選択されたコンポーネントの単体テストを自動生成します。
  • Improve Component (/improve_component component_name): PRで変更された特定のコードコンポーネントに対するコード提案を生成します。
  • CI Feedback (/checks ci_job): 失敗したCIジョブに対するフィードバックと分析を自動生成します。

Proでは、この追加機能やプライバシー、サポートの向上などがメリットとしてあげられています。追加機能では結果の改善のため、LLMロジックに加えて静的コード解析の使用を重視しているようです。

PR-Agentでは、主にPRに関するタイトルやラベル、ブランチなどの情報を整形し各機能を提供しています。/similar_issueについては、OpenAIのEmbeddings APIを使用して情報をベクトル化し、LanceDBもしくはPineconeにベクトルを保存、そこから類似度の高いIssueを検索するようにして機能を提供しているようです。

3. PR Guardianの概要

PR Guardianは、PR-AgentやCopilot pull requestと同様に、PRの概要の作成とコードの改善提案の機能を提供します。また、これらの機能はPR Guardianをダウンロードしてのローカルでの実行もしくはGitHub Actions上での実行することで利用できます。

4. PR Guardianの検証

PR Guardianの検証として以下のことを検証します。

  1. 概要作成機能
  2. コード改善提案機能

4.1 概要作成機能

PRの内容から、全体としてどのような変更があるか、概要を作成します。概要の作成にはエンドポイント/repos/{owner}/{repo}/pulls/{pull_number}/filesからPRのPatchを取得し、概要作成プロンプトと共に任意のLLMモデルに入力します。LLMからの出力をissueコメントに変換し、エンドポイント/repos/{owner}/{repo}/issues/{issue_number}/commentsにPOSTします。すべてのPRはissueであるため、このようにissueコメントとして作成します。

概要作成プロンプトの例を以下に示します。

あなたは、PR-Guardianと呼ばれるプルリクエストの内容を要約し、改善するためのツールです。
このコミットで行われた全体的な変更内容、および個別のファイルで行われた変更内容について、コードの違いに基づいて説明してください。
以下の出力フォーマットに沿ってプルリクエストの内容を要約してください。

出力フォーマット:
```
### タイトル

### 概要

### 変更内容
- 
- 
- 

### 影響範囲
- 
- 
- 

### 補足情報
- 
- 
```

この機能はPRに/describeとコメントすることで使用できます。/describeとコメントすると、以下のようなコメントが追加されます。

この機能によって作成される概要の妥当性を検証します。検証方法は以下のとおりです。

  • PRの概要が書かれている実在するリポジトリをクローンし、同じコミットを持つPRを作成する
  • 作成したPRに対して、PR Guardianで概要を作成する
  • クローン元のリポジトリのPR概要とPR Guardianで作成したPR概要とを比較する

今回はpsf/black #3661を対象に比較します。psf/blackではプルリクエストテンプレートが用意されており、それに基づいて概要が書かれています。

オリジナルの概要に関するコメントは以下のとおりです。

### Description

Hi! It looks like you have some special handling for `# type: ignore` comments to avoid moving said comments around due to positional meaning. I think this same special handling should also apply to [`# pyright: ignore` comments](<https://microsoft.github.io/pyright/#/comments?id=line-level-diagnostic-suppression>).

The use case for `# pyright: ignore` comments is being able to ignore Pyright-specific diagnostic rules that might not make sense in the context of more generic `# type: ignore` comments.

### Checklist - did you ...

- [x]  Add an entry in `CHANGES.md` if necessary?
- [x]  Add / update tests if necessary?
- [ ]  Add new / update outdated documentation?

これを日本語に訳すと以下のようになります。

### 説明

こんにちは!`# type: ignore`コメントに対して特別な処理を行い、位置的な意味合いのためにこれらのコメントが移動しないようにしているようです。同じ特別な処理を[`# pyright: ignore`コメント](<https://microsoft.github.io/pyright/#/comments?id=line-level-diagnostic-suppression>)にも適用するべきだと思います。

`# pyright: ignore`コメントのユースケースは、より一般的な`# type: ignore`コメントの文脈では意味をなさない場合があるPyright固有の診断ルールを無視することができるという点にあります。

### チェックリスト - 以下を行いましたか?

- [x]  必要に応じて`CHANGES.md`にエントリを追加しましたか?
- [x]  必要に応じてテストを追加/更新しましたか?
- [ ]  新しい/古いドキュメントを追加/更新しましたか?

オリジナルに関しては、プルリクエストテンプレートが用意されています。テンプレートではコメントの項目が細かく指定されているわけではなく、以下のようなおおまかな指示がなされています。

### Description

<!-- ここに記載すると良いことには、変更の理由(関連する問題があればリンクしてください)、注意すべき特筆すべき(またはハッキーな)選択、解決された問題の内容などがあります。長々とした話も歓迎します :) -->

それに対しPR-Guardianで作成した概要は以下のとおりです。

### タイトル

コメントの行保持に関するバグ修正とテストケース追加

### 概要

このプルリクエストでは、`Black`が`# pyright: ignore`コメントを含む行を分割しないようにする修正が行われています。それに伴い、関連するコードを更新し、新しいテストケースも追加しています。

### 変更点

- `CHANGES.md`
    - `Black`が`# pyright: ignore`コメントを含む行を分割しないように修正。
- `src/black/nodes.py`
    - `is_type_comment`関数が`# pyright: ignore`コメントも検出するように変更。
- `tests/data/preview/comments7.py`
    - `# pyright: ignore[reportGeneralTypeIssues]`コメントを含む新しいテストケースの追加。
- `tests/data/preview/prefer_rhs_split.py`
    - `# pyright: ignore`コメントの保護に関するテストケースの追加。
- `tests/data/simple_cases/comments6.py`
    - 各種`# pyright: ignore`コメントを含む行のテストケースの追加。

### 影響

これからは`Black`により、`# pyright: ignore`コメントを持つ行が誤って分割されることがなくなり、結果としてコーディングスタイルが維持されます。テストケースを追加することで、今後の変更に対する堅牢性も向上します。

### 追加情報

特になし。

これら二つを比較してそれぞれ固有のメリットを以下に列挙します。

  • オリジナル
    • PRに至った背景と意図が明確に示されている
    • 変更のない関連するコードに関する情報が示されている
  • PR Guradian
    • PRに含まれる変更点が漏れなく、個々のファイルについて示されている
    • 変更によって得られる影響が明確に示されている

4.2 コード改善提案機能

PRによって変更されたコードを確認し、改善できる箇所がある場合はそのコードの改善案を提示します。①概要の作成と同様にPRのPatchを取得し、改善提案プロンプトと共に任意のLLMモデルに入力します。②LLMからの出力の改善提案が正しいものかを検証します。③提案されたコードがプロジェクトのフォーマットに則っているものか、提案コードとプロジェクトの設定ファイルを共にLLMに入力します。④LLMからの出力をReviewコメントに変換し、/repos/{owner}/{repo}/pulls/{pull_number}/commentsにPOSTします。

①改善提案プロンプト

改善提案プロンプトの例を以下に示します。

あなたはPR-guardianであり、プルリクエスト(PR)のコード改善を提案することを専門とする言語モデルです。
このコミットで行われた全体的な変更、およびこの個別のファイルで行われた変更を、コードの違いに基づいて改善してください。

特定の指示:
- 改善が必要なコードに対して、改善された具体的なコードを提案に記載してください。
- 言語に基づいたコーディング規則およびコーディング規約に従うように変更してください。
- 冗長なプロセスを省略してください。
- 入力内の+で始まる行のみに焦点を当ててください。
- *.mdまたは.gitignoreファイルの変更に対しては改善提案を行わないでください。
- 結果の出力には"```yaml"のようなコードブロックを使用する必要はありません。
- 出力に対する説明を省略し、純粋なYAMLオブジェクトのみを生成してください。
- コード内の記号やインデントを正しく解釈するために、codeキーの|の後にコードを""内に配置してください。
- 記号、改行などを正しく解釈するために、改善理由をreasonsキーの""内に配置してください。
- 提案されたコードのインデントが、入力内の元のコードのインデントと正確に一致するようにしてください。
- "path:"の前にシーケンス記号"-"を挿入し、純粋なYAMLオブジェクトのみを生成してください。
- 各ファイルについて、その言語に対応する公式コーディング標準(例:Pythonの場合はPEP8)を検索し、その標準に従って修正を行ってください。
  言語に公式のコーディング標準がない場合は、標準ライブラリや類似のソースで使用されている規約を参照してください。
- 各言語について1つのコーディング標準のみを参照し、複数のファイルを修正する場合でも、同じ言語である限り、同じコーディング標準を参照してください。
- 改善すべきコードと改善提案が完全に同じ場合は、提案を行わないでください。

出力フォーマットはyaml形式で以下のように指定します。

code_suggestions: #リスト形式で複数のコード提案を含む。各提案は以下の項目を持つ。
  - path: #対象ファイルのパスを示す文字列。
	  start_line: #プルリクエストの差分でマルチラインコメントが適用される最初の行番号。
	  line: #コメントが適用されるプルリクエストの最後の行番号
	  original: #元のコードを示すマルチライン文字列。インデント付きで記述。
	  suggestion: #改善されたコードを示すマルチライン文字列。元のコードのフォーマットに基づく。

②改善提案の検証

フォーマットに沿って出力された結果が正しいかどうかを検証します。検証内容としては以下のとおりです。

  • 提案しているファイルが存在するか
  • 提案元のコードは存在するか
    • それは何行目に存在するのか

基本的にこの改良提案プロンプトでの出力は、ファイルのパスは間違うところは見たことがない、提案元のコードはほぼ存在する、行数はよく間違う(数行ずれる)という印象です。そのため、コードの確認と行数の確認のを重点的に行なっています。

実際のコードと出力の提案元のコードは構文としては同じことがほとんどです。しかし、ながらインデントがズレたものが出力されることが少なからずあります。出力がズレるのは行頭のインデントのみであり、実際のコードは適切にフォーマットされているものであるとすると、ここでの検証は、単純に、それぞれのコードから行頭のスペースを除いたコードで比較を行います。

提案元のコードが実際のコードから見つかった場合、提案コードに対してインデントの調整を行います。これは、提案コードも提案元コードと同様に行頭のインデントがずれている場合が多いためです。コードの検索と共に元のコードのインデントを記録し、コードが見つかった時点のインデントを提案コードに対して適用します。

③プロジェクトのフォーマットに準拠しているかの確認

存在することが確認された提案コードがプロジェクトで使用しているlitnerやformatterのフォーマットに準拠しているかを確認します。初めのコードの提案時にフォーマットの確認をすることもできますが、コードの提案時にフォーマットまで確認するよりも、コードの提案とフォーマットの確認を分割した方が精度が良いためこのようにしています。現在、対応している言語のPythonにおいては、リポジトリの.pyproject.tomlがある場合はそれを取得し、提案コードと以下のプロンプト共にLLMに入力します。

個々のコード改善に関する情報(該当する行、改善前のコード、提案されたコードなど)を含むYAML形式のデータと、
そのコードに使用されるフォーマッタまたはリンタに関する情報を含むプロジェクトの設定ファイルの内容が提供されます。
この情報を使用して、提案されたコードが設定ファイルで指定されたフォーマットに準拠しているかどうかを確認します。
結果に応じて、以下の指示に従ってください:

提案されたコードが準拠している場合:
コード改善に関する情報をそのまま返します。
提案されたコードが準拠していない場合:
提案されたコードを指定されたフォーマットに準拠するように修正し、修正後のコード改善に関する情報を返します。

具体的な指示:

	•	結果の出力には “```yaml” のようなコードブロックを使用する必要はありません。
	•	出力に対する説明を省略し、純粋なYAMLオブジェクトのみを生成してください。
	•	記号やインデントを正しく解釈するために、original および suggestion キーの | の後にコードを “” 内に配置します。
	•	文字、改行などを正しく解釈するために、reasons キー内の改善理由を “” 内に配置してください。
	•	提案されたコードのインデントが入力の元のコードのインデントと正確に一致するようにしてください。
	•	“path:” の前にシーケンス記号 “-” を挿入してください。

④LLMからの出力をReviewコメントに変換

フォーマットの確認が行われた出力をPRのコメントとして投稿します。LLMからの出力はコード提案時のフォーマットと同様です。PRのコメントにはいくつか種類がありますが、diff表示のできるreview commentとして投稿します。review commentにはpathや行番号などと共に、変更点と変更理由をBodyとして含めます。変更点を““suggestion`で始まるコードブロックで記述することで、行番号から参照した変更前の部分とともに変更点を以下の図のように示すことができます。

機能の検証

この機能によって行われる改善提案の検証を行います。検証内容としては、プロジェクトのフォーマットに準拠しているかの検証および、同じPRに対する、人のレビューとPR Guardianの結果の比較です。

プロジェクトのフォーマットに準拠しているかを検証するために、例として、linterおよびformatterにRuffを使用しているプロジェクトを作成します。このプロジェクトの.pyproject.tomlを以下のように設定します。

[tool.ruff.lint]
ignore = [
  "E501",  # line too long
  "E722",  # do not use bare 'except'
  "E731",  # do not assign a lambda expression, use a def
]
select = [
  "F",  # pyflakes
  "FA",  # flake8-future-annotations
  "I",  # isort
  "UP",  # pyupgrade
  "W",  # pycodestyle warnings
]

[tool.ruff.format]
quote-style = "double"

設定の検証のため、以下のような部分を含むPythonファイルをコミットします。このファイルにはRuffのE501、E722、E731に該当するコードが含まれています。また、提案をさせるために不要な空文字の代入や0の代入をしています。

very_long_variable_name = ""
very_long_variable_name = "This is a very long line of code that exceeds the maximum line length limit, which is typically set to 79 or 88 characters."

try:
    x = 1 / 0
except:
    error_num = 2
    print("An error occurred. Error number: " + str(error_num))

lambda_function = 0
lambda_function = lambda x: x + 1

このコードに対してPR Guardianの改善提案を実行します。②改善提案の検証 後には以下のような結果が得られました。ここでは不要な代入の削除とf-stringの使用を提案しているようです。f-stringの使用を提案しているコードはではなく が使われています。

@@ -19,2 +19,1 @@
-very_long_variable_name = ""
-very_long_variable_name = "This is a very long line of code that exceeds the maximum line length limit, which is typically set to 79 or 88 characters."
+very_long_variable_name = "This is a very long line of code that exceeds the maximum line length limit, which is typically set to 79 or 88 characters."

@@ -24,3 +23,3 @@
except:
    error_num = 2
-   print("An error occurred. Error number: " + str(error_num))
+   print(f'An error occurred. Error number: {error_num:02}')

@@ -28,2 +27,1 @@
-lambda_function = 0
-lambda_function = lambda x: x + 1
+lambda_function = lambda x: x + 1

この改善提案に対して、フォーマットの確認が行われ、その後PRにコメントが投稿されます。コメントではプロジェクトの設定通りにE501、E722、E731に該当するコードはそのまま出力され、quote-style=”double”に従ってだった部分がに修正されています。この結果からフォーマットに準拠した提案ができていることが確認できました。

続いて、人のレビューとPR Guardianとを比較します。検証方法は概要作成と同様です。PRの例として、psf/blackの#4153を使用します。

元のPRでは以下のようにcomments.pyとlinegen.pyについて以下のようなレビューが行われています。

対して、PR Guardianでは以下のような結果が得られました。

PR GuardianではこのPRで主に議論がなされているcomments.pyについての提案はなく、そのほかのファイルの細かな部分についての提案がされました。人間による提案では、「以前のフォーマットの方が好ましい」「(そのフォーマットは)好きではない、個人的にはこれをよく使います」のような議論が交わされています。この、以前のフォーマットに関しては、今回のdiffからは取得できない部分もあり、PR Guardianには理解ができなかった可能性があります。また、PR Guardianにとってはこのコードの良い、悪いの判断が難しくコードの提案が出来なかった可能性が考えられます。

PR Guardianの提案としては、条件の正確さやコードの読みやすさに関する提案がされています。また、提案コードのインデントのズレなどもなく、そのまま提案をコミットすることも可能です。このプロジェクトではblackが使用されていますが、Ruffが使用されているリポジトリのconda/condaでも同様にコードの改善提案を行ったところpyproject.tomlで設定されている通りの結果を得ることができました。

5. 所感

今回の開発、検証を通してPR GuardianやLLMを利用したツールについて以下のような感想を抱きました。

現在のPR GuardianはPRに該当するPatchのみを情報として入力しています。その結果、人によるコメントやレビューとはある程度の差が出てきています。PRの範囲からでは読み取れない背景、より具体的に言うとプロジェクトのコード内で統一されている書き方などによるものです。ここに関してはPRの名前や関連のコミットやユーザによる適当な説明などがあれば補えるものであると考えられます。一方でPR Guardianによる概要説明、レビューはPRの内容のみにスコープをあて無駄な情報のない純粋なものであるとも言えます。これに関してはPR Guardianやその他ツールの運用上の立ち位置によるものであるとは思います。ただ、性能が低下しない限りは情報は多く入力できた方が、的外れなものである可能性は下がるはずです。また、PR Guardianにはアウトプットの質がある程度一定であるという利点があります。PR Guardianには示すべき内容が明確に指示されているため、ある程度は同等のものが出力されます。

PR Guardianの概要や改善提案に全く間違いがないとは言えません。これはcopilot pull requestでも同様のことが書かれています。

Copilot が学習できる入力とコンテキストが多いほど、出力は向上します。 ただし、この機能はまだ新しいため、生成される概要が正確な精度に到達するには時間がかかります。 一方で、生成された概要があまり正確ではない場合には、この説明が含まれた pull request を保存して公開する前にユーザーが変更を加える必要があります。 さらに、Copilot が不正確なステートメントを生成する “幻覚” のリスクもあります。 これらの理由から、見直しは必須であり、出力の見直しを慎重に行うことを Microsoft チームとして強くお勧めします。

PR-Agentも提案するコードの品質について注意を記載しています。この注意の日本語訳を以下に示します。

コード提案の品質についての注意

現在のAI(GPT-4など)は進化していますが、完璧ではありません。提案はすべてが完璧であるわけではなく、自動的にすべてを受け入れるべきではありません。批判的な読み取りと判断が必要です。

AIのミスは稀ですが、改善ツールの提案は、PR作成者のミスやバグを高確率で検出するのに有益です。提案を確認するために30~60秒をかけることをお勧めします。

提案の階層構造は、ユーザーが素早く理解し、関連性のあるものを判断できるように設計されています:

  • カテゴリヘッダーが関連している場合のみ、要約された提案の説明に進む。
  • 要約された提案の説明が関連している場合のみ、折りたたまれている部分を開き、コードのプレビュー例を含む提案の完全な説明を読む。

LLMによるレビューツールは、人によるチェックの仕方など活用方針を定めておくことで、ツールの導入によるメリットを最大限に享受することができると考えられます。

6. おわりに

今回は、PRの概要作成とコードの改善を提案するPR Guardianの開発と検証を行いました。言語対応や精度の向上など課題は残りますが、ツールの運用方法を含め適切な機能が提供できるよう開発を進めていきたいと思います。

参考文献

pr-agent: https://github.com/Codium-ai/pr-agent

psf/black: https://github.com/psf/black

conda/conda: https://github.com/conda/conda

ブログ一覧へ戻る

お気軽にお問い合わせください

SREの設計・技術支援から、
SRE運用内で使用する
ツールの導入など、
SRE全般についてご支援しています。

資料請求・お問い合わせ