Vercel AI SDKを使って簡単にチャット風アプリを作るところまでやってみた

この記事は[ファンタアドベントカレンダー2024](https://qiita.com/advent-calendar/2024/funteractive-inc) 12月5日の記事です！

## 0. はじめに

こんにちは！ファンタラクティブでエンジニアをしている中村です

最近、猫を飼いはじめて順調に猫の下僕として成長しています！

日々の癒しに感謝しながらこの記事を書いています 🐾

## 1. この記事について

先日、社内メンバーとの技術記事を読む会で、VercelのAI SDKの記事を紹介してもらいました

「こんなのあるんだ〜」と思いつつ、そのときは触るには至りませんでしたが、アドベントカレンダーで良いタイミングなので触ってみようと思いました

この記事では、実際に手を動かして簡単なアプリを作ってみた体験を共有します
「ちょっと興味あるけど触ったことはない」という方の参考になれば嬉しいです

## 2. Vercel AI SDKってなに？

公式には

> The AI SDK is the TypeScript toolkit designed to help developers build AI-powered applications with React, Next.js, Vue, Svelte, Node.js, and more.
> 
> 引用元 : https://sdk.vercel.ai/docs/introduction

とあるので

「React, Next.js, Vue, Svelte, Node.jsとかとかでAIを組み込んだアプリケーションを簡単に構築できるTypeScriptのツールだよ！」的な感じだと思います

### このSDKの特徴

このSDKの特徴のひとつだな〜と思ったところが、**Server-Sent Events(SSE)**を活用している点です

SSEとはサーバーからクライアントへリアルタイムに通信を行うための仕組みです

[サーバー送信イベントの使用 - Web API | MDN](https://developer.mozilla.org/ja/docs/Web/API/Server-sent_events/Using_server-sent_events)

この仕組みを使うことで、以下のような体験を実現することができます

- **リアルタイムなレスポンスのストリーミング**
    - ChatGPTのチャットのようなUXを実現できる
- **シンプルな実装**
    - WebSocketほど複雑ではないため、お手軽にアプリに組み込める

では実際に環境構築から始めてみます！

## 3. 環境構築

まずは新しくNext.jsのプロジェクトを作成します
自分はCLIが進めるままにAppRouter, TailwindCSS, ESLint, Prettierを設定して作成しました
(`my-vercel-ai-sdk-app` はプロジェクト名なので適宜名前を変更してください)

```bash
npx create-next-app@latest my-vercel-ai-sdk-app
cd my-vercel-ai-sdk-app
```

次は必要なライブラリをインストールしましょう

```bash
yarn add @ai-sdk/openai
yarn add ai

# 実行時のエラー解消のため追加
yarn add zod
```

## 4. 色々触ってみる

まずは公式ドキュメントのExampleを読みながら触ってみます

こちらを参考にしました

[AI SDK Providers: OpenAI](https://sdk.vercel.ai/providers/ai-sdk-providers/openai#example)

`src/app/page.tsx`を以下のように編集しました

```tsx: src/app/page.tsx
"use client";

import { createOpenAI } from "@ai-sdk/openai";
import { generateText } from "ai";

import { useState } from "react";

export default function Home() {
  const [text, setText] = useState("");
  const openai = createOpenAI({
    apiKey: "ここにはAPIキーを入れてください！",
  });

  const handleClick = async () => {
    const { text } = await generateText({
      model: openai("gpt-3.5-turbo"),
      prompt: "Write a vegetarian lasagna recipe for 4 people.",
    });
    setText(text);
  };

  return (
    <div>
      <div className="text-black">{text}</div>
      <button className="text-black border p-2 border-black" onClick={() => handleClick()}>
        click
      </button>
    </div>
  );
}

```

実行してボタンをクリックした結果はこの通りです

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164253/62a16520-df5a-3a9b-5300-695d4e1f749a.png)

かなり簡素なUIですが、「うわ〜、こんなに簡単にできちゃうのか〜。これはすごい…！」と衝撃を受けました

### もうちょっと触ってみる

せっかくなのでもうちょっと凝って、チャット風のものを作ってみたいなと思いました

少しネットサーフィンしてみた感じこちらの記事がとても参考になりました

[Vercel AI SDK の Quickstart で AI Chatボットアプリを構築](https://zenn.dev/hayato94087/articles/7123efcba5c262#route-handler-%E3%82%92%E4%BD%9C%E6%88%90-1)

上記の記事をかなり参考にさせてもらった結果、最終的にこんなUIのものができました

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164253/8044ee7b-19a6-0b81-0c55-57b14730c14d.png)

では実際に作っていきます

まずはプロジェクトのルートに `.env.local` を作成して、先ほどベタ書きしていたAPIキーをこちらに移します

```tsx: .env.local
OPENAI_API_KEY=ここにAPIキーを書く
```

次にRoute Handlerを`app/api/chat/route.ts`に作成します

```tsx: src/app/api/chat/route.ts
import { openai } from "@ai-sdk/openai";
import { streamText } from "ai";

export const POST = async (req: Request) => {
  const { messages } = await req.json();
  const result = await streamText({
    model: openai("gpt-3.5-turbo"),
    messages,
  });

  return result.toDataStreamResponse();
};

```

次はページを作っていきます

`app/page.tsx` 内でユーザーが送信したメッセージとAIアシスタントが送信したメッセージのコンポーネントを追加します
(別ファイルに分けるべきかもしれませんが今回は面倒なので一緒にしちゃってます)

```tsx

// ユーザーが送信したメッセージ(白背景・右寄せ)
const UserMessage = ({ m }: { m: Message }) => (
  <div key={m.id} className="border px-5 py-2 rounded-full w-fit shadow-xl ml-auto bg-white">
    <span className="whitespace-pre-wrap text-black">{m.content}</span>
  </div>
);

// AIアシスタントが送信したメッセージ(黒背景・左寄せ)
const AssistantMessage = ({ m }: { m: Message }) => (
  <div key={m.id} className="border px-5 py-2 rounded-full w-fit shadow-xl mr-auto bg-black">
    <span className="text-white whitespace-pre-wrap">{m.content}</span>
  </div>
);

export default function Home() {
...
}
```

次にメインの処理を書くのですがこれが短すぎて笑っちゃいました

```tsx
const { messages, input, handleInputChange, handleSubmit } = useChat();
```

この一文だけです

「本当にこれだけでいいんですか？」と疑いたいですがこれでいけます

あとはUIの切り替えをゴニョゴニョ書いて完成です

こちらが完成品です

```tsx
"use client";

import { useChat } from "ai/react";

import type { Message } from "ai/react";

const UserMessage = ({ m }: { m: Message }) => (
  <div key={m.id} className="border px-5 py-2 rounded-3xl w-fit shadow-xl ml-auto bg-white">
    <span className="whitespace-pre-wrap text-black">{m.content}</span>
  </div>
);

const AssistantMessage = ({ m }: { m: Message }) => (
  <div key={m.id} className="border px-5 py-2 rounded-3xl w-fit shadow-xl mr-auto bg-black">
    <span className="text-white whitespace-pre-wrap">{m.content}</span>
  </div>
);

export default function Home() {
  const { messages, input, handleInputChange, handleSubmit } = useChat();

  return (
    <div className="flex flex-col w-full max-w-md py-24 mx-auto stretch gap-3 bg-white">
      {messages.map((m) => (m.role === "user" ? <UserMessage key={m.id} m={m} /> : <AssistantMessage key={m.id} m={m} />))}
      <form onSubmit={handleSubmit}>
        <input className="outline-none fixed bottom-0 w-full max-w-md p-2 mb-8 border border-gray-300 rounded shadow-xl text-black" value={input} placeholder="Enterでメッセージを送る" onChange={handleInputChange} />
      </form>
    </div>
  );
}

```

## 5. おまけ

### 環境変数について

`.env.local` で追加した環境変数がコード上のどこからも呼び出されていないのにSDKの機能が使えて「？？？」となっていましたが公式にちゃんと書いてくれてました

デフォルトで参照してくれるとのことです


> The AI SDK's OpenAI Provider will default to using the `OPENAI_API_KEY` environment variable.
>
> 引用元 : [Routing: Route Handlers](https://nextjs.org/docs/app/building-your-application/routing/route-handlers#streaming)


### OpenAIとChatGPT

この記事を書くにあたって自分は「会社でChatGPT Plus契約してるからそのAPIキーをそのまま使えばいいや！」と安易に考えていたのですが間違いでした

ChatGPT PlusとOpenAIのAPI利用は全くの別物なので別途課金が必要でした…

結果、ラ◯ボー怒りの課金を行ってこの記事を書いています

### 実装中に…

今回UI実装時点で「課金した分のAPI呼び出し数使い切っちゃった…」なんてことを防ぐために以下のような簡易的なuseChatのモックを作成しました

これであればモックのデータを使用しながらUIの実装が進められます

```tsx

const useChatMock = () => {
  const [input, setInput] = useState("");
  return {
    messages: [
      {
        id: "1",
        role: "user",
        content: "ユーザーからのメッセージ",
      },
      {
        id: "2",
        role: "assistant",
        content: "アシスタントからのメッセージ",
      },
      {
        id: "3",
        role: "user",
        content: "ユーザーからのメッセージ",
      },
    ],
    input,
    handleInputChange: (e) => setInput(e.target.value),
    handleSubmit: (e) => console.log({ e }),
  } satisfies Pick<ReturnType<typeof useChat>, "messages" | "input" | "handleInputChange" | "handleSubmit">;
};

export default function Home() {
  const { messages, input, handleInputChange, handleSubmit } = useChatMock();

  return (
  ...
  );
}

```

## 6. おわりに

この記事では、Vercel AI SDKを使い、少ないコードでAI機能を搭載したWebアプリを構築する方法を紹介しました

特に印象的だったのは、**シンプルな実装で簡単に実現できる点**です
わずかなコード変更でAIチャットのようなUXを提供できるため、初めて触ってもある程度使いこなせるなと感じました

この記事が、Vercel AI SDKに興味を持つ方にとって、手を動かすきっかけになったらと思います！

また、今回作成したコードは自分のGitHubに置いてあるのでそちらも参考になれば幸いです

https://github.com/yoheeeei/vercel-ai-sdk-app

明日はデザイナーの牧さんがおすすめのFigmaプラグインを教えてくれます！お楽しみに〜✨✨✨

## 7. 参考

以下参考にさせていただきました

https://sdk.vercel.ai/docs/introduction#ai-sdk

https://zenn.dev/hayato94087/articles/7123efcba5c262#route-handler-%E3%82%92%E4%BD%9C%E6%88%90-1