メドピア開発者ブログ

集合知により医療を再発明しようと邁進しているヘルステックカンパニーのエンジニアブログです。読者に有用な情報発信ができるよう心がけたいので応援のほどよろしくお願いします。

GitHub Copilot を味方につける:AI に渡すコンテキスト整備の工夫

こんにちは。事業本部開発部 MISP グループのフロントエンドエンジニアの小林和弘(@kzhrk0430)です。

メドピアでは「AI ファーストカンパニー」を目指すことを全社で掲げています。実際に社内では、AI ツールを活用して業務を効率化する動きが活発に行われています。たとえば、Gemini を使って Google Meet の文字起こしや議事メモを作成したり、Notion AI で要件定義とテストケースの整合性を確認したり、スライド作成を AI に任せたりと、日々の業務に AI を積極的に取り入れています。

今回は、開発環境をより快適にするために GitHub Copilot(VS Code 拡張)を活用した取り組みをご紹介します。

AI に渡すコンテキストを整備する

Copilot は多様な機能を提供していますが、それぞれ異なる種類のコンテキストを参照している印象があります。そのため、意図通りに動作させるには、各機能ごとに適切なコンテキストを整備することが重要です。

今回は、以下の 3 点にフォーカスして取り組みました:

  • VS Code におけるコード生成
  • GitHub におけるコードレビュー支援
  • VS Code におけるコミットメッセージ自動生成

VS Code におけるコード生成

まず、コード生成機能に関しては、.github/copilot-instructions.md というファイルをプロジェクトに追加しました。このファイルには、プロジェクト特有の文脈や設計方針、命名規則などを記載しています。

作成時には、まず Copilot に初稿を書かせ、その内容を人間がレビュー・修正してブラッシュアップするという流れを取りました。なお、社内では AI ツールの Cline も利用しているため Cline 用にコンテキストを渡す.clinerules ファイルには .github/copilot-instructions.md を参照させるよう設定し、Cline 経由でも文脈が共有されるようにしています。

.clinerules の中身はこの用になっています。

# やくばとシステム開発ガイドライン

## 注意事項

このファイルは参照用として保持されていますが、最新かつ詳細な開発ガイドラインは `.github/copilot-instructions.md` に移動しました。
開発作業を行う際は、`.github/copilot-instructions.md` を参照してください。

GitHub Copilot をはじめとする開発ツールは、`.github/copilot-instructions.md` を参照してコード提案やガイドラインの適用を行います。

## リンク

開発ガイドラインの詳細は [.github/copilot-instructions.md](.github/copilot-instructions.md) を参照してください。

当初は .clinerules.github/copilot-instructions.md と同様に Cline に生成させていたのですが、コンテキストが二重管理になっていたため Copilot Agent に 2 つのファイルを統合させて .clinerules の中身を書き換えています。

GitHub におけるコードレビュー支援

GitHub の Pull Request テンプレートにも Copilot 活用の工夫を加えました。具体的には、Copilot のレビューがわかりやすくなるように、PR テンプレート内にレビューのコンテキストとなる情報を明記するようにしています。

今はまだレビューを日本語で書かせて、レビューコメントの表示がバグる HTML タグのコメントのルールを書いているだけですが、今後コンテキストを増やしてレビュー支援の質を高めて、より有用なフィードバックが得られるようにしたいと考えています。

<details>
<summary>このブロックは Copilot レビューのためのコンテキストです。Copilot は下記の命令を守ってください。</summary>

