この記事は CyberAgent Developers Advent Calendar 2023 4日目の記事です。

 

こんにちは!
オンラインクリニック事業部で開発責任者を務めているクボ太郎こと窪田(@kubo_programmer)です。

私たちの事業部では「TOIRO美肌院」というサービスを運営しています。

私たちのサービスは、個々の肌の悩みに合わせた美肌治療を提供するオンライン診療プラットフォームです。提携する医師がオンライン診療により、患者の肌に寄り添って処方を行います。TOIRO美肌院を利用すれば、国内どこにいても、いつでも美肌治療を受けることが可能です。

 

TOIRO美肌院 | オンライン美容皮膚診療プラットフォーム | 美肌ケアとLINEチャットサポート

 

本記事では「TOIRO美肌院」の開発を行う上での生成AIの活用方法について紹介します。

開発体制

下記のような体制で開発を行いました。

【開発チーム】

  • 2名(7月18日~7月31日 にかけては一時的に3名)

【リリースまでの開発期間】

  • 4ヵ月(4月中旬に開発開始、8月10日リリース)

【技術スタック】

  • フロントエンド:Next.js, TypeScript, tailwindcss
  • サーバーサイド:Next.js(API route を利用), TypeScript
  • インフラ:AWS, terraform

【リリースまでに制作したもの】

  • ホームページ
  • 広告用ランディングページ
  • 予約管理システム
  • 診察前/診察後のリマインド(LINE Bot)
  • Amazon Chime を用いた通話機能
  • 医師向けの診察ページ(Amazon Cognito による認証機能付き)

【開発フロー】

  • GitFlow(開発開始から~10月26日)
  • トランクベース開発(10月26日~執筆時点)

【リリース頻度】

9月から平均して週に10個以上の機能追加/改善/修正のリリースを行なっています。

スプレッドシートによるリリース管理情報

 

生成AIの活用状況

オンラインクリニック事業部では、サービス自体に生成AIを活用しているわけではありませんが、開発効率の向上の一環として「GitHub Copilot」「ChatGPT」を利用しています。特に「GitHub Copilot」については事業部の人数が2人にも関わらず、生成したコードの量が全事業部で4位となっています。

 

GitHub org別のメトリクス | サイバーエージェントでは組織やプロダクト等の単位で org を分別している

引用元:サイバーエージェントのGitHub Copilot導入と 開発生産性

GitHub Copilot の活用方法

まずは、私の Copilot の使い方について紹介します。
Copilot の基本的な使い方である「コードの続きを予測させる」などは省略します。

活用方法①:実装を始める前に、コメントアウトで実装したい内容を記述

GitHub Copit で React, tailwindcss のコードを自動生成するデモ

やること自体は「コメントアウトで実装して欲しいことを書く」だけです。これにより、前後の実装の内容を考慮して、「コメントアウトに記載した目的」を達成するためのコードが生成されます。

特にこの手法は tailwindcss を使ったフロントエンドのコードと相性がよく、周囲のHTML構造から適したcssまでを提案してくれるため、多くの部分の実装が省略できます。

活用方法②:実装をする前に参考にして欲しいコードを貼る

GitHub Copit で typescript の便利関数を自動生成するデモ

コードの実装を始める前に、参考になるコードを該当箇所の上に貼り付けると、それを考慮したコードが生成されます。その為、ある実装を参考にして似た機能を作りたい場合については、参考にする予定のコードを貼ってから実装するだけで出力されるコードの精度が大きく変わります。

上記の例では、特定の機能のコードから便利関数を作成しようとしています。その結果、関数を命名しただけで、その関数がやりたいことを Copilot が汲み取り大部分の実装を済ませています。

GitHub Copilot の活用のための思考:

自分が GitHub Copilotでコードを作成する際には次のステップを踏みます。

  1. 自分の頭の中で実装したいコードの最終系をフワッとイメージする。
  2. それを Copilot が出力するための手前の文言系が十分かを考える。
  3. 不十分であれば、コメントアウトやコードの途中までを記載することで十分な状態にする。
  4. これで40-70点が出力される。

