🐈

Vercel AI SDKがすごい!!!

2023/06/20に公開

この記事は語彙力D-の人間が語彙力XのChatGPTを使って書く、Vercel AIの超簡単紹介記事です。
公式のDocsが非常に簡潔に書かれていて読みやすいため是非みなさん読んでみてください。

Vercel AI SDKでできること

Vercel AI SDKを使うとあのChatGPT風のパラパラと回答が表示される系UIを超簡単に作れます!↓みたいなやつです。

sse sample

Examplesもいっぱいあって自分の作りたい物を簡単に作れます!

はじめに

僕はChatGPTのUIがすごく好きです。
本業でもOpen AIのAPIを利用した機能を実装する機会があり、ChatGPTを参考にしてパラパラと断続的にも文字が出力されるようにしました。

ChatGPTのパラパラUIには Server-Sent Events(SSE) という技術が使われています。
SSEは、サーバーからクライアントへの一方向通信を可能にする技術です。サーバーは新しいデータが利用可能になったときにクライアントにリアルタイムで更新を送信します。
つまりクライアントはサーバーの完全な返答を待つことなくレンダリングを開始できます。
詳しい説明は↓の記事がとてもわかりやすかったので是非読んでみてください。
https://zenn.dev/chot/articles/a089c203adad74

Open AIのAPIを使ったことのある人はわかると思いますが、返答が返ってくるまでものすごく時間がかかります。
そこでSSEを使うと、全文が終わるまでぐるぐるローディングを表示することなく、1文字ずつパラパラと実際に人が喋っているかのようなUIを実現できます。

僕はぐるぐるしている時間が長いとちゃんと動いているのか不安になります。
なんなら五秒くらい待たされた時点で他の画面を開いて別の作業を開始してしまうくらいにはせっかちです。
でもChatGPTはすぐに反応があり、じわじわと読むことができるので使いやすいと感じる人は多いのではないでしょうか。

Vercel AI SDKがなかった頃

ChatGPTのようなSSEを使ったUIを実装する時、フロントエンドだけでも色々な点を考慮しながら自力で実装する必要がありました。

実装してみたものがあるので貼っておきます。
https://github.com/tongari07/my-ai-app/blob/main/src/app/legacy/hooks.tsx

ものすごく簡単に作っていますが、大分ごり押ししていることがわかると思います。

以下は、各部分の簡単な解説です。

const res = await fetch("https://api.openai.com/v1/chat/completions", {
  ...
  body: JSON.stringify({
    ...
    stream: true,
    ...
  }),
});

ここでOpenAIのAPIを呼び出しています。
stream: trueを使うとSSEを返却してくれます。
実際にはサーバーで動くコードとして実装してください。

const reader = res.body?.getReader();
if (!reader) {
  return;
}

const decoder = new TextDecoder("utf-8");

const read = async (): Promise<void> => {
  const { done, value } = await reader.read();
  if (done) {
    return;
  }

ここで応答本体からリーダーを取得し、テキストデコーダを初期化します。非同期関数readを定義して、リーダーからデータを読み込みます。
もし全てのデータが読み込まれていれば(doneがtrue)、関数はそのまま終了します。

const decodedValues = decoder.decode(value).trim();
if (decodedValues.trim()) {
  const lines = decodedValues.split("\n");

デコーダーを使ってバイトデータをデコードし、それを行ごとに分割します。

for (const line of lines) {
  const message = line.replace(/^data: /, "");
  if (message === "[DONE]") {
    return;
  }
  try {
    const parsed = JSON.parse(message);
    const id = parsed.id;
    const data = parsed.choices[0].delta.content;

SSEによるデータは 'id: ' または 'data: ' で始まる文字列として返されます。
なので各行から"data: "プレフィクスを取り除き、残りの部分をJSONとしてパースします。パースしたオブジェクトからidとdataを取得します。

setMessages((current) => {
  const target = current.find((c) => c.id === id);
  return target
    ? [
        ...current.filter((c) => c.id !== id),
        { ...target, content: `${target.content}${data}` },
      ]
    : [
        ...current,
        {
          id,
          role: "assistant",
          content: data,
        },
      ];
});

過去のログも表示するためにメッセージリストとしてStateで保持しているので、そこに新しい回答を追加します。
2文字目以降はすでに追加したメッセージの末尾に追加していく形で、徐々に文章ができていくようにしています。

以上がこのコードの全体的な解説です。

実際にはデータを受け取っている最中にエラーが発生してしまった際に再度接続を試みたりなんやかんやするためのエラーハンドリングを実装する必要があります。
また、長期間開いている接続はリソースを消費します。不要になったときはEventSourceオブジェクトのcloseメソッドを呼び出して接続を閉じる必要もあります。

ちゃんと運用する際には色々なことを考慮する必要があります。

そこで登場したのがVercel AI SDK!!

Vercel AI SDKができた後の世界

Vercel AI SDKを使うとSSEを使った実装を簡略化できます。
もちろん内部の実装は自前で実装するより最適化されていると思います。

実装済みのものを見てみましょう。(少しだけ変えてます)

サーバー
https://github.com/tongari07/my-ai-app/blob/main/src/app/api/chat/route.ts

OpenAIからのストリーミング応答をOpenAIStreamに渡します。APIからのレスポンスをいい感じに変換してくれるやつです。
OpenAIStreamにはonStart、onToken、onCompletion等を渡すことができ、データをDBに保存したりキャッシュしたり、それぞれのタイミングで処理を入れることができます。

StreamingTextResponseデフォルトのヘッダー('Content-Type': 'text/plain; charset=utf-8')をつけてくれるみたいです。

クライアント
https://github.com/tongari07/my-ai-app/blob/main/src/app/page.tsx

クライアントはとても簡単で、useChatを利用するとAPIから結果をストリームを取得して、チャットのUIを作るために必要な関数を生成してくれます。
useChatはデフォルトで /api/chat を使うみたいですが、第一引数に別のエンドポイントを渡すことでオーバーライドできるみたいです。

公式のチュートリアルを実際に試してみるとその簡単さに驚くと思いますので是非やってみてください!!
https://sdk.vercel.ai/docs/getting-started

まとめ

Vercel AI SDKを使うとChatGPTのようなUXを超簡単に再現できる!
自力で実装すると大変な、各タイミングでのコールバックも簡単に書ける!
他にも様々なコンセプトが書いてあってとても勉強になりますので、ぜひ公式ドキュメントを読んでください!

ここまで稚拙な文章を読んでくださった方ありがとうございました🥰

GitHubで編集を提案

Discussion