- レビューコメントは日本語で行う
- レビューコメントの HTML タグはマークダウンの Code spans (`) でラップする

</details>

GitHub 側でコンテキストを設定する機能は用意しているようですが、Copilot Enterprise プランでのみ利用でき、現在は一部のユーザーしか利用できない状態なので、今回の PR の説明文にコンテキストを注入する方法は一時的なハックになっています。

VS Code におけるコミットメッセージ自動生成

Copilot Chat 拡張機能のひとつに、コミットメッセージを自動生成してくれる機能があります。これに対しても、プロジェクトの文脈を反映させる設定を行いました。

具体的には、以下のように VS Code の settings.json.github/copilot-instructions.md を指定しています:

{
  "github.copilot.chat.commitMessageGeneration.instructions": [
    {
      "file": ".github/copilot-instructions.md"
    }
  ]
}

.github/copilot-instructions.md にはコミットメッセージにおけるルールを下記のように記載しています。

## 提案すべきコミットメッセージ

- コミットメッセージは日本語で書く
- git log で参照した過去のコミットメッセージを参考にする
- Conventional Commit を基本とする
- 1 行目のコミットの下には空白の行間を設ける
- 複数行の詳細なコミットメッセージを書く
  - 詳細なコミットメッセージは `## 背景``## 修正内容` などのマークダウンの見出しをつける

この設定により、コミットメッセージの生成時にもプロジェクト固有の背景が反映されるようになり、精度の高い出力が得られるようになりました。

Copilot で生成したコミットメッセージの一例

簡単なコミットメッセージであれば Copilot でコミットメッセージをジェネレートさせてさっとレビューするだけでコミットを作成しています。

VS Code における Copilot 設定の展望

VS Code の Copilot の instructions の設定は Experimental で提供されていますが、下記の 5 つの機能にコンテキスト設定ができるようになっています。

  • Review Selection: Instructions
  • Code Generation: Instructions
  • Commit Message Generation: Instructions
  • Pull Request Description Generation: Instructions
  • Test Generation: Instructions

VS Code の Copilot の設定画面のキャプチャー

GitHub 側も、.github/copilot-instructions.md にすべてのユースケースの情報を統合することが難しいと判断したのか、将来的には用途ごとにマークダウンファイルを分けることを検討しているのかもしれません。

AI に渡すコンテキスト整備の重要性

最近公開された Devin Wiki は、AI のコード理解能力を強く印象づけるものでした。

メドピアでも Devin AI を導入しており、社内で Devin Wiki の生成結果を確認する機会がありました。その中で、医療機関向けおよび薬局向けの Nuxt アプリを管理しているモノレポ構成のリポジトリに対して、Devin が自動的に、医療機関・薬局・患者間の処方せんの流れを示すフローチャートを生成していたのを目にし、大きな驚きがありました。

ただし、すべての機能について完璧に Wiki 化されているわけではなく、ハルシネーション(誤生成)が起きそうな領域については、あえてページを生成しないようにしているケースも見受けられました。

このように、AI ツールの進化によって、今後さらに多くの業務が AI によって支援・代替されるようになると感じています。これはエンジニアの仕事を奪うということではなく、むしろ課題発見力や判断力といった本質的な能力に集中できる環境が整っていく、というポジティブな変化だと考えています。

この点については、VPoE の保立さんも以下のインタビュー記事で言及しています: style.medpeer.co.jp

現時点では、AI がどこからどのようにコンテキストを取得しているのかを意識し、AI が誤解しないようなデータセット(明確な変数名や整理されたコード構造など)を整備することが、エンジニアに求められていると強く感じます。

おまけ:AI 活用と執筆の裏側

「AI に渡すコンテキストを整備する」セクションは、まず箇条書きで要点を整理し、それを ChatGPT に文章化してもらったうえで、内容を加筆・修正して仕上げました。

その他のセクションは、最初に自分で文章を書き、その後 ChatGPT にレビューを依頼して改善点を洗い出しました。

また、OGP 画像も記事をレビューさせたついでに ChatGPT に出力させています。

このように、試せるところから積極的に AI を活用し、自分なりに現在地を確認していくことが、AI 時代を前向きに生きる上で大切な姿勢だと考えています。

今後も、実務に即した形での AI 活用について、実験と発信を続けていきたいと思います。


是非読者になってください!


メドピアでは一緒に働く仲間を募集しています。
ご応募をお待ちしております!

■募集ポジションはこちら medpeer.co.jp

■エンジニア紹介ページはこちら engineer.medpeer.co.jp

■メドピア公式YouTube  www.youtube.com

■メドピア公式note
style.medpeer.co.jp

#RubyKaigi 2025 セッションレポート

皆様こんにちは、メドピアのサーバーサイドエンジニアの内藤(@naitoh)です。

RubyKaigi 2025に参加されていた皆さん、お疲れ様でした。

今回、内藤がRubyKaigi 本編に登壇しました。 発表内容の詳細は以下の記事にまとめておりますのでよろしければご覧ください。

naitoh.hatenablog.com

セッションレポート

RubyKaigi のセッションの中で特に印象に残ったセッションをご紹介します。 タイムテーブルは下記から確認ください。

rubykaigi.org

Make Parsers Compatible Using Automata Learning

rubykaigi.org

オートマトン理論は理解していなかったのですが、「オートマトンと正規表現は相互に変換できる」とのことなので、正規表現が数学的にオートマトンで表現できるのは美しく非常に良いですね!

RubyKaigi に来るたびに、自分の知らないコンピュータサイエンスの知識を思い知らされます。

聞きたいセッションがあれば、タイムテーブルに書かれている説明を予習しておくとセッションをもっと楽しめるんですよね。 続きものであれば、同じ人の昨年以前のセッション動画を見ておくのもお勧めです。

Goodbye fat gem 2025

rubykaigi.org

様々な gem をメンテナンスされている須藤さんの公演で、fat gem*1 の辛みを面白おかしく共感を呼ぶ形でお話されるトークでした。 自分のトークもこのように、会場の反応を楽しみながらできれば良いのですが、なかなかハードルが高いです。

公演の内容は、fat gem はユーザー視点だと利用するだけなら楽で良いけど、これって実は開発者側に多大なコストがかかっているので、持続可能性の意味で厳しいんですよね。 例えば nokogiri gem の場合、Ruby 3.4 がリリースされたその日に、対応する11プラットフォーム(内部的にサポート対象の Ruby のバージョンは4つ)が用意されています。ユーザーとしては非常にありがたいのですが、開発者目線で見ると nokogiri は頑張りすぎだと思います。 自分の gem でこんな事求められたら無理ですと断るレベル。

なので、須藤さんの提案は、

  1. C拡張 gem でもユーザー自身にビルドしてもらいましょう
  2. ビルド環境を用意するのが手間(よくビルドエラーになる)なので、ビルド環境を自動で準備できれば良さそう
  3. Windows 環境には RubyInstaller2 という先駆者がいる
    • Devkit というビルド環境がセット
    • パッケージマネージャもセット
    • 依存パッケージを自動インストール
    • 依存パッケージが存在せずインストールに失敗するということはない
  4. インストール時に自動で外部依存もインストールする rubygems-requirements-system gem を用意

このように、ユーザー自身でビルドする世界になれば持続可能性が高まり、みんなハッピーになるだろうということです。

ユーザーの環境で毎回ビルドのコストがかかる点と、ユーザーの環境に依存パッケージをインストールする必要がある点がデメリットですが、前者は許容できるコストで、後者はローカル環境を汚染したくない場合、Docker 上で実施するのが良いのではないでしょうか。(この gem がもし主流になれば、Dockerfile に依存パッケージ名を記載する手間がなくなる可能性もあるかもしれません。)

RuboCop: Modularity and AST Insights

rubykaigi.org

精力的に開発が続いている RuboCop のモジュール性のお話です。 これまで RuboCop は公式のプラグイン API を提供していなかったため、inject と呼ばれるモンキーパッチを利用する形で実現されており、それがデファクトスタンダードだったそうです。末恐ろしい状況ですね。

体系的なプラグインシステムが提供されるとユーザーとしても安心して使えるし、開発者としても貢献しやすくなりますよね!

また、RuboCop のバックエンドパーサーとして機能してきた Parser gem の代わりに、今後は Prism が採用されるとのことで、Ruby エコシステムの世代交代が進んでいますね。 Ruby の最新の機能が実用段階に来ているということで、どんどん最新版を使っていきましょう!

おわりに

3日間にわたる RubyKaigi 2025が終了しました。 非常に魅力的な公演が目白押しでしたが、3トラックのため、視聴できるセッションが限られていた点と、自身の発表の裏番組だった ZJIT を聞けなかったのが残念です。

次回のRubyKaigiは 2026年4月22日から4月24日、場所は北海道函館市です。


是非読者になってください!


メドピアでは一緒に働く仲間を募集しています。
ご応募をお待ちしております!

■募集ポジションはこちら medpeer.co.jp

■エンジニア紹介ページはこちら engineer.medpeer.co.jp

■メドピア公式YouTube  www.youtube.com

■メドピア公式note
style.medpeer.co.jp

*1:事前にビルド(コンパイル)された C拡張バイナリを同梱した gem。新しい Ruby が出るとその Ruby のバージョンにあわせた対応バイナリが必要。

ScrollableTabRowでスクロール状態を監視する

はじめに

こんにちは!メドピアにてモバイルアプリエンジニアをしている佐藤です。
今回の「ClinPeerアプリ開発の裏側連載記事」では、ScrollableTabRowでのスクロール状態の監視方法について解説していきたいと思います。 tech.medpeer.co.jp

背景

ClinPeerアプリでは、ユーザーが関心のあるカテゴリーをタブとして動的に追加できるようにしており、追加したカテゴリーを上部のタブで表示しています。その際、右側にまだ表示しきれていないタブがあることをユーザーに伝えるため、右端にスクロール可能な場合はアイコンを表示しています。

左:スクロールできるときは赤枠内のアイコンを表示
右:スクロールできない場合はアイコンを非表示

XMLレイアウトのTabLayout + ViewPager2であればaddOnScrollChangedListenerで監視出来るので、Jetpack Composeでも rememberScrollState などを使えば簡単に実装できると思っていましたが、実際には少しハマりどころがありました。
そのため、今回はその解決方法を共有します。

結論

PrimaryScrollableTabRow を使うことで解決できます!
androidx.compose.material3:material3-*:1.2.0-alpha09」のリリースでスクロール状態が公開されるようになりましたので、本バージョン以降のPrimaryScrollableTabRowを用いれば簡単に監視が出来るようになります。

PrimaryScrollableTabRow は執筆時点ではまだ試験運用版で @OptIn(ExperimentalMaterial3Api::class) のアノテーション付与が必要です。
将来的に仕様が変更される可能性もあるため、バージョンアップ時の挙動に注意しましょう。

PrimaryScrollableTabRowについて

ScrollableTabRowとPrimaryScrollableTabRowの定義を比較してみます。(定義のandroidx.compose.material3:material3のバージョンは1.3.1のものです)

@Composable
fun ScrollableTabRow(
    selectedTabIndex: Int,
    modifier: Modifier = Modifier,
    containerColor: Color = TabRowDefaults.primaryContainerColor,
    contentColor: Color = TabRowDefaults.primaryContentColor,
    edgePadding: Dp = TabRowDefaults.ScrollableTabRowEdgeStartPadding,
    indicator: @Composable (tabPositions: List<TabPosition>) -> Unit =
        @Composable { tabPositions ->
            TabRowDefaults.SecondaryIndicator(
                Modifier.tabIndicatorOffset(tabPositions[selectedTabIndex])
            )
        },
    divider: @Composable () -> Unit = @Composable { HorizontalDivider() },
    tabs: @Composable () -> Unit
)
@ExperimentalMaterial3Api
@Composable
fun PrimaryScrollableTabRow(
    selectedTabIndex: Int,
    modifier: Modifier = Modifier,
    scrollState: ScrollState = rememberScrollState(),
    containerColor: Color = TabRowDefaults.primaryContainerColor,
    contentColor: Color = TabRowDefaults.primaryContentColor,
    edgePadding: Dp = TabRowDefaults.ScrollableTabRowEdgeStartPadding,
    indicator: @Composable TabIndicatorScope.() -> Unit =
        @Composable {
            TabRowDefaults.PrimaryIndicator(
                Modifier.tabIndicatorOffset(selectedTabIndex, matchContentSize = true),
                width = Dp.Unspecified,
            )
        },
    divider: @Composable () -> Unit = @Composable { HorizontalDivider() },
    tabs: @Composable () -> Unit
)

ScrollableTabRowではscrollStateを指定することは出来ませんが、PrimaryScrollableTabRowではscrollStateのパラメーターが追加されているので、scrollStateにrememberScrollStateを指定してスクロール状態を監視出来るようにします。

解説

これらの内容を踏まえた上で実装例を書きます。
まず、スクロール状態の監視が行えないScrollableTabRowのコードが下記の通りです。

Scaffold(
    modifier = Modifier.fillMaxSize(),
    topBar = {
        CenterAlignedTopAppBar(
            title = {
                Text(
                    "ScrollableTabRowExample",
                    fontSize = 18.sp,
                    textAlign = TextAlign.Center
                )
            }
        )
    },
    content = { paddingValues ->
        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(paddingValues)
        ) {
             val items = listOf(
                "Tab 1",
                "Tab 2",
                "Tab 3",
                "Tab 4",
                "Tab 5",
                "Tab 6",
                "Tab 7",
                "Tab 8",
                "Tab 9",
                "Tab 10"
            )
            val pagerState =
                rememberPagerState(pageCount = { items.size })
            val scope = rememberCoroutineScope()

            ScrollableTabRow(
                selectedTabIndex = pagerState.currentPage,
                edgePadding = 0.dp
            ) {
                items.forEachIndexed { index, tab ->
                    Tab(
                        selected = pagerState.currentPage == index,
                        onClick = {
                            scope.launch {
                                pagerState.animateScrollToPage(index)
                            }
                        },
                        modifier = Modifier.height(48.dp)
                    ) {
                        Text(tab)
                    }
                }
            }
            HorizontalPager(
                state = pagerState
            ) {
                Box(
                    modifier = Modifier
                        .fillMaxSize(),
                    contentAlignment = Alignment.Center
                ) {
                    Text(
                        "page: ${items[it]}"
                    )
                }
            }
        }
    }
)

