Copilotを“最強の相棒”に育てる!copilot-instructions.mdでアーキテクチャルールを教え込んでみた

AIによるコーディング支援ツール「GitHub Copilot」。非常に便利で、今や開発に欠かせない相棒となりつつあります。しかし、時々「そうじゃないんだよな…」というコードを提案されて、手戻りが発生することはあります。

たとえ個人開発であっても、一貫性を保つために最低限のアーキテクチャや命名規則は設けたいもの。しかし、Copilotがその「暗黙のルール」を理解してくれず、生成されたコードを結局手直しする…という手間が発生しがちです。

そんな悩みを解決する機能として、Copilotにプロジェクト固有のルールを教え込むためのファイル copilot-instructions.md があるそうです。

参考:GitHub Copilot のリポジトリ カスタム命令を追加する

今回は、この copilot-instructions.md を作成し、どれほどCopilotが「自分の思っている通り」にコードを書き出してくれるのかを試してみました。本記事では、その際に作成した指示書の内容と、実際に使ってみた結果をレポートします。

copilot-instructions.md とは? プロジェクトの「ルール」をAIに教える

copilot-instructions.md は、リポジトリのルートや .github ディレクトリに配置するマークダウンファイルです。GitHub Copilotは、このファイルの内容を読み込み、コードを生成する際の指示として利用します。

ルールをAIにしっかり教え込むことで、より一貫性があり、プロジェクトのルールに準拠したコードを生成してくれるようになります。

今回作成した「ルール」の紹介

今回、私が作成した copilot-instructions.md の主な内容は以下の通りです。

copilot-instructions.md.github/ の配下に作成しています。

1. アーキテクチャと依存関係のルール

まず、プロジェクトの全体像を理解してもらうために、ディレクトリ構成と技術スタックを明記しました。

calculate-back/
├── app/
│   ├── domain/
│   │   ├── model/
│   │   └── service/
│   ├── infrastructure/
│   │   ├── model/
│   │   └── repository/
│   ├── schema/
│   └── util/
├── docker-compose.yml
├── Dockerfile
├── main.py
└── requirements.txt

そして、最も重要なのが**各レイヤー間の依存関係(呼び出しルール)**です。クリーンアーキテクチャの考え方に基づき、依存性の方向を厳密に定義しました。

  • schema/ (APIの入出力層) は domain/ (ビジネスロジック層) を呼び出せる
  • domain/infrastructure/repository/ (データ永続化層) を呼び出せる
  • 禁止事項: 上記以外の方向への呼び出しは禁止(例: domain から infrastructure/model の直接呼び出しはNG)

このルールをCopilotに教えることで、「この処理はここに書くべきではない」というアーキテクチャレベルの規約を理解してくれることを期待しました。

2. 厳密な命名規則

チーム開発では、命名規則の統一がコードの可読性を大きく左右します。そこで、ファイル名、クラス名、関数名、変数名、定数名それぞれについて、具体的な命名規則(スネークケース、キャメルケースなど)と、良い例・悪い例を提示しました。

ファイル名: スネークケース (user_service.py) クラス名: キャメルケース (UserService) 関数名・変数名: スネークケース (get_user_by_id) 定数名: 大文字スネークケース (MAX_RETRIES)

これにより、誰が書いても(Copilotが書いても)、一貫したスタイルのコードになることを目指します。

3. main.pyの役割を限定

main.py は、FastAPIアプリケーションのエントリーポイントですが、ここにビジネスロジックが書かれてしまうと、コードが肥大化し、見通しが悪くなります。

そこで、main.pyの役割を「ルーティングのみに限定し、実際の処理は必ずapp/schemaに移譲する」というルールを明確にしました。

# GitHub Copilotへの指示書

このファイルは、GitHub CopilotやAIアシスタント、開発者がプロジェクトの構造・コーディング規約・命名規則を理解し、一貫したスタイルでコード生成・修正を行うためのガイドです。

---

## 1. プロジェクト概要

### 技術スタック
- Python (main.py, requirements.txt)
- Docker (Dockerfile, docker-compose.yml)

### ディレクトリ構成
calculate-back/
├── app/
│   ├── domain/
│   │   ├── model/
│   │   └── service/
│   ├── infrastructure/
│   │   ├── model/
│   │   └── repository/
│   ├── schema/
│   └── util/
├── docker-compose.yml
├── Dockerfile
├── main.py
├── requirements.txt

