🦔

Vercel AI SDKのGenerative UI with RSCを試してみた

2024/12/16に公開

この記事は株式会社ガラパゴス(有志)アドベントカレンダー2024の16日目の記事です。

About

本日は、Vercel AI SDKのGenerative UI with RSCを動かしてみた時のメモを共有します。

そもそもVercel AI SDKとは?については別途紹介記事などがあるので割愛しますが、Vercel AI SDKの主要な機能の1つである、Generative UI with RSCに興味を惹かれたので、実際に動かしてみました。

基本的に公式サイトの内容をベースにしています。

https://vercel.com/templates/next.js/rsc-genui

セットアップ

まずは、公式で公開されているサンプルNextアプリケーションを作成します。

npx create-next-app --example https://github.com/vercel-labs/ai-sdk-preview-rsc-genui ai-sdk-preview-rsc-genui-example

作成後、環境変数に利用するLLMのAPI Keyを設定します。ここではOpenAIを利用していますが、利用するLLMに応じて変えて下さい。

cd ai-sdk-preview-rsc-genui-example/
cp .env.example .env
vim .env

OPENAI_API_KEY=<OpenAIのAPI Key>

npm run devします。

npm install
npm run dev

すると、 http://localhost:3000/ でアプリケーションが起動するので、アクセスして画面が表示されればOK。


とりあえず動かしてみる

Generative UI with RSC の中身を全く理解していない状態ですが、とりあえずアプリケーションのメッセージに↓の文章を投入してみました。

投入した文章 (500文字程度)

次の文章を、分かりやすい見せ方で表現して下さい。

私は3つのご飯が好きです。1つは焼き肉、2つ目は寿司、3つ目はカレーです。これらはいずれも異なる味わいと食感を持っており、その日の気分や季節によって選び分けることができます。例えば、暑い夏の日には、さっぱりとした寿司をつまみながら冷たい麦茶を飲むと、心地よい涼しさが体中に広がります。一方で、寒い冬にはスパイスのきいたカレーを頬張ると、体の芯から温まり、ほっとした気持ちになります。そして、友人たちと集まってワイワイ楽しむ焼き肉は、煙の香ばしさと肉汁が口の中ではじけ、思わず笑顔がこぼれます。 これらの料理は、単なる食事以上の意味を持っています。寿司屋のカウンター越しに握られた一貫は、子供の頃に父と通った思い出を呼び覚まし、焼き肉を囲んだ深夜の語らいは、学生時代の仲間たちとの固い絆を思い出させます。また、カレーの豊かな香りに包まれると、旅先で出会った異国の味や文化が脳裏に甦り、遠い土地と今の自分をつなぐ不思議な懐かしさを感じます。 こうした3つのご飯は、私の人生の断片を彩る大切な存在なのです。食べる度に過去と現在、そして未来がつながり、美味しさと共に心の中に豊かな物語を紡いでいくのかもしれません。

あれ?これはただのText(Markdown)出力では。。

Generative UI with RSC の機能を、状況に応じて多種多様なUIをその場で生成してくれる、v0ライクにものだと勝手に期待していたのですが、どうも違うようでした。
(そりゃそうだ)

中身を覗いてみる

そこで、公式ドキュメントとアプリケーションをソースコードをもう少し覗いてみます。

https://sdk.vercel.ai/docs/ai-sdk-rsc/streaming-react-components

公式ドキュメントに記載のあったサンプルコードで、世界観を理解しました。

公式ドキュメントにあったサンプルコード
const result = await streamUI({
  model: openai('gpt-4o'),
  prompt: 'Get the weather for San Francisco',
  text: ({ content }) => <div>{content}</div>,
  tools: {
    getWeather: {
      description: 'Get the weather for a location',
      parameters: z.object({ location: z.string() }),
      generate: async function* ({ location }) {
        yield <LoadingComponent />;
        const weather = await getWeather(location);
        return <WeatherComponent weather={weather} location={location} />;
      },
    },
  },
});