スクロール状態の監視を行なっていない画面

右側にスクロールが出来る状態である場合にアイコンを表示するといったことをしたい場合、以下の通り変更します。

Scaffold(
    modifier = Modifier.fillMaxSize(),
    topBar = {
        CenterAlignedTopAppBar(
            title = {
                Text(
                    "PrimaryScrollableTabRowExample",
                    fontSize = 18.sp,
                    textAlign = TextAlign.Center
                )
            }
        )
    },
    content = { paddingValues ->
        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(paddingValues)
        ) {
             val items = listOf(
                "Tab 1",
                "Tab 2",
                "Tab 3",
                "Tab 4",
                "Tab 5",
                "Tab 6",
                "Tab 7",
                "Tab 8",
                "Tab 9",
                "Tab 10"
            )
            val pagerState =
                rememberPagerState(pageCount = { items.size })
            val scope = rememberCoroutineScope()
            val scrollState = rememberScrollState()

            // スクロール可能なアイコンの表示状態を管理するフラグを追加する
            var showArrow by remember { mutableStateOf(false) }
            LaunchedEffect(scrollState.maxValue, scrollState.value) {
                // 右側にスクロール可能な状態の場合はshowArrowをtrueにする
                showArrow = scrollState.value < scrollState.maxValue
            }

            Box {
                PrimaryScrollableTabRow(
                    selectedTabIndex = pagerState.currentPage,
                    edgePadding = 0.dp,
                    scrollState = scrollState
                ) {
                    items.forEachIndexed { index, tab ->
                        Tab(
                            selected = pagerState.currentPage == index,
                            onClick = {
                                scope.launch {
                                    pagerState.animateScrollToPage(index)
                                }
                            },
                            modifier = Modifier.height(48.dp)
                        ) {
                            Text(tab)
                        }
                    }
                }
                if (showArrow) {
                    // 右側にスクロール可能な状態の場合は右端にアイコンを表示する
                    Box(
                        modifier = Modifier
                            .width(44.dp)
                            .height(48.dp)
                            .background(
                                Brush.linearGradient(
                                    colors = listOf(
                                        Color.White.copy(0f),
                                        Color.White.copy(1f)
                                    )
                                )
                            )
                            .align(Alignment.CenterEnd)
                    ) {
                        Image(
                            imageVector = Icons.AutoMirrored.Default.KeyboardArrowRight,
                            contentDescription = null,
                            modifier = Modifier.align(Alignment.Center)
                        )
                    }
                }
            }
            HorizontalPager(
                state = pagerState
            ) {
                Box(
                    modifier = Modifier
                        .fillMaxSize(),
                    contentAlignment = Alignment.Center
                ) {
                    Text(
                        "page: ${items[it]}"
                    )
                }
            }
        }
    }
)

