Vercel AI SDKを使って、SSE(ServerSentEvent)のハンドリングを簡潔に書く
こんにちは、Explazaでエンジニアをしています @_mkazutaka です。
Vercel AI SDKをみなさまご存知でしょうか。1ヶ月ぐらい前に公開されたオープンソースのライブラリで、チャットアプリケーションを構築するための便利なフックやクラスが実装されています。
チャットアプリケーションは、ChatGPTをはじめ多くの場合、SSE(Server Sent Events)を使ってストリーミングの処理が実現されています。下記のようなイメージです。このSSEの処理は、1セッションあたりで二箇所で用いられます。
[OpenAI] <- SSE -> [Web Backend] <-- SSE --> [Web Frontend]
Vercel AI SDKを使うことで、二箇所で持ちられるSSEのハンドリングの処理を簡潔に書くことができます。ドキュメントにあるコードを用いて具体的な例を見ていきます。
OpenAIとWebBackend間は、以下のコードだけで済みます。SSEの処理をうまくOpenAIStreamというクラスが隠蔽してくれています。StreamingTextResponseのインスタンスを返すことで、WebFrontendとWebBackend間の処理も簡潔に行なえます
# イメージ
import { StreamingTextResponse, OpenAIStream } from 'ai'
export async function POST(req: Request) {
const { messages } = await req.json()
const response = await openai.createChatCompletion({
model: 'gpt-4',
stream: true,
messages
})
const stream = OpenAIStream(response)
return new StreamingTextResponse(stream)
}
Webフロントエンド側の処理を見ていきます。useChatを使ってWebBackendとのSSEでの通信を行います。フック一つで済むので、実装は簡潔になります。
// イメージ
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.length > 0
? messages.map(m => (
<div key={m.id} className="whitespace-pre-wrap">
{m.role === 'user' ? 'User: ' : 'AI: '}
{m.content}
</div>
))
: null}
<form onSubmit={handleSubmit}>
<input
className="fixed bottom-0 w-full max-w-md p-2 mb-8 border border-gray-300 rounded shadow-xl"
value={input}
placeholder="Say something..."
onChange={handleInputChange}
/>
</form>
</div>
)
}
またVercel AI SDKは、OpenAIだけでなく自社で用意したカスタムサーバにも対応しています。本記事では、Vercel AI SDKを使ってカスタムサーバとチャットのやり取りを簡潔に書く方法を紹介します。
やってみる
(事前準備) プロジェクトの作成と起動
今回は、https://github.com/vercel-labs/ai/tree/main/examples/next-openai 上のプロジェクトを使います。これは、Vercel AI SDKを使ってOpenAIと通信する実装が書かれた例です。これの向き先をOpenAIではなく、ローカルホストににしてSSEでの通信が確認できるかを検証します。
npx create-next-app --example https://github.com/vercel-labs/ai/tree/main/examples/next-openai vercel-ai
cd vercel-ai
cp .env.local.example .env.local
# .env.localにOPENAI_API_KEYを設定する
vim .env.local
npm run dev
起動すると以下のような画面になります。チャットを送信すると返答が帰ってきます。
WebBakendのコードを変更する
接続先をOpenAIからLocalhostに変更します。ローカルのアプリケーションは、「LangChainのストリーミングレスポンスをFastAPIを介してクライアントに返す」にて構築したアプリケーションを利用します。
ローカルホストに向ける場合、Vercel AI SDKが提供するAIStreamクラスを利用します。AIStreamクラスについては下記をご参照ください。Next.JSのOpenAIと通信している部分(コード)を書き直していきます。
import { AIStream } from 'ai'
import type { AIStreamParser, AIStreamCallbacks } from 'ai'
function parseLocalStream(): AIStreamParser {
return data => {
const json = JSON.parse(data) as {
choices:[
{
delta: {
content: string
}
finish_reason?: string,
}
]
}
return json.choices[0].delta.content
}
}
export function LocalStream(
res: Response,
cb?: AIStreamCallbacks
): ReadableStream {
return AIStream(res, parseLocalStream(), cb)
}
以上でほぼ完成です。作ったクラスを利用するようにバックエンドのコードを変更します。最初のコードを比べると、`openai.createChatCompletion` の部分がfetchに変わって、Streamのクラスが変わったぐらいです。
export async function POST(req: Request) {
// Extract the `prompt` from the body of the request
const { messages } = await req.json()
const fetchResponse = await fetch(
'http://localhost:8000/v1/chat/completions',
{
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
messages: messages
})
}
)
const stream = LocalStream(fetchResponse)
return new StreamingTextResponse(stream)
}
WebFrontendのコードを変更する
必要なしです。useChatをそのまま使うことができます。
試してみる
以上の変更を加えた上で、メッセージを送ります。API側でログが出てるので動作させることができました
messages=[ChatMessage(role='user', content='こんにちわ')]
INFO: 127.0.0.1:62833 - "POST /v1/chat/completions HTTP/1.1" 200 OK
まとめ
Vercel AI SDKを使うとSSEのハンドリングを完結に書くことができます。
またローカルサーバ等に向き先を変更することもできるで、おすすめのライブラリです。
この記事が気に入ったらサポートをしてみませんか?