紹介した2つの方法は、このプロセスを具体的に行うためのアプローチです。このプロセスを応用すれば、さまざまなシチュエーションに対応する新たな方法を思いつくことができるでしょう。

注意点としては、Copilotによって出力されるコードの品質は個人的な評価にはなりますが 大体40-70点 です。それ以上の質のコードを出力させるのは難しく、精度向上のために時間を過度に費やすと、かえって生産効率が悪化してしまいます。しかし、その40-70点のコードを出力する速度は人間よりもはるかに速いため、それ自体が大きな価値は持っていると考えています。

 

ChatGPT の活用方法

オンライン診療事業部ではプロジェクトの Notion にソースコードを自動生成するためのプロンプトをまとめています。この利点は以下の通りです。

  • プロンプト内に「プロジェクト内のコードを記載し、それを参考にする」というような指示を書くことで生成されるコードに統一感が生まれる。
  • 入りたてのメンバーが実装する際に何から実装すれば良いか分からないという状態を防げる。

開発用プロンプトの共有ドキュメントのスクリーンショット

 

▼ ChatGPT を利用したコードの自動生成の例

ChatGPT で React, tailwindcss のコードを自動生成するデモ動画

 

▼ 要件

## 会員情報登録ページ

仕様①:入力するべき情報

  1. 氏名
  2. ふりがな
  3. 電話番号
  4. 生年月日
  5. 性別

仕様②:フォームの下部にこれを設置:

  • ボックス内に利用規約のベタ貼り。
  • スクロールで動かせる
  • 「プライバシーポリシーを表示する」のテキストリンク
  • 利用規約および、プライバシーポリシーに同意するかのチェックマーク

 

仕様③:登録ボタンを押すことで仮登録ができる。

▼ 自動生成されたコード

import { FC, ChangeEventHandler, MouseEventHandler, useCallback, useState } from 'react';

interface FormFieldProps {
  label: string;
  type: string;
  value: string;
  onChange: ChangeEventHandler<HTMLInputElement>;
}

const FormField: FC<FormFieldProps> = ({ label, type, value, onChange }) => (
  <div className="mb-4">
    <label className="block text-gray-700 text-sm font-bold mb-2">
      {label}
    </label>
    <input
      className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
      type={type}
      value={value}
      onChange={onChange}
    />
  </div>
);

export const RegistrationForm: FC = () => {
  const [name, setName] = useState('');
  const [furigana, setFurigana] = useState('');
  const [phone, setPhone] = useState('');
  const [birthdate, setBirthdate] = useState('');
  const [gender, setGender] = useState('');
  const [termsAccepted, setTermsAccepted] = useState(false);

  /**
  * 入力値の変更を処理するハンドラ
  */
  const handleChange = useCallback<ChangeEventHandler<HTMLInputElement>>(event => {
    const { name, value } = event.target;
    switch (name) {
      case 'name':
        setName(value);
        break;
      case 'furigana':
        setFurigana(value);
        break;
      case 'phone':
        setPhone(value);
        break;
      case 'birthdate':
        setBirthdate(value);
        break;
      case 'gender':
        setGender(value);
        break;
      case 'termsAccepted':
        setTermsAccepted(event.target.checked);
      break;
    }
  }, []);

  /**
  * 登録ボタンクリック時のハンドラ
  */
  const handleSubmit: MouseEventHandler<HTMLButtonElement> = useCallback(event => {
    event.preventDefault();
    if (!termsAccepted) {
      alert('利用規約に同意してください。');
      return;
    }
    // ここに登録処理のロジックを追加する
    console.log('登録情報:', { name, furigana, phone, birthdate, gender });
  }, [name, furigana, phone, birthdate, gender, termsAccepted]);

  return (
    <form className="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
      <FormField label="氏名" type="text" value={name} onChange={handleChange} />
      <FormField label="ふりがな" type="text" value={furigana} onChange={handleChange} />
      <FormField label="電話番号" type="tel" value={phone} onChange={handleChange} />
      <FormField label="生年月日" type="date" value={birthdate} onChange={handleChange} />
      <FormField label="性別" type="text" value={gender} onChange={handleChange} />
      <div className="mb-4">
        <label className="block text-gray-700 text-sm font-bold mb-2">
          利用規約
        </label>
        <textarea className="shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" rows={3} readOnly>
          ここに利用規約を貼り付ける
        </textarea>
      </div>
      <div className="mb-6">
        <label className="block text-gray-700 text-sm font-bold mb-2">
          <input type="checkbox" checked={termsAccepted} onChange={handleChange} name="termsAccepted" />
          利用規約およびプライバシーポリシーに同意する
        </label>
      </div>
      <button
        className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
        type="button"
        onClick={handleSubmit}
      >
        登録
      </button>
    </form>
  );
};

 