スクロール状態を監視して右側にスクロール出来る時はアイコンを表示

最後に

PrimaryScrollableTabRowに関する記事がほとんどなかった為、この場を借りて紹介させていただきました。
PrimaryScrollableTabRowを使うことで、スクロール状態を監視してユーザー体験を向上させるような細かなUI調整も可能になります。
本記事が同じような課題で悩んでいる方や今後同じような機能を実装する方の助けになれば幸いです!


是非読者になってください!


メドピアでは一緒に働く仲間を募集しています。
ご応募をお待ちしております!

■募集ポジションはこちら medpeer.co.jp

■エンジニア紹介ページはこちら engineer.medpeer.co.jp

■メドピア公式YouTube  www.youtube.com

■メドピア公式note
style.medpeer.co.jp

RubyKaigi 2025 に@naitoh が登壇します #rubykaigi

皆様こんにちは、メドピアのサーバーサイドエンジニアの内藤(@naitoh)です。

この度、2025/04/16(水)-18(金)の3日間で開催される「RubyKaigi 2025」に登壇させていただくこととなりました! タイトルは「Improvement of REXML and speed up using StringScanner」となります。

rubykaigi.org

スケジュールは、 Day2 11:50 〜 12:20 / Pearls Room を予定しています。 ぜひセッションにお越しください。

登壇内容について

セッションでは以下の内容をお伝えします。

REXML is a standard XML library (Bundled Gem) for Ruby implemented in Pure Ruby. It is up to 40% faster between rexml 3.2.6 gem attached to Ruby 3.3.0 and rexml 3.4.0 gem attached to Ruby 3.4.0. Through our REXML speedup efforts using StringScanner, I will explain why using StringScanner is faster and how it can be implemented to make it faster.

昨年の RubyKaigi 2024 のLT でお話した話

の続きで、 REXML のXMLパース処理を StringScanner を使ってREXML 3.2.6 (Ruby 3.3.0 添付のバージョン) からREXML 3.4.0 (Ruby 3.4.0 添付のバージョン) の間で約4割速くしたので、StringScanner を使うと何故パース処理が速くなるのか、どのような点に気をつけてパース処理を書けば速くなるのかなど、高速化のポイントを皆様に紹介します。

おわりに

それでは皆様、当日お会いできることを楽しみにしております!


是非読者になってください!


メドピアでは一緒に働く仲間を募集しています。
ご応募をお待ちしております!

■募集ポジションはこちら medpeer.co.jp

■エンジニア紹介ページはこちら engineer.medpeer.co.jp

■メドピア公式YouTube  www.youtube.com

■メドピア公式note
style.medpeer.co.jp

Railsの「ActiveSupport::ErrorReporter」って知ってる?

こんにちは。サーバーサイドエンジニアの三村(@t_mimura39)です。

またまた「ClinPeerアプリ開発の裏側連載記事」です。 tech.medpeer.co.jp

今回はClinPeerで活用しているRailsの ActiveSupport::ErrorReporter についてご紹介します。

目次

ActiveSupport::ErrorReporter とは

Railsに標準添付されているエラー管理の仕組みです。

↓これが

begin
  do_something
rescue SomethingIsBroken => error
  MyErrorReportingService.notify(error)
end

↓こうなります。

Rails.error.handle(SomethingIsBroken) do
  do_something
