Vercelを使い社内向けチャットボットを開発して感じたこと

この記事は [クラウドワークス Advent Calendar 2024](https://qiita.com/advent-calendar/2024/crowdworks) シリーズ4の22日目の記事です。

クラウドソーシングサービス「[クラウドワークス](https://crowdworks.jp/)」（以下 crowdworks.jp ）にてエンジニアをしておりますhattorishotaです。

昨年、生成AIの技術が急速に発達し各所で生成AIを使ったサービスがリリースされるようになりました。業務効率化を図り、日常の業務で生成AIを導入しているといった話をよく耳にします。

弊社でも生成AIを利用しようという流れが昨年から起こっており、その第一歩として社内向けのチャットボットを開発することになりました。技術選定から実装までをしてみた感想を書いてみたので、お時間のある時のご一読いただければと思います。

## 背景と目的

先述しましたが、社内に生成AIの利用を推進すべくその第一歩として社内向けのチャットボットを開発しました。開発を開始する前に生成AIの利用状況について聞き取り調査を行ったところ、「利用してみたいが使い方がわからない」という声が非常に多く、社外向けの機能を提供する前に社内に普及させようとなりました。

## 使用技術
- **ホスティング**: Vercel
- **フロントエンド**: Next.js, TypeScript
- **バックエンド**: Next.js, TypeScript
- **データベース**: Vercel KV
- **ユーザー認証**: next-auth
- **AIモデル**: [OpenAIのAPI](https://platform.openai.com/docs/models)（現在は4o-miniを使用）

## 実装の詳細

### ①チャットボットのフロー設計
  - **入力**
    - ユーザーがチャットインターフェースでメッセージを入力
    - メッセージはPOSTリクエストとして送信
  - **処理**
    - auth関数を使ってユーザーの認証を確認し、ユーザーIDを取得
    - メッセージとユーザー情報がVercel KVに保存
    - OpenAI APIを使用して、ユーザーのメッセージに対する応答を生成
  - **応答**
    - OpenAIからの応答がストリーミングされ、ユーザーにリアルタイムで表示される
    - 応答は再びVercel KVに保存され、チャット履歴として管理

    このフローにより、ユーザーはリアルタイムでAIとの対話を行うことが可能になります。
  
### ②[Vercel Functions](https://vercel.com/docs/functions)の活用
  - Vercelでは`pages/api`または`app/api` に置かれているファイルはサーバーレス関数として動作しています。そのため、アクセス数に応じたリソースの調整や、リクエストがないときはサーバーがアイドル状態にならないため、無駄なコストが発生しないといったメリットがあります。
  - 実行時間の制限などのデメリットはありますが、現状テキストの送信のみのため30秒もかかることはそこまで起こりえず、デメリットと感じる場面には遭遇せずに利用できています。（Proプランでは30秒以上でタイムアウトします。）
      - ただ今後、重い画像やファイルを送信できるような実装をした際には遭遇する可能性があり、その場合は対策を考えなければと思っています。（早く実装したいところ）
  - サーバーレス関数は、JavaScript のほかに Go、Python、Ruby などでも記述できます

### ③[Vercel KV](https://vercel.com/docs/storage/vercel-kv)の利用
  - 今回はDBにVercel KVを採用しました。ホスティングをVercelで行っているのと、そこまで複雑なDB設計が必要だったわけでもなかったため半ば勢いで使ってみましたが、コストの管理が行いやすかったり、Vercel内で完結しているためドキュメントの検索も行いやすかったりと、メリットは多かったと感じました。
    
    ```tsx
    // KVからユーザー情報を取得する例
    import { kv } from '@vercel/kv'
    
    export async function getUserInfo(userId: string) {
      const user_info = await kv.hgetall<UserInfo>(`user:info:${userId}`)
    
      if (!user_info || (userId && user_info.userId !== userId)) {
        return null
      }
    
      return user_info
    }
    ```
    
### ④デプロイのプロセス
  - GithubとVercelを連携させることで、Vercel環境に簡単にデプロイができます。ワークフローを作成し、シークレット情報を設定するだけでデプロイができるようになるため、非常に便利だと感じました。
      - [GitHub Actions で継続的デリバリー
](https://zenn.dev/hayato94087/books/6a55108faa37ba/viewer/h030ni8p32akv6p4)
      - [How can I use GitHub Actions with Vercel?](https://vercel.com/guides/how-can-i-use-github-actions-with-vercel)

## 開発の中での課題
### ①NextAuthを使ったログイン認証の実装
#### 環境変数の管理
  課題: Google認証を設定する際、クライアントIDやクライアントシークレットを環境変数として管理する必要がありました。開発環境と本番環境で異なる値を使用するため、設定ミスが発生しやすく、デバッグに時間を要しました。
  解決策: .envファイルを利用して環境ごとに異なる設定を管理し、環境変数のロードを確実に行うためにdotenvパッケージを導入しました。本番環境か否かを判定する実装をすることで、シークレットキーが変わっても開発環境と本番環境での設定ミスを防ぐことができました。

  ```javascript 
  const isDevelopment = (): boolean => process.env.NODE_ENV === 'development'

  if (isDevelopment()) {
    googleClient = {
      clientId: process.env.GOOGLE_CLIENT_ID_LOCAL,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET_LOCAL
    }
  } else {
    googleClient = {
      clientId: process.env.GOOGLE_CLIENT_ID,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET
    }
  }

  export const {
    handlers: { GET, POST },
    auth
  } = NextAuth({
    providers: [GoogleProvider(googleClient)],
  })
  ```

#### カスタム認証フローの実装
  課題: 標準の認証フローでは対応できないカスタム要件（例: 特定のメールドメインのみを許可する）を実装する必要がありました。
  解決策: NextAuthのcallbacksオプションを活用し、メールドメインのチェックを行うロジックを追加しました。

  ```javascript
  export const {
    handlers: { GET, POST },
    auth
  } = NextAuth({
    callbacks: {
      async signIn({ account, profile }: { account: any | null; profile?: any }) {
        if (account?.provider === 'google') {
          return Promise.resolve(
            profile?.email_verified &&
              profile?.email &&
              [
                '@example.com',
              ].some(domain => profile.email.endsWith(domain))
          )
        }
        return Promise.resolve(true)
      },
    },
  })
  ```

#### セッション管理の選択
  課題: JWTを使用するか、データベースセッションを使用するかの選択に悩みました。JWTはステートレスでスケーラブルですが、トークンの無効化が難しいというデメリットがあります。
  解決策: スケーラビリティを重視し、JWTを選択しましたが、トークンの有効期限を短く設定し、セキュリティリスクを軽減する対策を講じました。

  ```javascript
  export const {
    handlers: { GET, POST },
    auth
  } = NextAuth({
    callbacks: {
      jwt({ token, profile }) {
        if (profile) {
          token.id = String(profile.sub)
          token.image = profile.picture
        }
        token.exp = Math.floor(Date.now() / 1000) + 7200
        return token
      },
    },
    authorized({ auth }) {
      return !!auth?.user
    },
    session({ session, token }) {
      session.user.id = token.id as string
      return session
    }
  })
  ```

### ②Vercel KVを用いたデータ管理
#### データ構造の設計
  課題: Vercel KVはキー・バリューストアであるため、リレーショナルデータベースのような複雑なクエリができません。チャット履歴やユーザー情報などのデータ構造をどのように設計するかが大きな課題でした。
  解決策: ソート済みセットを使用して、特定の条件でデータを効率的に取得できるようにしました。
  ``` typescript
  kv.hmset(`user:info:${userId}`, {
    userId: userId,
    email: email,
    createdAt: createdAt
  })

  kv.zadd(`user:chat:${userId}`, {
    score: createdAt,
    member: `user:info:${userId}`
  })
  ```

#### Redisの知識不足
  課題: Vercel KVはインメモリデータストアのRedisをベースにしたキーバリューストアです。キーと値のペア、あるいはJSONデータの保存と取得に対応します。Redisをこれまで触ってきたことがなかったため、インプットの時間を要しました。
  解決策: まずはVercel KVの公式ドキュメントを読み、それでも理解しきれない場合はRedisの公式ドキュメントを読んだりしました。そもそものRedisの使い方を理解することで、Vercel KV上での実装のイメージを掴むことができました。
[Vercel KV SDK](https://vercel.com/docs/storage/vercel-kv/kv-reference)
[Redis commands](https://redis.io/docs/latest/commands/)

## 社内での利用状況
開発が終わり、実際に社員に使っていただいて1年以上が経過しました。4ヶ月おきに「現在の業務で生成AIを利用していますか？」というアンケートを取り、実際に数字で利用率を確認してみました。（一回目は回答の文言が異なっています）結果として、

- 2024年2月
    - 使っている: 29%
    - 部分的に使っている: 41%
    - 使ってない: 30%

- 2024年6月
    - 毎日使っている: 25%
    - 週に数回: 45%
    - 使ってない: 30%

- 2024年10月
    - 毎日使っている: 50%
    - 週に数回: 35%
    - 使ってない: 15%


という結果になりました。毎日利用する人数を増やして全く使わないという人数を減らすことに成功し、より業務の中で生成AIが使われていることがわかります。個人的には、「チャットボットめっちゃ使ってます」「便利でありがたい」と言っていただける機会が多く、開発してよかったと思っています。

しかし、機能としてまだ追加したい機能があり、ファイルアップロード機能などが主に当てはまります。DBの容量が問題ないかなど調査中なので、早く機能追加してより利用満足度を高めていきたいと思っています。

## おわりに

生成AIの利用について、社内では「使った方がいいものの何を使えばいいかわからないし使い方もわからない」という方が大半でした。チャットボットを開発することでその状況を打破し、業務の中で使ってもらうことで社員の業務効率化を進めることができているのであればとても嬉しいですし、アップデートも続けていきたいです。

またVercelを使った開発は初めてでしたが、多くの事前知識を有する必要なくすぐに開発を始められるし、ドキュメントも揃っているので苦手意識も特になく開発が進められました。今後も同様なプロジェクトがあればVercelを使った開発を進めていきたいと考えています。