## 2. アーキテクチャルール

### 各ディレクトリの役割

- **`app/`**: アプリケーションの主要なコードを格納します。
    - **`domain/`**: ビジネスロジックの中核をなすドメイン層です。
        - `model/`: **ドメインモデル**を定義します。スキーマから受け取ったデータを扱うオブジェクトです。
        - `service/`: **ドメインサービス**を実装します。具体的なビジネスロジックをここに記述します。
    - **`infrastructure/`**: データベースや外部APIなど、技術的な詳細を扱うインフラ層です。
        - `model/`: **永続化モデル**(例: ORMモデル)を定義します。
        - `repository/`: **リポジトリ**を実装します。データベースへのCRUD操作など、データの永続化処理を担当します。
    - **`schema/`**: APIの入出力など、データの受け渡し形式(DTO)を定義します(例: Pydanticモデル)。ビジネスロジックは含めず、`domain/service/`に処理を移譲します。
    - **`util/`**: 特定の層に依存しない共通のユーティリティ関数(モデル変換など)を格納します。

#### 呼び出しルールの制約 (依存関係)

コードの依存関係は、以下のルールに厳密に従ってください。

- **`schema/` から呼び出し可能:**
    - `domain/`
    - `util/`
- **`domain/` から呼び出し可能:**
    - `infrastructure/repository/`
- **禁止事項:**
    - `domain/` から `infrastructure/model/` を直接呼び出すことは**できません**。
    - `infrastructure/` から `infrastructure/` 以外を直接呼び出すことは**できません**。
    - `infrastructure/model/` から `infrastructure/model/` 以外を直接呼び出すことは**できません**。

### 作成に関する禁止事項
- 現存するディレクトリ以外に新しいディレクトリを作成しないでください。

---

## 3. コーディング規約

### 命名規則

- **言語**: ファイル名、ディレクトリ名、変数名などは全て英語で記述してください。
- **基本**: `user_service.py` のように、役割が明確にわかる名前を付けます。
- **ファイル名**:
    - **形式**: スネークケース (`snake_case`) に統一します(例: `my_file.py`)。
    - **命名**: 所属するディレクトリの役割を表す名前を含めます(例: `app/domain/model/` 配下なら `user_model.py`)。
- **クラス名**: キャメルケース (`CamelCase`) を使用します(例: `UserService`)。
- **関数名・変数名**: スネークケース (`snake_case`) を使用します(例: `get_user_by_id`, `total_price`)。
- **定数名**: 大文字のスネークケース (`UPPER_SNAKE_CASE`) を使用します(例: `MAX_RETRIES`)。

#### 良い例・悪い例

- **ファイル名**
    - 良い例: `user_service.py`, `order_model.py`, `user_model.py`(`app/domain/model/`配下)、`order_repository.py`(`app/infrastructure/repository/`配下)
    - 悪い例: `UserService.py`, `OrderModel.PY`, `userservice.py`, `user.py`、`repository.py`、`model.py`(役割や所属が曖昧なもの)
- **クラス名**
    - 良い例: `UserService`, `OrderModel`
    - 悪い例: `user_service`, `order_model`, `userservice`
- **関数名・変数名**
    - 良い例: `get_user_by_id`, `total_price`
    - 悪い例: `GetUserById`, `TotalPrice`, `getUserById`
- **定数名**
    - 良い例: `MAX_RETRIES`, `DEFAULT_TIMEOUT`
    - 悪い例: `maxRetries`, `defaultTimeout`, `Max_Retries`

### 可読性

- 複雑なロジックやコードの意図を明確にするため、コメントを追加してください。
- ファイルや関数の冒頭には、その役割を簡潔に説明するコメントを記述します。
- 適切なインデントと空白を使い、コードの構造を視覚的に分かりやすくしてください。

### ドキュメンテーション (Docstrings)