end

詳細はRails公式ドキュメントをご参照ください。

guides.rubyonrails.org

以上になります。ありがとうございました。

と、終わるわけにもいかないのでもう少し深い話を書きます。

なぜ ActiveSupport::ErrorReporter を使うのか

上述の通り典型的な例外ハンドリング処理に対して統一的なI/Fを提供してくれるのですが、それ以外にもいくつかの利点があります。

まず一つ目はPubSub的な仕組みになっているため「例外発生時に実行したい処理」の増減に対して柔軟に対応可能という点です。

ClinPeerでは例外発生時に以下2種類の処理を実行しています。

  • Rollbarへの例外通知
    • エラー管理サービスとして利用しているRollbarに例外情報を通知します。他SaaSを利用しているプロジェクトは良いように読み替えてください。
  • ログ出力
    • 上記のようなエラー管理サービスが不調な場合でも例外状況を把握できるようにログも出力しています。

例えば以下のように初期設定をしてみます。

# config/initializers/rails_error_subscriber.rb
class RailsLoggerErrorSubscriber
  def report(error, handled:, severity:, context:, source: nil)
    error_message = error.message
    message = "#{error.class}: #{error_message}\n#{(error.backtrace || caller)&.join("\n")}"
    severity = :warn if severity == :warning
    Rails.logger.public_send(severity, message)
  end
end

class RollbarErrorSubscriber
  def report(error, handled:, severity:, context:, source: nil)
    extra = context.is_a?(Hash) ? context.deep_dup : {}
    Rollbar.log(severity, error, extra)
  end
end

Rails.application.config.after_initialize do
  Rails.error.subscribe(RailsLoggerErrorSubscriber.new)
  Rails.error.subscribe(RollbarErrorSubscriber.new)
end

Rails.error.reportRails.error.handle で例外を処理する際に、登録したサブスクライバー全てに例外情報を通知することができます。

# これを
begin
  do_something
rescue SomethingIsBroken => error
  Rails.logger.error("#{error.class}: #{error_message}\n#{(error.backtrace || caller)&.join("\n")}")
  Rollbar.log(:error, error)
end

# こう書き換えられて
begin
  do_something
rescue SomethingIsBroken => error
  Rails.error.report(error)
end

# こう書くこともできる
Rails.error.handle(SomethingIsBroken) do
  do_something
end

この仕組みは「Rollbarから別のサービスに乗り換えるケース」でもとても役立ちます。そうしたケースでもRailsアプリケーション内の例外ハンドリング処理に手を加えずにinitializerの中身を変更するだけで対応が可能になります。

実行コンテキストの注入

どのリクエスト・ジョブで発生した例外なのかを表す「実行コンテキスト」情報がエラー通知には付与されて欲しいものです。それを便利に取り扱うための仕組みが ActiveSupport::ErrorReporter には用意されています。
各サブスクライバーに定義するreportメソッドの引数には context というものがあります。

def report(error, handled:, severity:, context:, source: nil)

本連載記事を購読してくださっている方はもうお気づきかもしれません。
はい、この context の実体は ActiveSupport::ExecutionContext です。

https://github.com/rails/rails/blob/v8.0.2/activesupport/lib/active_support/error_reporter.rb#L224

ActiveSupport::ExecutionContext の詳細については以下の記事をご参照ください。

ClinPeer Railsプロジェクトのオブザーバビリティ強化施策#実行コンテキスト

かなり掻い摘んで説明すると、 context にはリクエストやジョブが実行されているActionControllerやActiveJobのインスタンスが格納されています。

ClinPeerではこのようなSubscriberを定義することで、Rollbarへの全てのエラー通知に自動的に実行コンテキスト情報が付与されるようにしています。

class RollbarErrorSubscriber
  def report(error, handled:, severity:, context:, source: nil) # rubocop:disable Lint/UnusedMethodArgument
    extra = context.is_a?(Hash) ? context.deep_dup : {}

    controller = extra[:controller]

    extract_context!(extra)

    extra[:custom_data_method_context] = source

    scope = { request: controller&.rollbar_request_data, person: controller&.rollbar_person_data }
    Rollbar.scoped(scope) { Rollbar.log(severity, error, extra) }
  end

  private

  def extract_context!(context)
    # 現在実行されているコントローラまたはジョブの情報が設定されている
    # https://github.com/rails/rails/blob/v8.0.2/actionpack/lib/action_controller/metal/instrumentation.rb#L60
    # https://github.com/rails/rails/blob/v8.0.2/activejob/lib/active_job/execution.rb#L66
    controler_or_job = context.delete(:controller) || context.delete(:job)
    return unless controler_or_job.present? && controler_or_job.respond_to?(:_execution_context)

    context.reverse_merge!(controler_or_job._execution_context)
  end
end

なぜ ActiveSupport::ErrorReporter を使うのか(本当のメリット)

「PubSubな仕組みの便利さ」「実行コンテキスト注入の仕組み」について説明しましたが、実はこの程度であれば十分に自前で実装することが可能です。
その2点よりも遥かに大きな強みとして私が考えるのは「Railsが提供しているI/F」という点です。

この Rails.error.report というI/FがRails公式で提供されているため、Rails内やフレームワーク的なGemでの例外処理にデフォルトで組み込まれやすくなります。

実際にRails内でも何箇所か ActiveSupport::ErrorReporter が利用されています。

Rails内での利用例

v8.0.2時点

これを執筆している今現在も ActiveSupport::ErrorReporter の活用が進んでいます。
以下は2025年4月時点でのmainに取り込まれているPRです。

Rails以外の利用例

などのRails以外のGemでも ActiveSupport::ErrorReporter が利用が進んでいます。

また、ClinPeerでは自前でRollbarのSubscriberを定義していますが、 RollbarSentry が公式でSubscriberを定義していたりします。 実行コンテキスト周りにこだわる必要がなければそれらのGemを導入するだけでほどほどに例外通知される状態になります。