プロンプト例

実際にプロジェクト内で利用しているプロンプトをいくつか紹介します。

PRの文章を自動作生成する

下記のソースコードを実装しました。
フォーマットに従った適切なPullRequestの説明文を考えてください。

```
## 背景
このセクションでは、このPRの作成するに至った背景や前提条件を記載してください。

## 概要
このセクションでは、このPRの目的と概要を簡潔に説明してください。

## 変更点
このセクションでは、具体的な変更点や修正箇所を箇条書きでリストアップしてください。

- 変更点1
- 変更点2
- 変更点3

## 影響範囲
このセクションでは、このPRが影響を及ぼす範囲や他の機能への影響を説明してください。

## テスト項目
このセクションでは、このPRに関連する必要となるテストケースやテスト方法を記載してください。

- テストケース1
- テストケース2
- テストケース3
```

▼ 実装したソースコード
// ここに git diff main | pbcopy の結果を貼る

ソースコードの作成 – react

あなたは アクセシビリティ と セマンティックマークアップ に
精通したWebフロントのエンジニアです。

添付したコードは React/Next.js/TypeScript/tailwindcss のコンポーネントの実装です。
このコードを「export の方法、コメントの書き方、型の定義方法」をそのままに、下記に記載した要件のものに変更してください。
※ Props の interface/type は定義せずにコンポーネントの FC{…} のジェネリクス内に定義する形にしてください
※ イベントハンドラの型は `MouseEventHandler / ChangeEventHandler` などのハンドラ系のものを利用してください
※ コンポーネント内の関数には useCallback を利用するようにしてください
※ コンポーネント内の関数には内部の処理を短く説明した JsDocでコメントを残すようにしてください

▼ 要件
- XXXX
- XXXX
- XXXX
- XXXX

▼ 変更してほしいコード
```tsx
import classNames from 'classnames';
import { FC, MouseEventHandler, PropsWithChildren } from 'react';

export const Button: FC<
  PropsWithChildren<{
    className?: string;
    disabled?: boolean;
    pattern?: 'v1' | 'v2';
    onClick?: MouseEventHandler;
  }>
> = ({
  children,
  className,
  pattern = 'v1',
  disabled = false,
  onClick,
}) => {
  /**
   * ボタンクリック時のハンドラ
   */
  const handleClick: MouseEventHandler = (event) => {
    if (disabled) {
      return;
    }
    onClick?.(event);
  };

  if (disabled) {
    return (
      <button
        class={classNames(
          'flex cursor-not-allowed items-center justify-center rounded bg-gray-300 text-sm font-bold text-white',
          className
        )}
        disabled
      >
        {children}
      </button>
    );
  }
  return (
    <button
        class={classNames(
          'toiro-pointer rounded bg-blue-500 text-sm font-bold text-white',
          {
            'bg-blue-500': pattern === 'v1',
            'bg-orange-br-10': pattern === 'v2',
          },
          className
        )}
        onClick={handleClick}
      >
      {children}
    <button>
  );
};
```

ソースコードの作成 – テーブル設計

下記のような要件があります。
これを機能を実現するために最適なテーブル設計を行ない、
そのテーブルを作成するための schema.prisma の内容を考えてください。

- xxx
- xxx
- xxx