- 全ての公開されているクラスと関数には、引数、戻り値、送出する例外を明記したDocstringを追加してください。
- コードを修正した際は、関連するDocstringも必ず最新の状態に更新してください。
- **良い例 (Do):**
    ```python
    def calculate_total_price(items: List[Item]) -> float:
        """Calculate the total price of a list of items.

        Args:
            items (List[Item]): A list of items to calculate the total price for.

        Returns:
            float: The total price of the items.

        Raises:
            ValueError: If the items list is empty.
        """
        if not items:
            raise ValueError("Items list cannot be empty.")
        return sum(item.price for item in items)
    ```

---

## main.pyの役割・ルール

- main.pyはルーティングのみを担当し、実際の処理は必ずapp/schemaに移譲すること。
- インターフェースはGraphQLとし、GraphQLのエンドポイントはapp/schemaで定義したスキーマを利用する。
- main.pyでビジネスロジックやデータ処理を直接実装しないこと。
- ルーティング以外の責務(バリデーション、ドメインロジック、永続化処理等)はapp/schema以下の各層で実装すること。

**良い例**
- GraphQLスキーマはapp/schema/配下で定義し、main.pyからimportしてGraphQLRouterに渡す。
- main.pyはFastAPIアプリケーションのエントリポイントとしてのみ機能する。
- main.pyでは、GraphQLのエンドポイントを設定し、リクエストをapp/schemaに移譲する。
**悪い例**
- main.pyにビジネスロジックやデータベースアクセスのコードを直接書くこと。
- main.pyでGraphQLスキーマを定義すること。
- main.pyで直接データのバリデーションや変換を行うこと。

実践!Copilotは指示を守ってくれるのか?【動画あり】

さて、いよいよ実践です。 この copilot-instructions.md をプロジェクトに配置した状態で、実際にCopilotにコードを生成させてみました。

その様子を撮影したのが、こちらの動画です。指示をすると、Copilotがアーキテクチャルールに従い、複数のファイルにまたがるコードを生成する様子がわかります。

動画での注目ポイント:

  • 複数ファイルを横断して生成: schema, service, repository など、役割に応じたディレクトリに新しいコードが追加・修正されています。
  • アーキテクチャの遵守: main.py にはロジックを書かず、service にビジネスロジックを、schema にAPI定義を、というルールが守られています。
  • 命名規則:ファイル名やクラス名など、指示書で「良い例」として具体的に示したものは高精度で守ってくれましたが、時々ルールから外れることもありました。

このように、事前にルールを教え込むだけで、Copilotがある程度「ルール」を守ってくれることが確認できました。

使ってみてわかったこと・考察

copilot-instructions.md を導入した結果、期待以上にCopilotが「自分の思っている通り」にコードを書き出してくれました。

良かった点

  • アーキテクチャを理解した提案: 最も驚いたのは、レイヤー間の依存関係を考慮したコードを生成してくれた点です。以前は平気で main.py にロジックを書いていましたが、指示後はきちんと schemaservice に処理を分離するような提案が増えました。
  • 命名規則の遵守: ファイル名や関数名の命名規則も、ある程度、指示通りに生成してくれました。これにより、コードレビューでの細かい指摘が減りそうです。
  • 思考の壁打ち相手になる: 「こういう機能を追加したい」とコメントを書くと、指示書に基づいた実装の雛形をすぐに作ってくれます。単なるコード補完ツールから、設計を理解したアシスタントに進化した感覚です。

課題・うまく使うためのコツ

  • 指示は具体的に: 曖昧な指示よりも、今回のように「良い例・悪い例」を具体的に示す方が、Copilotの理解度は格段に上がると感じました。
  • 万能ではない: もちろん、全ての指示を完璧に理解するわけではありません。特に、非常に複雑なビジネスロジックや、複数のファイルを横断するような大規模な修正は、まだ人間のサポートが必要です。
  • 継続的なメンテナンス: ルールが変わったら、この指示書も追従してメンテナンスしていく必要があります。

まとめ

copilot-instructions.md は、GitHub Copilotを「ただの便利なツール」から「プロジェクトを深く理解した開発パートナー」へと進化させる、非常に強力な機能です。

最初は指示書を作成する手間がかかりますが、一度作ってしまえば、その後の開発効率の向上、コード品質の均一化、新メンバーのオンボーディングコスト削減など、計り知れないメリットがあると感じました。

皆さんも、ご自身のプロジェクトでCopilotを「最高の開発パートナー」に育てるべく、オリジナルの「ルール」を作ってみてはいかがでしょうか。