各フレームワーク層で ActiveSupport::ErrorReporter を活用した例外通知を実装してくれることで、アプリケーション内での例外補足を一定サボれるだけでなく、これまで考慮外にあった例外なんかを漏れなく補足することもできるようになり嬉しいですね。

おわり

偉そうに ActiveSupport::ErrorReporter について語りましたが、実はClinPeerの開発を始めるまで存在も知りませんでした(Railsの更新はマメにウォッチしているつもりなのですが)。まだまだRailsには伸び代があり、痒い所に手が届く感じが気持ちが良いですね。

既存のRailsシステムの例外ハンドリング処理に手を加えるのは大変ですが、こういった所でも小まめにRails Wayに乗っておくと将来の技術的負債の解消に繋がるので導入を検討してはいかがでしょうか。


是非読者になってください!


メドピアでは一緒に働く仲間を募集しています。
ご応募をお待ちしております!

■募集ポジションはこちら medpeer.co.jp

■エンジニア紹介ページはこちら engineer.medpeer.co.jp

■メドピア公式YouTube  www.youtube.com

■メドピア公式note
style.medpeer.co.jp

技術顧問Matzとは、どんな話をしているの?

こんにちは、組織開発グループの榎本です。

弊社の技術顧問にはまつもとゆきひろ(通称・Matz)さんがおり、Matzさん(以下、親しみも込めてMatzと表記します)とは定期的に「Matz会」と称して、Google Meetを繋いでリモートミーティングを開催しています。

Matz会のテーマは多岐にわたりますが、「毎回Matzとどんな話をしているの?」と気になっている開発者の方もいるかもしれません。本記事では、過去のMatz会開催実績を元に、その内容の一部をお伝えできればと思います。

講演の再演

過去のテーマ例:

  • RubyKaigi Keynote の再演
  • Matzが登壇したイベントの講演の再演
  • Matzチャンネルで話していたトピックの再演

一番わかりやすいのはこちらでしょうか。全員が全員、RubyKaigi現地に足を運んで、生Matzのキーノートを聴けるわけではないので、RubyKaigi閉会後すぐにMatzに再演してもらえるのは良い機会となりました。

tech.medpeer.co.jp 👆️RubyKaigi閉会後に行った感想戦の様子

過去に好評だった会でいうと、「動的型付け言語と大規模開発」の再演は、Matzの型に対する考え方を深く聞くことができました。

Rubyの最新機能の解説

過去のテーマ例:

  • YJIT
  • Prism
  • 次期Rubyバージョン解説

最新バージョンのRubyで導入される技術について、Matzに解説をしてもらいました。Rubyのリリースノートだけでは知ることのできない導入経緯(なぜその機能が入ったか、どんな課題を解決したいのか)や裏エピソード(Rubyコアチーム内でどんなコミュニケーションがあったのか)などが聞けて良かったです。

Rubyのコア技術の解説

過去のテーマ例:

  • YARV について
  • Ruby GVL について
  • Ruby GC の仕組み

このあたりの話を完全に理解するには、コンピュータサイエンスの知識も必要になってくるので、なかなか難しいテーマではありました。しかしエンジニアの知的好奇心を刺激する良いテーマだったと感じています。ときに話題はCRubyの内部実装に及ぶこともあり、Rubyエンジニアにとっては話についていくのでイッパイイッパイ、という人も多かったようです。

OSS

過去のテーマ例:

  • OSSへの貢献方法
  • Rubyのコミュニティ運営について
  • Rubyの新機能の意思決定について

OSS貢献未経験のエンジニアから「OSSに貢献したいけど貢献の仕方がわからない...」「OSSに貢献したいけどどうしたらいいの?」という声があり、それに対してMatzからいろいろアドバイスを貰えたのは良い機会でした。今後社内からOSS貢献するエンジニアが増えていくといいなと考えています。

Rubyのコミュニティ運営についても伺いました。個人的にあまり表に出てこないRubyの意思決定(新機能のAccept、あるいは提案のReject)の裏事情的な話が聞けたのが良かったと感じています。Rubyの進化の裏にあるMatzの様々な葛藤を垣間見ることによって、Rubyというプログラミング言語の存在の有り難みを改めて感じることができました。

キャリア論

過去のテーマ例:

  • エンジニア・キャリア戦略
  • AIと開発者の今後

Matzの考えるキャリア戦略について語ってもらいました。キャリアに関しては若手からシニアまで悩む開発者が多いと思うので、キャリアに迷う開発者にとって参考になる話だったと思います。

昨今のAIトレンドも取り入れて「AIと開発者の今後ってどうなると思う?」みたいなテーマについても語っていただきました。

Matzへプレゼン

過去のテーマ例:

  • 弊社メンバーが作成した LTをプレゼン
  • 弊社の事業紹介をプレゼン

弊社メンバーが過去に行った発表内容や弊社の展開している事業について Matz にプレゼンテーションを行う機会を設けました。Matzから内容について直接フィードバックいただける良い機会になりました。

tech.medpeer.co.jp 👆️MatzにRubyKaigi関連LTをプレゼンする様子

その他

過去のテーマ例:

  • Matzの開発環境について
  • Matzに何でも質問コーナー
  • mrubyについて

エンジニアとしては、Matzが普段使っているPC、OS、キーボード、デスク環境など気になる方も多いのではないかと思います。そのあたりの開発環境について掘り下げさせてもらいました。

またMatz会では毎回最後に「Matzに何でも質問コーナー」のような質問時間を設けています。この時間では時間の許す限り、弊社メンバーから思い思いのMatzに聞きたいことをぶつけています。生Matzをカンファレンスで見かけることはあっても、時間をとってもらって直接質問する機会はなかなか訪れないので、メンバーにとっては嬉しい機会になっていると感じます。

そしてMatzさんは月一でWeb会議を開いて頂いているので、そこで毎月疑問などをぶつけられます。(中略)こう言った著名な技術顧問の方がいるおかげで会社全体の技術力の底上げになっていると思います。 Railsの実装やRuby内部のことが気になった際にすぐに質問できるという環境はエンジニアにとってものすごく良い環境だと思っています。