▼ 参考 (schema.prisma)
```
model Sample {
  id                        String    @id @db.Char(36)
  status                    Status    @default(PENDING)
  detail                    String?
  createdAt                 DateTime  @default(now())
  @@map(“samples”)
  @@index([createdAt])
}

enum Status {
  SUCCESS
  PENDING
  FAILED
}
```

リファクタリング – 命名

あなたはコード品質やリファクタリングに精通したエンジニアです。

以下のポイントに注意して、レビューフォーマットに従い、
実装されたコードのリファクタリングをお願いします。

- 名前の明確さ: 〇〇が変数、関数、クラスなどの名前がコンテキストに即しており、その目的や役割を明確に示しているか?
- キャメルケースまたはスネークケースの使用: 〇〇が言語やプロジェクトの標準に合わせて、適切な命名規則を使用しているか?
- 短くて簡潔な名前: 〇〇が名前が長すぎず、できるだけ短くて簡潔な形で表現されているか?ただし、名前が意味を失ってしまわないように注意が必要です。
- 同義語や類似の単語の使用を避ける: 〇〇が同じ意味の単語や似たような名前を使用することで、読み手が混乱する可能性があるため、避けるようにしているか?
- 統一性: 〇〇が同じ種類の要素に対して同じような命名パターンを使用することで、一貫性を保っているか?
- 名前の重要性: 〇〇がコード内でよく参照される重要な要素には、より具体的で意味のある名前を付けているか?
- 名前の省略形や略語の適切な使用: 〇〇が長い名前を避けるために省略形や略語を使用する場合、それが広く理解され、混乱を招かないことを確認しているか?
- メソッドや関数の動詞: 〇〇がメソッドや関数の名前には、その動作や操作を表す動詞を含めることで、より意味のある名前になっているか?

参考書籍: リーダブルコード
参考書籍: プリンシプル オブ プログラミング
参考書籍: リファクタリング 既存のコードを安全に改善する

▼ レビューフォーマット
```
## 改善点

このセクションでは、改善した内容を箇条書きで簡潔にまとめてください。

## 差分

このセクションでは、変更前のコードと変更後のコードの差分を抜き出して、解説してください。

## 変更後のコード

このセクションでは、変更後のコードを実装してください。
```

▼ 実装したコード

 

リファクタリング – 処理

あなたはコード品質やリファクタリングに精通したエンジニアです。

以下のポイントに注意して、レビューフォーマットに従い、
実装されたコードのリファクタリングをお願いします。

- 条件分岐の簡略化: 〇〇が複数の条件分岐がある場合、条件を組み合わせた複雑な論理式を使用する代わりに、個々の条件を独立したステートメントに分割しているか?また、早期リターンや例外処理を活用して、条件に合致しない場合の処理を早期に終了させているか?
- ループの簡略化: 〇〇がループ内での複雑な制御フローを避けるために、イテレータや高階関数を使用することでシンプルなループを実現しているか?また、ループを抽象化するための関数やメソッドを活用しているか?
- スイッチステートメントの代替: 〇〇が複数の条件に基づいて異なる動作を行う場合、スイッチステートメントの代わりにポリモーフィズムやストラテジーパターンなどを活用して、各条件ごとに独立したクラスや関数を作成しているか?
- エラーハンドリングのシンプル化: 〇〇が複雑なネストされたエラーハンドリングコードを避けるために、例外処理を適切に活用しているか?エラーハンドリングコードを最小限に抑え、例外を適切にスローして上位のコンテキストで処理されるようにしているか?
- コードの再利用: 〇〇が似たような処理を繰り返し書く代わりに、共通の処理を関数やクラスにまとめて再利用できるようにしているか?

参考書籍: リーダブルコード
参考書籍: プリンシプル オブ プログラミング
参考書籍: リファクタリング 既存のコードを安全に改善する

▼ レビューフォーマット
```
## 改善点

このセクションでは、改善した内容を箇条書きで簡潔にまとめてください。

## 差分

このセクションでは、変更前のコードと変更後のコードの差分を抜き出して、解説してください。

## 変更後のコード

このセクションでは、変更後のコードを実装してください。
```

