copilot-instructions.mdは使えるぞ!実戦投入レポート
こんにちは、JX通信社でシニアエンジニアをしているSirosuzumeです。
先日、GitHub Copilotの新機能として、リポジトリのルートから.github/copilot-instructions.md
を読み込み、対話型UIの出力に反映する機能が追加されました。
この機能を使ってみて、どのような効果があるのか、どのように活用するのが良いのか、実際に新しく立ち上がったプロジェクトで試してみた結果を共有したいと思います。
忙しい人向けの結論
- 0 → 1 でコードを生成する場合、サジェストを使うよりCopilotを有効活用できるかもしれない
- Hygenなどのコードジェネレーターのテンプレートを運用するより、copilot-instructions.mdを更新する使い方のほうが楽そうだと感じている
- ファイル自体の生成機能はないので、そこだけ既存のコードジェネレーターを併用するのが良さそう
copilot-instructions.mdの効果の検証
まずは実例を見てみましょう。
src/feature/entry-horse/presentational/list-item.tsx
というファイルがあったとします。
この中にはEntryHorseListItem
という名前のコンポーネントがあり、PropsはEntryHorseListItemProps
という名前で定義されているとします。
今回、このコンポーネントのUnitテストを作りたいため、src/feature/entry-horse/presentational/list-item.test.tsx
というファイルを作成しました。
copilot-instructions.mdがない場合
楽をしようと考えて、GitHub Copilotの対話型UIを使用し「テストを作って」とお願いしてみたところ、以下のコードが生成されました。
import { render, screen } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import { EntryHorseListItem } from './list-item';
describe('EntryHorseListItem', () => {
it('renders the name prop correctly', () => {
render(<EntryHorseListItem name="Thunderbolt" />);
expect(screen.getByText('Thunderbolt')).toBeInTheDocument();
});
it('renders another name prop correctly', () => {
render(<EntryHorseListItem name="Lightning" />);
expect(screen.getByText('Lightning')).toBeInTheDocument();
});
});
しかし、私はこのコードを使う気にはなれませんでした。
- 後だしになって申し訳ないのですが…実はこのプロジェクトは
vitest
ではなくjest
を使っています。 - それに開発者は全員日本語話者のため、テストケースは日本語で書かれていてほしいのです。
- あと
toBeInTheDocument
はtoBeVisible
に変えてほしいです。
といった具合で、GitHub Copilotが生成したコードは、そのまま使えるということは殆どありません。
そもそもの話ですが、GitHub CopilotはあくまでAIであり、プロジェクトの背景情報を知りません。
開発者が全員日本人であるとか、jest
を使っているとか、そんな情報をGitHub Copilotは持っていないのです。
それを教えるための手段として、copilot-instructions.md
を読むという機能が追加されました。
では次にcopilot-instructions.md
に、上の情報を追加して出力してもらいましょう。
copilot-instructions.mdがある場合
copilot-instructions.md
には以下の内容を記載します。
# コード規約
このプロジェクトはtest runnerに`jest`を使っています。
開発者は日本語話者のため、テストケースは日本語で書いてください。
テストはdescribeでグループ化し、itでテストケースを書いてください。
`toBeInTheDocument`より`toBeVisible`を優先して使ってください。
そして、GitHub Copilotに「テストを作って」とお願いしてみたところ、以下のようなコードが生成されました。
import React from 'react';
import { render, screen } from '@testing-library/react';
import { EntryHorseListItem } from './list-item';
describe('EntryHorseListItemコンポーネント', () => {
it('名前が表示されることを確認する', () => {
render(<EntryHorseListItem name="サンプルホース" />);
const nameElement = screen.getByText('サンプルホース');
expect(nameElement).toBeVisible();
});
it('空の名前が表示されることを確認する', () => {
render(<EntryHorseListItem name="" />);
const nameElement = screen.getByText('');
expect(nameElement).toBeVisible();
});
});
きちんとjest
を使っていて、テストケースも日本語で書かれていて、toBeInTheDocument
もtoBeVisible
に変更されています。
細かい点ではもっとこうしてほしい、というところはありますが、都度copilot-instructions.md
に追記していくことで、かなりプロジェクトに合わせたコードを生成してくれるようになります。
2024-11時点での課題
copilot-instructions.md
を100行以上書き込み、人間が読んでも役に立つくらいのノウハウ集にすると、対話型UIが出力してくれるコードはかなり精度が高くなります。
しかし、この機能にはまだ大きな弱点があります。サジェストの出力には未対応だということです。
おそらく、コードを書くのに慣れている人ほど、対話型のUIは使わず、サジェストをちょっと賢い予測変換機能として使っていることが多いのではないでしょうか。
私もどちらかといえばそのタイプで、これまでほとんど対話型UIは使用していません。
ものは試しにと、一日ほど対話型UIをつかってコーディング作業をしてみましたが、やはりサジェストを活用したほうが早くコーディングできるという結論に至ってしまいました。
ただ、使用しているうちに、対話型UIがサジェストよりも勝っていると感じられた点を見つけました。
それはゼロからコードを出力する場合です。
サジェスト機能は、現在開いているファイルと、他に開いているタブの内容を元に、カーソル位置に入るコードを推測し出力しています。
質の高いサジェストを出力させるには、importするパッケージやファイルを先に指定したり、お手本となるファイルを別のタブに開いたりするなど、小ワザを活用する必要もあります。
一方、copilot-instructions.md
を使用して出力したコードは、importするパッケージを正確に推測することができたり、現在のフォルダと命名規則から思った通りの関数名を出すことができます。
この特性は、HygenやTurborepのコードジェネレーション機能のテンプレートを凝って保守するよりも、楽でかつ柔軟な運用ができると感じました。
コードジェネレーターとしてcopilot-instructions.mdを運用する
既存のコードジェネレーターに抱えていた課題
私は普段、Reactのコンポーネントや、それに付随するテストファイルを作成するときに、Hygenを活用することが多いです。
言語を問わずに使えることや、テンプレートのカスタマイズ性の高さが魅力ですが、テンプレートの保守に課題を感じていました。
生成されたコードがエラーをなるべく吐かないようにしたり、命名規則を一定にしたり、条件によって分岐したり。
出力に使用する.ejs.t
ファイルも、可読性が良いとは言えず、IDEの支援も受けにくいです。
便利にしようとするほど、入力しなければいけない項目も増えていくため、メンテしている本人以外は使いこなすのが難しいという問題も出てきます。
copilot-instructions.mdをコードジェネレーターとみなす場合
copilot-instructions.md
は自然言語で書くことができるため、可読性の面では問題ありません。
条件分岐もAI側にある程度お任せすることができます。
コードジェネレーターとしてCopilotを活用する場合、この3つの要素が活用しやすいと考えます。
- 現在のフォルダ、ファイル名
- 生成するコードの役割を推測させるのに役に立つ
-
copilot-instructions.md
- コード規約やノウハウ集に近いものになる
- 生成時にユーザーが入力する文章
- 細かいオーダーがある場合に使用する。なるべくこの要素の比率を少なくする
出力するファイル、使用する機能に応じてセクショニングする
copilot-instructions.md
はMarkdownの形式で記載することができます。
全ての内容を並列に記載するのではなく、セクションごとに分けて記載することで、Copilotがどの部分に対してどのようなコードを生成すべきかを判断しやすくなります。
- fetch関連のコードのルール
- コンポーネントのテストコードのルール
- モックデータの生成関数のルール
といった具合に、こういうコードを書くときはここを参考にしてくださいと例示します。
プロジェクトのフォルダ構成から関数名、ファイル名を推測できるようにする
フォルダのやファイル、関数の命名規則を統一し、その内容をcopilot-instructions.md
に記載しておくと、Copilotが精度の高い関数名やimportを考えてくれるようになります。
例えばEntryHorse
というドメインがある場合
## フォルダ構成
あるドメインに所属するコードは`src/features/{{ ドメイン名(ケバブケース )}}`の下に格納します。
Mockデータの生成関数は`src/feature/entry-horse/mock.ts`に格納されており、`generateMock`というprefixで始まるMockデータ生成関数が存在しています。
コンポーネントは`src/feature/entry-horse/components`に格納されています。
コンポーネントは基本的に`{{ ドメイン名 }}{{ Role }}`の形式で命名されており、Propsは`{{ ドメイン名 }}{{ Role }}Props`という形式で命名されています。
のような情報をcopilot-instructions.md
に記載した上でsrc/feature/entry-horse/components/list.tsx
内で「コードを書いてください」と指示すると
以下のようなコードが出力されます。
import type React from "react";
type EntryHorseListProps = {};
const EntryHorseList: React.FC<EntryHorseListProps> = (props) => {
return <div>{/* Render your component here */}</div>;
};
export default EntryHorseList;
上手く生成されたときの例を記載する
プロンプトエンジニアリングなどでもある手法ですが、具体的な例をMarkdownに提示してあげると、より精度の高いコードを生成します。
細かくフィードバックをして更新する
生成されたファイルが期待通りではない場合、随時copilot-instructions.md
に追記していくことで、Copilotが生成するコードの精度を向上させることができます。
ファイルやフォルダ自体の作成には、既存のコードジェネレーターを利用する
GitHub Copilotはファイルやフォルダ自体を生成することはできません。
決められたフォルダ構成やファイル名でファイルを生成するためには、既存のコードジェネレーターを使うことが有効です。
併用することで、コードの初期開発のスピードが上がります。
生成の命令時に指示を追加して使う
この点が既存のコードジェネレーターには特にないメリットだと感じています。
copilot-instructions.md
に書ききれない、少し例外的な関数が必要だとしても、生成時に注意点としてその旨を追記しておくことで柔軟な対応が可能です。
例えばこのコンポーネントはforwardRef
を使う必要があるという場合、「forwardRefを使ってください」という指示にするだけで、対応が可能になります。
まとめ
サジェストの出力に未対応である点が解決されれば、この機能の価値は相当に高まるように思えます。
またcopilot-instructions.md
を育てていくことは、人間の開発者にとっても副次的な効果があるのではないかと予想しています。
自然言語で書くことができ、積極的に更新していくモチベーションにもなるため、開発者間にとっても有益なノウハウ集や、実効性のあるコード規約集として活用できる可能性があるのではないかと感じています。
Discussion