tech.medpeer.co.jp

さいごに

いかがでしたでしょうか。

本記事で紹介したテーマは、Matz会運営チームが中心となって決めています。運営チームとしては、Rubyのコアな技術話からキャリアや開発環境などのカジュアルなテーマまで、幅広い開発者に楽しんでもらえるようなテーマ設定を心がけています。こうすることでジュニアレベルのエンジニアからシニアレベルのエンジニアまで、またRubyエンジニアから普段Rubyを使わないエンジニアまで、楽しんでもらえるテーマ設定になっていると思います。

本記事で紹介したトピックをMatzから直接聴きたい!という方がいらっしゃれば、ぜひ弊社への入社をご検討いただければと思います!


是非読者になってください!


メドピアでは一緒に働く仲間を募集しています。
ご応募をお待ちしております!

■募集ポジションはこちら medpeer.co.jp

■エンジニア紹介ページはこちら engineer.medpeer.co.jp

■メドピア公式YouTube  www.youtube.com

■メドピア公式note
style.medpeer.co.jp

Devin AIは技術的負債解消の救世主となるか?

Answer: 救世主まではいかないが、間違いなく助けになる。


組織開発グループの榎本です。世は大AIコード生成時代、皆さんバイブコーディングしていますか?

弊社においてもDevin、Clineを試験的に導入して活用し始めていますが、本記事では「Devin AIが技術的負債の解消に役立った話」を紹介したいと思います。

Devin AIとは

Devin AI(以下、Devinと表記)については既に各所で話題なので詳しい説明は省きますが、一言でいうと開発を手伝ってくれるAIエージェントです。イメージとしては、開発チームに新たに入ってくるジュニアクラスの開発者 が一番近いと言えるでしょう。

devin.ai

Clineとの違いとしては、クラウド上に独自の開発環境を持っていることでしょうか。一度クラウドに開発環境を作っておけば、Web UIやSlackから指示出しするだけで自律的にタスクをこなしてくれます。つまり、手元に動作する開発環境がなくとも、指示出しだけで開発が進められてしまうのがDevinの利点と言えます。

DevinにWebUIからコードフォーマットの指示出しをしている様子

技術的負債の現状

さて、現在私が対峙している技術的負債は下記のようなものです。

  • オレオレ・フレームワーク(OSSではない独自のフレームワーク実装)
  • デッドコード多数
  • テストなし
  • コード重複多数
  • 不要なファイル、不要な関数、不要な定数・変数が多数あり
  • 一貫性のないコーディングスタイル

これらの問題を効率的に解消するためにDevinの力が使えるのでは、と考えました。

どれくらいレガシー
発表資料「10年モノのレガシーPHPアプリケーションを移植しきるまでの泥臭くも長い軌跡 / legacy-php-app-migration - Speaker Deck」より引用

2023年時点のAI活用

2023年に「この負債解消にAIは活用できないものか...」と試したのですが、そのときはせいぜい ChatGPT や GitHub Copilot に読み解けない複雑なコードをぶん投げてわかりやすく解説してもらう程度でした。

そのとき抱いたAIに対する印象としては、「AIはコード・リーディングの助けにはなるが、コード・ライティングに関しては自分で書いたほうが速い」というものでした。つまり、役には立つが、技術的負債解消に大きく寄与するものとは言えませんでした。

AIは銀の弾丸か
発表資料「10年モノのレガシーPHPアプリケーションを移植しきるまでの泥臭くも長い軌跡 / legacy-php-app-migration - Speaker Deck」より引用

2025年、Devinの活用

しかしDevinは違いました。きちんと開発ルール(Devin用語的にはKnowledge)さえ整備しておけば、簡単なタスクはある程度雑な指示でも適切にこなしてくれました。

活用ユースケース

実際にDevinに依頼したタスクとしては下記のようなものです。

  • テストを書かせる
    • your/target/file の関数fooのテストコードを書いて」
  • コード・フォーマット
    • ※ 事前に 使用するフォーマッターの設定を済ませておき、フォーマットコマンドを開発ルールに明記しておく
    • your/target/file にコードフォーマットをかけて」
  • 簡単なリファクタリング
    • 「AクラスとBクラスを統合して」
    • 「CクラスをディレクトリXに移動して」
    • 「Dクラスの命名を適切なクラス命名 Xyzに変更して」
  • 不要コード削除
    • 「Aクラスの不要関数を削除して」
    • 「Bクラスの不要定数を削除して」
    • 「Cクラスの不要な require 処理を削除して」
    • 「Dファイルは不要なファイルだから削除して」
  • ライブラリ追加
    • 「このプロジェクトに静的解析ツールを追加したい。適切なツールを追加して」

技術的負債として長年放置されたコードベースを触っていると、作業を進めている中で無限にリファクタリングしたい箇所が出てきます。その中でも上述した簡単なリファクタリング・タスクをDevinに丸投げできるのはとても便利でした。

うまくいかなかった指示の例

逆にうまくいかなかった指示出しの例としては、下記のようなものです。

  • 「このプロジェクト全体から不要なコードを見つけて削除して」
    • Devinの計算資源(Devin用語的にはACU)を消費した割には、大した成果は出なかった
    • 対象がでかすぎて、結局「どこを重点的にリファクタしたい?」と逆質問されて止まった
  • 「(複雑なコードの)テストを書いて or リファクタリングをして」
    • Devinはシンプルなクラスのユニットテスト実装はそこそこ悪くないものを書いてくれた
    • 一方、複雑に入り組んだスパゲッティのようなコードのテストを書かせると、モックするコードだらけで何をテストしているか全くわからないコードが出力された
    • リファクタリングも同様で、それっぽいリファクタリングはしてくれるが、元のコードが複雑すぎてリファクタリングされてもレビュワーがその正当性を自信を持ってレビューできず、マージができない

AIエージェントが変なところでスタックして計算資源を使い続けるパターンはDevinに限らずあるようなので、変なループに入ったらさっさと「損切り(タスクの強制終了)」をしてしまうのがよさそうに感じました。

