📑

人間 vs AI!生成AIを使ったゲーム風アプリ『AmIAi』を開発してみた!

2024/10/17に公開

はじめに

◆ 生成AIを使ったアプリを作ってみた!!ということで、概要や使用した技術等をまとめてみました!

作成したアプリ

⭐️ AmIAi という名のアプリです

ぜひ覗いてみてください

https://am-i-ai.com

概要

出題されるいくつかの質問に答えて、ゲーム形式でユーザの人間性を表現します!!
ユーザ vs ユーザ 形式でお互いの人間性をそれぞれのユーザがジャッジしていきます!
ジャッジ方法として、ユーザの中にAIを紛れ込ませて相対的な人間性を測ってみる形式で試しています
人間性を高めて、AIよりさらに高みに登りましょう。

機能説明

Heroページ

簡単な説明を施したHeroセクションを配置しています。
もっとリッチなUIにしたいかも...
ロボットくんがチャームポイントです

ユーザホーム画面

現在のアクティブユーザ数の情報を記載しています
また自分の人間性レートや、人間探し力レートのグラフが表示されています
ゲーム概要もここで見れます
ゲーム開始ボタンでゲームが開始されます❤️
言語を選んでマッチング開始できます!(英語、日本語)

マッチング画面

マッチング待機画面です
ここで相手ユーザが現れてくれるのを待ちます...
twitterシェアボタンもここに配置してみました...

ゲーム画面 (質問回答段階)

5つの質問にそれぞれ制限時間30秒以内で回答していきます
質問はAIに生成させてみましたが、正直よくわからない質問が多い..
ユーザにはできるだけ詳しく回答してもらいます😃

ゲーム画面 (投票段階)

ユーザからの質問の回答が全て集まると投票段階に移行します
回答には自分と相手ユーザ以外に2人のAIユーザの回答が紛れ込んでいます
一番人間ぽい回答をしているユーザに投票しましょう!!
(課題として、ユーザが質問に回答しないと未回答になっちゃうのが...)

ゲーム画面 (結果確認)

投票結果を確認できます
AIユーザを当てることができると、HumanDetectionRateが向上します!!
相手ユーザが自分を人間だと投票したら自分のHumanNessRateが向上します!!
目指せ1位!!??🏆

作ってみたきっかけ

日々生成AIが進歩していく中で、自分の作るものの価値とAIの作るものの価値の差がどんどん埋まってきているような気がして(怖い)、その差みたいなものをうまい感じに表現できるアプリを作ってみたいなと思って作成してみました😺

https://zenn.dev/h_p_yancy/articles/doc7-is-engineer-needed-in-ai-era

使った技術

抜粋して紹介していきます

言語

  • typescript

やっぱこれです!!(これしか使えない💦)

インフラ

  • GCP (Google Cloud Platform)
    • firebase functions
    • firebase hosting
    • firebase auth
    • cloud sql (postgres)

基本的に全てGCP上に載せていて費用を抑えるためにコンテナ運用ではなくfirebaseの専用関数を使いdeployしています。
cloud sqlは最安構成だけど、金かかるの嫌だ!😭

functionsとcloud sql間の通信はサーバーレスVPCアクセスを使用しています

https://cloud.google.com/vpc/docs/serverless-vpc-access?hl=ja

フロントエンド

  • React.js
    • tailwindcss
    • firbase-auth

やっぱりReact+Tailwindは最強ということで今回についても愛用しています。firebse-hostingを使うなら特に安上がりなので最高!

バックエンド

  • Nest.js
    • mikro-orm
    • firbase-auth
    • gemini

Nest.jsについても堅牢なAPI作成に一役買ってくれます。拡張性高いのがいい❤️
db driverはmikro-ormを使用。前から使ってみたかった!

https://gemini.google.com/app?hl=ja

https://mikro-orm.io/

https://nestjs.com/

API

  • ts-rest

今回の一押しライブラリです!!
tsファイルにREST-APIのボディ、レスポンスやパスを全て定義して、型づけやバリデーションからフロントエンドでの呼び出し関数(client)関数までts-restが全て管理してくれます。いちいち型づけしなくていいのが最高😍

trpcみたいな感じですが、ts-restに関してはREST-APIをベースにしているのが気に入っています。
フロントエンドでもts-restを使用しなきゃいけない!!みたいな感じではないので、ライブラリに縛られないのがいいです