▼ 実装したコード

リファクタリング – SOLIDの原則

あなたは変更の激しいプロジェクトで変更に強いコードを実装し続けたエンジニアです。

下記の点に留意して、レビューフォーマットに従い、
実装したコードのリファクタリングを行なってください。

1. 単一責任の原則の適用:
   - 各クラスやメソッドが何を行っているかを1文で説明できるか
   - コードの機能が変更された場合、影響を受けるクラスやメソッドが1つだけであるか
2. オープン・クローズド原則の適用:
   - 新しい機能を追加する際に、既存のクラスやメソッドを変更せずに済むか
      - 解決策1: InterfaceやAbstractクラスを使って抽象化する。
      - 解決策2: 抽象化したものを継承した具象クラスを作る
      - 解決策3: Factory経由でクラス生成を行う
      - クラスやメソッドの拡張性を確保するために、継承やインターフェイスを活用しているか
3. リスコフの置換原則の適用:
   - 派生クラスが基底クラスのメソッドをオーバーライドする際、引数の型や戻り値の型が同じであるか
   - 派生クラスが基底クラスのメソッドをオーバーライドする際、事前条件は同等か緩やかであり、事後条件は同等か厳密であるか
4. インターフェイス分離の原則の適用:
   - インターフェイスが小さく、特定の役割に特化しているか
   - クライアントが実装しなければならないメソッドが最小限になるようにインターフェイスを設計しているか
5. 依存性逆転の原則の適用:
   - 高レベルのモジュールが具体的な実装ではなく、抽象に依存しているか
   - 依存関係を注入するために、コンストラクタインジェクションやメソッドインジェクションを使用しているか

参考書籍: アジャイルソフトウェア開発の奥義
参考書籍: Clean Code アジャイルソフトウェア達人の技
参考書籍: Practical Object-Oriented Design in Ruby

▼ レビューフォーマット
```
## 改善点

このセクションでは、改善した具体的な内容を箇条書きで記載してください。

## 差分

このセクションでは、変更前のコードと変更後のコードの差分を抜き出して、解説してください。

## 変更後のコード

このセクションでは、変更後のコードを実装してください。
```

▼ 実装したコード

 

ChatGPTの活用のための思考:

紹介したプロンプトで生成されるコードの品質については「GitHub Copilot」と同様に大体40-70点です。その為、「GitHub Copilot」と同様にまずは基本的なコードをすぐに作り、その後人間がそれを修正するという方法が基本的な使い方です。また、リファクタリング用のプロンプトもありますが、これが提案する修正の約半分は適切とは言えません。しかし、人間が見落としやすい部分を修正してくれるので、既に高品質なコードをさらに微調整するのに役立ちます。

このように、ChatGPTを使って最初のコードの骨組みをすぐに作り、それを人間が修正し、最終チェックを再びChatGPTに任せるという流れで開発を進めることで、生成AIによる開発速度と品質の向上を実現することができます。

 

最後に

私たちのオンラインクリニック事業部では、「GitHub Copilot」や「ChatGPT」のような生成AI技術を開発プロセスに組み込んでいます。

しかし、その活用には現実的な視点が必要です。現状のAIが出力するコードの品質は一定のレベルには達していますが、人間の補完なしではまだ完璧とは言えず、人間が補完するための一助であると考えています。ただ、その補完作業自体が大きな価値を生むと考えています。

生成AIを使った業務効率化のためには常に「生成AIにできること/できないことは何か?」という問いを正しく理解した上で、できることを最大限に活用し、できないことは割り切るという思考が重要だと思います。

生成AI技術は急速に発展する分野で、その進化とともにエンジニア開発プロセスにおける役割がさらに増すと考えています。そのため生成AIの進化を素早くキャッチアップして「新たに出来るようになったことを使って業務改善できることは何か?」を考えることが、エンジニアとして求められる生成AIとの向き合い方だと考えています。

最後までお読みいただきありがとうございます。今回の記事が生成AIを開発プロセスに活用して業務効率化を考えられている皆さんの参考になれば幸いです。