複雑なタスク依頼は工夫が必要

複雑なコードに関しては人間側がある程度適切な戦略を提示する必要があるようにも感じました。例えば下記のようなものです。

  • 「処理は流れは大きく変えずにリファクタリングして」
  • 「{手本となるユニットテストファイルのパス} のように、モックは最小限にしてテストを書いて」

新米エンジニアに難解なタスクを丸投げしてうまくいかないのと同様に、Devinにいきなり難しいタスクを依頼してもうまくはいかない ようです。

シンプルなプロンプトで複雑な巨大関数をDevinにリファクタさせた例。人間がレビューできるレベルの差分にはなっていない。

良い指示出し・悪い指示出し例に関しては、下記の公式ドキュメントにまとまっているので、あわせてご参照ください。

docs.devin.ai

良かった点

上述したユースケースはどれも簡単なものなので、別に人間に任せても問題なく遂行できます。

ではDevinに任せるメリットは何でしょうか?

  • 一人で作業が完結する
    • DevinがPull Request作成の主体となることで、それを私がレビュー → マージすることで一人で作業が完結します
    • 「人間のApproveがマージ条件」と設定しているチームにとって、レビュー待ちがボトルネックになりがちですが、一人で作業が完結する場合はPR作成→レビューがシームレスに実行できます
    • 結果、PRの作成数・マージ数が飛躍的に向上しました
  • 必要に応じてDevinを分身させることができる
    • 上述したタスクはそれぞれ独立しており、互いに依存性のないタスクです
    • したがって、それぞれのタスクを Devin を一時的に5体に分身させ進めてもらうことも可能です(やっていることは指示出しを並列に行うだけです)
  • 24時間/365日対応可能
    • 働き方改革により時間外労働の上限規制が厳しい昨今に、24時間365日対応可能なDevinは心強いです
    • 自分が稼働していない時間に、Devinが手足となって働いてくれます
      • ランチ休憩前に指示出し → 休憩後、成果確認
      • 終業前に指示出し → 翌日、成果確認
  • 文句を言わない
    • 新人エンジニアにつまらない単純作業を依頼し続けるのは、依頼者としても気まずいし、作業者としてもウンザリしちゃいますよね
    • その点、Devin は文句を言うことはなく、遠慮なくタスクを依頼し続けられるのが有り難い存在です
  • 小さいタスクに脳内ワーキングメモリを消費しなくて済む
    • 例えばあるファイルにコードフォーマットをかけるだけの簡単なタスクがあったとして、簡単だけど考えることはたくさんあります
      • 適切なブランチ名を考えて、ブランチ作成
      • 変更を行う
      • 適切なコミットメッセージとともに変更をコミット
      • git push する
      • 適切なDescriptionとともにPull Request作成
      • CIがパスすることを確認
      • 適切なレビュワーにレビュー依頼→Approve
      • Approve もらったらPRをマージ
    • このように簡単なタスクだったとしても、脳のワーキングメモリへの負荷は重く、タスクのコンテキストスイッチ・コストが少なからず発生します
    • これらの作業手順を「xxxをフォーマットしといて」の一言だけで片付けられるのは大革命です

どれくらい成果上がった?

下記は技術的負債対象リポジトリのPR数を月ごとに棒グラフに表示したものです。

  • 🔵青: 私の作成したPR数
  • 🔴赤: 私の指示でDevinに作らせたPR数

筆者のPR数と筆者の指示によってDevinに作らせたPR数

Devin導入月である2025年2月から明らかにPR数が増えていることが見て取れます。簡単なタスクな積み重ねではあるものの、一ヶ月間でDevinと力を合わせて100PR近く作成できたのは、私の開発者としてのキャリア史上初だったのでAIによる生産性向上の力を感じさせられました。

課題

一方で使ってみて感じた課題は下記です。

  • レビューの客観性の担保
    • 多くの開発現場で<第三者視点によるレビュー>をMUSTにしているかと思います
    • 開発者自らDevinに指示出ししてPR作成してもらい、指示出しした開発者がレビューを行うのは考え方によっては<セルフレビュー>とも言え、レビューの客観性が薄まる点が課題
    • 本記事で紹介した簡単なタスクは問題ないように思いますが、クリティカルな変更は然るべき第三者視点を持った開発者のレビューを受ける必要があると感じています
  • めちゃめちゃ疲弊します
    • 一人でエンドレスに働くことができるので、本気で一日Devinと協働するととても疲弊します
    • Devinとの働き方イメージ
      • Devin指示出しA → Devin指示出しB → 自分の作業進める → Devin AのPRレビュー&マージ → Devin BのPRレビュー&マージ → 自分の作業に戻る → Devin AとDevin Bの変更をリリース
    • 一日みっちりペアプロやったときと同じような疲労感に似ています
  • やってもらうタスクはあくまでジュニア開発者レベル
    • シニアエンジニアレベルの構造的なリファクタリングをやらせるのはまだ難しいなと感じました
    • 今後どこまで難しいタスクをスムーズにできるようになっていくのか、AIエージェントの進化が楽しみです

(おまけ)ACU消費量の目安

Devinの使用料金ですが、2025年3月現在、 Teamプランが月500ドル、月に使えるACU(計算資源)は250 ACUまでとなっています。

devin.ai

タスクの種類 消費ACUs
簡単なタスク 0.5 ~ 3 ACUs
重めのタスク 5 ~ 10 ACUs

※弊社では1 SessionあたりのACU Usage Limitを10に設定しているので10が最大となります

簡単なタスク依頼のACU消費例

重いタスクのACU消費例

まとめ

とにかくリファクタリングで手数が必要な技術的負債の解消には、Devinはとても役立つと感じました。


是非読者になってください!


メドピアでは一緒に働く仲間を募集しています。
ご応募をお待ちしております!

■募集ポジションはこちら medpeer.co.jp

■エンジニア紹介ページはこちら engineer.medpeer.co.jp

■メドピア公式YouTube  www.youtube.com

■メドピア公式note
style.medpeer.co.jp