どうやら、Function Calling(Tool Calling)の枠組みの中で動く仕組みのようです。

推論の過程で特定のToolの利用が妥当であると判断した場合にToolを参照し、紐づけられたUIコンポーネントをクライアント側に返す仕組みのようです。

ここから読み取れる点として、次の2点が挙げられます。

  • Function Callingが前提
  • 事前に用意されたUIコンポーネントが前提

1点目に関しては、Function Calling未対応のo1-preview(o1)では動かないだろうな、と思って試してみましたが、予想通り動きませんでした。

2点目に関しては、これだけでも十分魅力的なアプリケーションを作れそうな感触もありますが、やはりコンポーネントを揃えて管理していくコストを考えると、もう少し動的な仕組みにならないか?と期待したいところです。

動的にコンポーネントを生成して返せないか?

そこで、動的にコンポーネントを生成して返せないか?、という問いに対して、React(Next.js)素人の自分なりに試してみました。
(が、すぐに諦めましたので、そのログだけ公開します。)

具体的には、

  • LLMにReactコンポーネントの文字列(JSX準拠)を生成させる
  • 生成した文字列をParseしてコンポーネントのオブジェクトに変換し、クライアントに返す

というシンプルなアプローチを試してみました。

react-jsx-parser の導入

文字列からReactコンポーネントに変換するライブラリをさがしたところ、react-jsx-parserなるものを発見しました。

他にもあったのですが、Star数を加味してもっとも安定してそうだったので選択しました。

npm installですぐに使えるようになります。

サンプルアプリケーションroot
npm install --save react-jsx-parser

実装してみる

サンプルアプリケーションでは、RSCコンテキストでのstreamUIの処理がapp/(preview)/actions.tsxに実装されています。

その中で、toolsの定義に1件追加しました。要約すべきテキストを受取り、LLMを使ってHTML化、そのままJSXとして解釈させてコンポーネント化する流れです。

app/(preview)/actions.tsx
    tools: {
      viewTextSummary: {
        description: "view text summary",
        parameters: z.object({
          // ツールとして整理すべき文章をtext変数で受け取る
          text: z.string(),
        }),
        generate: async function* ({ text }) {
          // LLMを使ってコンポーネントのJSXを記述させる
          const prompt = `
          下記の文章を、端的に3点で整理して表示するためのHTMLを実装して下さい。

          ・bodyタグの内側に記述するタグのみ出力して下さい
          ・外部のCSSやJavaScriptは使わない前提で実装して下さい
          ・とはいえ、味気ない見た目にならないように、できる限り分かりやすいビジュアルを目指して下さい

          <文章>
          ${text}
          `;
          const { text: jsx } = await generateText({
            model: openai("gpt-4o"),
            prompt,
          });

          // LLMが生成したJSXをコンポーネント化 
          const DynamicComponent = () => (
            <JsxParser
              bindings={{}}
              components={{}}
              jsx={jsx}
            />
          );

          return <Message role="assistant" content={<DynamicComponent></DynamicComponent>} />;
        },
      },

この状態で npm run devしてみたところ、次のようなエラーが。。

どうやら、react-jsx-parser自体が、use client的な世界で使うことを前提とした造りになっているようで、RSCのコンテキストでそのまま使うことは難しそうでした。
(react-jsx-parser以外のライブラリも試してみましたが、同様)

もう少し頑張れば出来そうな気がしましたが、時間切れでこれまで。

まとめ

自分は普段、v0を使ってフロントの開発を進めていますが、「このv0の機能性が、エンドユーザー側にもそのまま持っていけたらなー」と感じていました。

Vercel AI SDKのGenerative UI with RSCは、その世界観に向けた最初の1歩になるSDKだと思いますが、現状では固定のコンポーネントを返す世界観であるため、ある程度フロントエンドの実装側で頑張る必要がありそうです。

今後もアップデートに注目していきたいと思います。

株式会社ガラパゴス(有志)

Discussion