https://ts-rest.com/

以下のようにContractファイルを定義してそこで全てのResponse型やvalidationやパスを全て入力していきます
openApiも対応してるみたい!

(バックエンドとフロントエンドで共通してフォルダをコピーしてCONTRACTファイルを使用するので相対importにしてあります)

/**
 * ###IMPORTANT###
 * this files import path should be relative to the contract file
 */
import { MAX_ANSWER_STRING } from './constants';
import { initContract } from '@ts-rest/core';
import { SHORT_UUID_SCHEMA } from './validation/index';
import { z } from 'zod';
const c = initContract();

export const GAME_CONTRACT = c.router({
  /**
   *  game中にgameDataを取得するエンドポイント
   * */
  progress: {
    method: 'GET',
    path: '/game/progress/:gameUserId',
    pathParams: z.object({
      gameUserId: SHORT_UUID_SCHEMA,
    }),
    responses: {
      200: c.type<{
        gameId: string;
        questions: {
          id: string;
          phase: number;
          question: string;
          shouldAnswerAt: Date;
        }[];
      }>(),
      404 : c.type<{
        message: "not found"
      }>()
    },
}

型には以下のようにアクセス可能です

/**
 * ###IMPORTANT###
 * this files import path should be relative to the contract file
 */
import { NestRequestShapes, NestResponseShapes } from '@ts-rest/nest';
import CONTRACT from './rest';

export type GameContractRequestShapes = NestRequestShapes<
  typeof CONTRACT.GAMES
>;

export type GameContractResponseShapes = NestResponseShapes<
  typeof CONTRACT.GAMES
>;

nest.jsのコントローラにも対応しています

@TsRestHandler(CONTRACT.GAMES)
  async handler() {
    return tsRestHandler(CONTRACT.GAMES, {
      progress: async (param) => {
        return await this.progressUseCase.execute(param);
      },
    });
  }

ここのパラメータについても全てCONTRACTファイルに基づいて型付けがされています
サーバー側で付与したheader等も追加できて拡張性も高いです!!

(parameter) param: {
    params: {
        waitingUserId?: string;
    };
    headers: {
        [x: string]: string | string[];
        [x: number]: string | string[];
        "x-user-id"?: string;
        "x-language"?: string;
    };
}

ts-restベースのフロントエンドのclient関数です。こちらもしっかり拡張できます!

// eslint-disable-next-line @typescript-eslint/no-unused-vars
import axios, { AxiosError, Method } from "axios";
import { initClient } from "@ts-rest/core";
import CONTRACT from "src/__generate/rest";
import { auth } from "src/firebase/config";

export const client = initClient(CONTRACT, {
  baseUrl: "",
  baseHeaders: {
    "Content-Type": "application/json",
  },
  api: async ({ path, method, headers, body }) => {
    const idToken = await auth.currentUser?.getIdToken();
    try {
      const result = await axios.request({
        method: method as Method,
        url: `${process.env.REACT_APP_BACKEND_URL}${path}`,
        headers: {
          ...headers,
          Authorization: `Bearer ${idToken}`,
        },
        data: body,
      });
      return {
        status: result.status,
        body: result.data,
        headers: result.headers as unknown as Headers,
      };
    } catch (e: Error | AxiosError | any) {
      throw e;
    }
  },
});

課題

実際に作ってみると課題が見えてきますね
そもそも構想時点で見えるべき課題だったりもするので、プランをしっかり練ってから開発に挑むことの大切さを痛感させられます(構想練るのが下手かもしれない)

  • そもそもユーザが集まらないとどうしようもない点😭
  • ユーザの善性に頼っている点
    • 例えばユーザが適当に回答してしまったらゲーム性が崩壊してしまう
  • Geminiのコストが結構かかる
  • 生成される質問が適当すぎるかも...

まとめ

めちゃめちゃts-restの紹介をしてしまいました笑

生成AIを使用したプロダクトを作るのは初めてだったのですがかなりワクワクしながら開発できたと思います
ユーザの入力を厳格なプログラムとして処理せずに生成AIに丸投げするというのは開発者としても新しい体験でした

ここまで読んでいただきありがとうございました😀

Discussion