はじめに
Vercel AI SDKを使ってChatGPTの様なチャットアプリを実装してみました
Vercel AI SDKとは
Next.jsなどのフロントエンドフレームワークを使ったWebアプリにAI機能を簡単に実装する為のツールです。
Next.jsのほか、Nuxt、Svelteなどに対応しています。
以下の様な特徴があります。
- LLMとのAPI通信からUI表示までをカバーしてこれだけで簡単に実装できる
- レスポンスがストリーミングの形で取得できる。(ChatGPTの様なテキストがパラパラと表示される)
- 統一されたAPIで異なるLLMを使用する事が出来る。(開発側のコード変更が不要)
- プロンプトにはテキストと画像が使えて、テキストでは会話履歴を含めてコンテキストに沿った生成結果が得られる。
実装例
今回は、フレームワークにNext.js、モデルにCohereを使いチャットアプリを実装します。
実装の流れは
- 準備: SDKのインストール。CohereのAPI KEYの発行。
- API実装: AIと通信する為のAPIを作成します。このAPIでAIからチャットの会話履歴を元に生成した回答をストリーミングレスポンスで取得します。Next.jsのルートハンドラを使用します。
- 画面実装: 上記をAPIを呼んで、結果を表示する画面を作成します。
準備
SDKをインストールします
Zodも後で使うので合わせてインストールしておきます
1 | npm install ai zod |
今回はCohereモデルを使用するのでCohere用のプロパイダーを追加します
1 | npm add @ai-sdk/cohere |
CohereのAPIを使用するため、公式からAPI KEYを発行します。
発行したら、.env.localファイルに以下追加します
1 | COHERE_API_KEY=発行したAPI KEY |
API
ルートハンドラ(app/api/chat/route.ts)を以下の様に作成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 | import {streamText} from 'ai'; import { cohere } from '@ai-sdk/cohere'; export async function POST(req: Request) { const { messages } = await req.json(); const result = streamText({ model: cohere('command-r-plus'), messages, }); return result.toDataStreamResponse(); } |
POSTリクエストからmessages(会話履歴)を取得して
streamText
関数に渡しています。
この関数でCohereと通信を行い、返されたオブジェクトを
toDataStreamResponse()
関数でストリーミングレスポンスで返しています。
他LLMを利用したい場合は、引数のmodelの値を他LLMに変更するだけです。
という風にAIとのやりとりはSDKが丸っとやってくれています。
画面
生成結果を表示する画面側の実装はpage.tsxを以下の様にします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | 'use client'; import { useChat } from 'ai/react'; export default function Chat() { const { messages, input, handleInputChange, handleSubmit } = useChat(); return ( <div className="flex flex-col w-full max-w-md py-24 mx-auto stretch"> {messages.map(m => ( <div key={m.id} className="whitespace-pre-wrap"> {m.role === 'user' ? 'User: ' : 'AI: '} {m.content}<br/><br/> </div> ))} <form onSubmit={handleSubmit}> <input className="fixed bottom-0 w-full max-w-md p-2 mb-8 border border-gray-300 rounded shadow-xl text-black" value={input} placeholder="Say something..." onChange={handleInputChange} /> </form> </div> ); } |
ここでは
useChatフック
を使用しています。
それぞれ、
- messages: 現在までの会話履歴。ユーザーとAI側の両方の会話データ。AIからのレスポンスがこのオブジェクトに追加される。
- input: ユーザー入力フィールドの現在の値。
- handleInputChange: 入力フィールドへの入力処理関数。
- handleSubmit: form送信処理関数。先に作ったルートハンドラー(/api/chat エンドポイント)へのPOST。useChatの第一引数に任意のエンドポイントを指定する事も可能です。
のようになっており、API通信と結果取得まで行っています。
デベロッパーは結果の表示箇所のみ実装する形になっています。
これでAIチャットボットが実装出来ました。
ツール
AIはなどリアルタイムな情報や学習されていない情報にアクセス出来ませんが、ツールを設定しておく事で必要に応じてAIに外部APIの呼び出しなどを行わせる事が出来ます。
例えば、「現在の天気を知りたい」というプロンプトに対して、天気情報取得するAPIを呼び出しその結果を生成結果に反映させる事が出来ます。
以下の様に実装を変更します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | import {streamText, tool} from 'ai'; import { cohere } from '@ai-sdk/cohere'; import {z} from "zod"; export async function POST(req: Request) { const { messages } = await req.json(); const result = streamText({ model: cohere('command-r-plus'), messages, tools: { weather: tool({ description: 'Get the weather in a location (farenheit)', parameters: z.object({ location: z.string().describe('The location to get the weather for'), }), execute: async ({ location }) => { // ここで外部API通信など行う // 今回は固定値で30としてる const temperature = 30; return { location, temperature, }; }, }), }, maxSteps: 5, // allow up to 5 steps }); return result.toDataStreamResponse(); } |
streamText()
の引数に
tools
を追加します。
コンテキストとdescriptionの値を見てAIが適宜必要なtoolを実行して、その結果を生成結果に反映させます。
上記の例で言うと、「東京の現在の気温を教えて」の様なプロンプトを入力すると「30°F」等と返ってきます。
masStepsは、一連の流れをstepという単位で数えた場合の上限です。
上記例では、step1で「プロンプトがモデルに送られ、モデルがtoolを呼ぶ」 → step2で「toolの結果がモデルに送られ、それを踏まえて結果を生成する」となるようで2stepが必要みたいです。
画面側も以下の様に変更すると、
AIの生成結果にツールが使われている場合に使われたツール名と関連オブジェクトの中身が表示されるようになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | 'use client'; import { useChat } from 'ai/react'; export default function Chat() { const { messages, input, handleInputChange, handleSubmit } = useChat(); return ( <div className="flex flex-col w-full max-w-md py-24 mx-auto stretch"> {messages.map(m => ( <div key={m.id} className="whitespace-pre-wrap"> {m.role === 'user' ? 'User: ' : 'AI: '} {m.toolInvocations ? ( <> {/*<p>{m.toolInvocations[0].toolName}</p>*/} {/*<pre>{JSON.stringify(m.toolInvocations, null, 2)}</pre>*/} <p>{m.content}<br/><br/></p> </> ) : ( <> <p>{m.content}<br/><br/></p> </> )} </div> ))} <form onSubmit={handleSubmit}> <input className="fixed bottom-0 w-full max-w-md p-2 mb-8 border border-gray-300 rounded shadow-xl text-black" value={input} placeholder="Say something..." onChange={handleInputChange} /> </form> </div> ); } |
さいごに
面倒な部分をSDKがやってくれて非常に簡単に実装出来ました。
中身についてはドキュメントが充実してるので、読めば大体は分かるかと思います