🔰

こういうのでいいんだよ、なCopilotの使い方を考える

に公開

AIコーディングややアンチ気味だったのですが、世も世なので少しちゃんと使ってみることにしました。
と言ってもGitHub Copilotのオートコンプリートとチャットモードはもともと使っており、
今回エージェントモードを使い始めた、というところです。
弊社では相談・申請すればコーディング系AI利用の費用を出してもらえるので助かります。

結論

  • モジュールの切り出し系のリファクタリング
  • エラーハンドリングの統一
  • 設定管理の統一

はagentに9割任せることにします。

背景

  • データ管理系の運用作業のためのStreamlit(Python)アプリを開発している。
  • すでに一個だけ小さめの機能が実装してある。
  • 大きめの機能をこれから実装する予定である。

やったこと

開発中のアプリに実装済みの機能は動けば良いというつもりで作ったので、
新機能を追加することで保守性が最悪になることは目に見えていました。
Tidy First?を思い出して、先に将来の変更容易性を高めるためにコードの整頓をすることにしました。
GitHub Copilot/Claude Sonnet 4を使ってみました。

リファクタリング

作業開始前はmain関数にほとんど全てのロジックを書いてしまっていたので、
モジュールの切り出しから行いました。
一応レビューはしたかったので少しずつ作業を進めてもらうようにしました。

メソッド切り出し

プロンプト

UIコンポーネントのレンダリングとデータ処理を関数化して、main関数のコード量を減らして。

コード(before)

import streamlit as st

def render_hoge_function_ui():
    # Step 1
    st.header("hoge")
    hoge = fetch_hoge()
    st.dataframe(hoge)

    # Step 2
    # 以下省略

コード(after)

import streamlit as st

def _render_hoge_step1():
    st.header("hoge")
    hoge = fetch_hoge()
    st.dataframe(hoge)
    return hoge

def _render_hoge_step2():
    # 以下省略

def render_hoge_function_ui():
    # Step 1
    hoge = _render_hoge_step1()

    # 以下省略

ファイル分割

プロンプト

UIコンポーネントのレンダリングとデータ処理の関数を別モジュールに分けてファイルも分割して。

ディレクトリ構造(before)

lib/operations/
├── hoge_function.py

ディレクトリ構造(after)

lib/operations/hoge_function/
├── __init__.py              # モジュール初期化(render_hoge_function_ui関数のエクスポート)
├── main.py                  # メイン処理(render_hoge_function_ui関数を定義)
└── ui/                      # UIコンポーネント
    ├── __init__.py
    ├── file_upload.py       # ファイルアップロード処理
    ├── data_preview.py      # データプレビュー表示
    ├── data_editor.py       # データ編集機能
    └── csv_export.py        # CSV出力機能

エラーハンドリングの統一

プロンプト

他に今の段階で整頓しておいて方がいい箇所はある?

> エラーハンドリングの統一、設定管理、〇〇〇〇などがあります

エラーハンドリングの統一をして。

こんな感じのモジュールを作ってくれました。
こんな書き方あるのか〜と勉強になりました。

def handle_error(error: Exception, fallback_value: Any = None, show_user_message: bool = True) -> Any:
    """
    エラーを処理し、適切なユーザーメッセージを表示する
    
    Args:
        error: 発生したエラー
        fallback_value: エラー時に返すデフォルト値
        show_user_message: ユーザーメッセージを表示するかどうか
        
    Returns:
        fallback_valueまたはNone
    """
    if isinstance(error, OperationError):
        # カスタム例外の場合
        logger.error(f"{type(error).__name__}: {error}", exc_info=error.original_error)
        if show_user_message:
            st.error(error.user_message)
    else:
        # その他の例外
        logger.error(f"Unexpected error: {error}", exc_info=True)
        if show_user_message:
            st.error("予期しないエラーが発生しました。しばらく待ってから再試行してください。")
    
    return fallback_value

def error_boundary(fallback_value: Any = None, show_error: bool = True):
    """
    デコレータ:関数にエラーバウンダリを設定する
    
    Args:
        fallback_value: エラー時に返すデフォルト値
        show_error: エラーメッセージを表示するかどうか
    """
    def decorator(func: Callable[..., T]) -> Callable[..., Optional[T]]:
        @wraps(func)
        def wrapper(*args, **kwargs) -> Optional[T]:
            try:
                return func(*args, **kwargs)
            except Exception as e:
                return handle_error(e, fallback_value, show_error)
        return wrapper
    return decorator

こんな感じにしてメソッドを実装すれば、エラーハンドリングを統一できます。
.github/instructions.mdにも今後は原則これを使うように記述しました。
(記述するようにCopilotに指示しました。)

@error_boundary(fallback_value=None)
def your_function():
    # 実装

設定管理の統一

プロンプト

設定管理の統一をして。

pydanticを使って設定管理システムを作ってくれました。(抜粋)

from pathlib import Path
from typing import List
from pydantic import Field
from pydantic_settings import BaseSettings

class AppConfig(BaseSettings):
    # ファイル設定
    hoge_file_path: Path = Field(default=None)
    max_file_size_mb: int = Field(default=50, ge=1, le=100")
    
    # 動的プロパティ
    @property
    def files(self):
        """ファイル設定へのアクセス"""
        return type('Files', (), {
            'hoge_file_path': self.hoge_file_path,
            'max_file_size_mb': self.max_file_size_mb
        })()
        
    model_config = {
        "env_file": ".env",
        "env_file_encoding": "utf-8",
        "case_sensitive": False,
        "validate_assignment": True
    }


# グローバル設定インスタンス
settings = AppConfig()

最終的なディレクトリ構造

lib/
├── shared/                    # 共有ライブラリ
│   ├── config_utils.py       # 設定管理ユーティリティ
│   ├── error_handling.py     # エラーハンドリング
│   ├── exceptions.py         # カスタム例外クラス
│   └── messages.py           # メッセージ管理
├── clients/                   # 外部サービスとの接続
│   └── ...
├── services/                  # ビジネスロジック
│   └── ...
└── operations/               # 機能別モジュール
    └── hoge_function/
        ├── __init__.py              # モジュール初期化(render_hoge_function_ui関数のエクスポート)
        ├── main.py                  # メイン処理(render_hoge_function_ui関数を定義)
        ├── config.py                # 機能固有設定(Pydanticベース)
        ├── messages.py              # 機能固有メッセージ
        ├── config_utils.py          # 共有ライブラリとの互換レイヤー
        ├── exceptions.py            # 共有ライブラリからのインポート
        ├── error_utils.py           # 共有ライブラリからのインポート
        └── ui/                      # UIコンポーネント
            ├── __init__.py
            ├── file_upload.py       # ファイルアップロード処理
            ├── data_preview.py      # データプレビュー表示
            ├── data_editor.py       # データ編集機能
            └── csv_export.py        # CSV出力機能

ディレクトリ構造も今後これに従うように.github/instructions.mdに記述しました。
新機能も早めに作っておきたいので、今回はこの辺で整頓を終えました。

おわりに

AIの出力をレビューするのがキツいな...と思っているので、今回のようにレビュー負荷が少ない範囲の差分で変更を進めていくのはちょうど良かったです。
「結果をイメージできるが実際に細かいこと(文法とかライブラリとかのこと)を調べて進めるのが面倒なこと」を任せるのは問題なさそうだという印象を持ちました。
この後調子に乗って、仕様書と実装計画書を作らせ、それに従い実装するように指示を出したら差分が多すぎて無理!と思ったのでリバートしました。
ほどよいAIとの付き合い方を考えていきたいなあと思います。

wwwave's Techblog

Discussion