Zenn
💬

【10 分で作れる】Vercel AI SDK の useChat でストリーミング対応チャットボット

2025/03/07に公開

最近 Vercel AI SDK をよく使っています。もう本当に本当に便利で、生成 AI を使ったアプリケーションを作るのにこれなしでは生活できません。

今回の記事では、Vercel AI SDK を使って基本的なチャットボットを作る方法を紹介します!

プロジェクトの作成

生成 AI との親和性が高い shadcn/ui を使ってプロジェクトを作ります。(ここはお好みでどうぞ)

pnpm dlx shadcn@latest init
┌───────────────────>
│~/dev/test on ☁️  shinji.takahashi@kikagaku.co.jp(asia-northeast1)
 ➜
└─> pnpm dlx shadcn@latest init
Packages: +220
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Progress: resolved 220, reused 0, downloaded 220, added 220, done
✔ The path /Users/shinji-takahashi/dev/test does not contain a package.json file.
  Would you like to start a new project? › Next.js
✔ What is your project named? … use-chat-demo
✔ Creating a new Next.js project.
✔ Which color would you like to use as the base color? › Neutral
✔ Writing components.json.
✔ Checking registry.
✔ Updating app/globals.css
✔ Installing dependencies.
✔ Created 1 file:
  - lib/utils.ts

Success! Project initialization completed.
You may now add components.

最近話題の Cursor Agent に Vercel AI SDK の useChat を使ってチャットボットを作ってとお願いして...

Cursor Agent

.env.local に OpenAI の API キーを設定して...

.env.local

完成!

チャットボット完成

と、さすがにこれでは各方面に怒られそうなので、もう少しちゃんとしたコードを紹介します。

useChat の使い方

といっても、Vercel AI SDK の useChat はとってもシンプルで簡単です。

Client

"use client";

import { useChat } from "@ai-sdk/react";
import { Button } from "@/components/ui/button";
import {
  Card,
  CardContent,
  CardFooter,
  CardHeader,
  CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Send } from "lucide-react";
import { Message } from "ai";

export function Chat() {
  // input, handleInputChange が useState のようなもの
  const { messages, input, handleInputChange, handleSubmit } = useChat({
    // API Routes の指定をしていなければ、デフォルトでは /api/chat を使う
    // api: "/api/chat/openai", // API Routes を指定する場合
  });

  return (
    <Card className="w-full max-w-2xl mx-auto">
      <CardHeader>
        <CardTitle className="text-xl font-bold">AIチャットボット</CardTitle>
      </CardHeader>
      <CardContent className="space-y-4 max-h-[500px] overflow-y-auto">
        {messages.map((message: Message) => (
          <div
            key={message.id}
            className={`flex ${
              message.role === "user" ? "justify-end" : "justify-start"
            }`}
          >
            <div
              className={`rounded-lg px-4 py-2 max-w-[80%] ${
                message.role === "user"
                  ? "bg-primary text-primary-foreground"
                  : "bg-muted"
              }`}
            >
              {message.content}
            </div>
          </div>
        ))}
      </CardContent>
      <CardFooter>
        <form onSubmit={handleSubmit} className="flex w-full gap-2">
          <Input
            value={input}
            onChange={handleInputChange}
            placeholder="メッセージを入力..."
            className="flex-1"
          />
          <Button type="submit" disabled={!input.trim()}>
            <Send className="h-4 w-4" />
          </Button>
        </form>
      </CardFooter>
    </Card>
  );
}

Server

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

export async function POST(req: Request) {
  const { messages } = await req.json();

  const stream = streamText({
    model: openai("gpt-4o-mini"),
    messages,
  });

  return stream.toDataStreamResponse();
}

かなりシンプルですね!これだけでストリーミング対応したチャットボットを作れます。

useChat 解説

コールバック関数を活用する

useChat にはさまざまなコールバック関数があり、チャットの状態変化に応じた処理を追加できます。ここでは、onResponse, onFinish, onError の 3 つのコールバックを紹介します。

...

export function ChatWithCallbacks() {
  const { messages, input, handleInputChange, handleSubmit } = useChat({
    // レスポンスが開始されたときに呼ばれる
    onResponse: (response) => {
      toast.info("レスポンスが開始されました", {
          description: `Status: ${response.status}`,
        });
      },

      // レスポンスが完了したときに呼ばれる
      onFinish: (message) => {
        toast.info("レスポンスが完了しました", {
          description: `Message: ${JSON.stringify(message)}`,
        });
      },

      // エラーが発生したときに呼ばれる
      onError: (error) => {
        toast.error("エラーが発生しました", {
          description: `Error: ${JSON.stringify(error)}`,
        });
        console.error("エラー詳細:", error);
      },
    });

  return (
    ...
  );
}

コールバック関数

  1. onResponse: API からのレスポンスが開始されたときに呼ばれます。
  2. onFinish: レスポンスが完了したときに呼ばれます。
  3. onError: エラーが発生したときに呼ばれます。

例えば、レスポンスが開始されたらチャットとユーザーのメッセージをデータベースに保存する、レスポンスが完了したら AI のメッセージをデータベースに保存するなどの処理に使えます。

useChat の戻り値

useChat フックの戻り値では、現在のステータスを管理する status プロパティと、レスポンスを中止する stop メソッド、エラーを処理する error プロパティなどが含まれています。

status で UI を制御する

status プロパティは、現在のチャットリクエストの状態を表します。以下の 4 つの値のいずれかを取ります。

  • "submitted": メッセージが API に送信された状態
  • "streaming": レスポンスを受信中の状態
  • "ready": レスポンスが完了した状態
  • "error": エラーが発生した状態

この status を使って、UI の状態を制御することができます。

status

export function ChatWithStatus() {
  const { messages, input, handleInputChange, handleSubmit, status } =
    useChat();

  return (
    <Card className="w-full max-w-2xl mx-auto">
      <CardHeader>
        <CardTitle className="text-xl font-bold">AIチャットボット</CardTitle>
        {status === "submitted" && (
          <div className="text-sm text-muted-foreground animate-pulse">
            AIが考え中...
          </div>
        )}
      </CardHeader>
      <CardContent className="space-y-4 max-h-[500px] overflow-y-auto">
        {/* メッセージ表示部分 */}
      </CardContent>
      <CardFooter>
        <form onSubmit={handleSubmit} className="flex w-full gap-2">
          <Input
            value={input}
            onChange={handleInputChange}
            placeholder={
              status === "streaming" ? "AIが応答中..." : "メッセージを入力..."
            }
            className="flex-1"
            disabled={status === "streaming"}
          />
          <Button
            type="submit"
            disabled={!input.trim() || status === "streaming"}
          >
            {status !== "ready" ? (
              <Loader2 className="h-4 w-4 animate-spin" />
            ) : (
              <Send className="h-4 w-4" />
            )}
          </Button>
        </form>
      </CardFooter>
    </Card>
  );
}

status の値に応じて:

  • 入力フィールドを無効化
  • 送信ボタンの見た目を変更
  • ローディングインジケーターを表示

などの制御が可能です。

stop でレスポンスを中止する

長いレスポンスを受信中に、ユーザーがキャンセルしたい場合があります。stop 関数を使うと、現在の API リクエストを中止できます。

export function ChatWithStopButton() {
  const { messages, input, handleInputChange, handleSubmit, status, stop } =
    useChat();

  return (
    <Card className="w-full max-w-2xl mx-auto">
      {/* ... */}
      <CardFooter>
        <form onSubmit={handleSubmit} className="flex w-full gap-2">
          <Input
            value={input}
            onChange={handleInputChange}
            placeholder="メッセージを入力..."
            className="flex-1"
            disabled={status === "streaming"}
          />
          {status === "streaming" ? (
            <Button type="button" variant="destructive" onClick={stop}>
              <Square className="h-4 w-4" />
            </Button>
          ) : (
            <Button type="submit" disabled={!input.trim()}>
              <Send className="h-4 w-4" />
            </Button>
          )}
        </form>
      </CardFooter>
    </Card>
  );
}
// api/chat/route.ts
const stream = streamText({
  model: openai("gpt-4o-mini"),
  messages,
  abortSignal: req.signal, // 追加
});

error でエラーを処理する

error プロパティを使って、エラーが発生した場合の処理を行うことができます。

export function ChatWithErrorHandling() {
  const { messages, input, handleInputChange, handleSubmit, error } = useChat();

  return (
    <Card className="w-full max-w-2xl mx-auto">
      <CardHeader>
        <CardTitle className="text-xl font-bold">AIチャットボット</CardTitle>
        {/* エラーが発生した場合にアラートを表示 */}
        {error && (
          <Alert variant="destructive">
            <AlertCircle className="h-4 w-4" />
            <AlertTitle>エラーが発生しました</AlertTitle>
            <AlertDescription>
              {error.message ||
                "不明なエラーが発生しました。もう一度お試しください。"}
            </AlertDescription>
          </Alert>
        )}
      </CardHeader>
      <CardContent className="space-y-4 max-h-[500px] overflow-y-auto">
        {/* メッセージ表示部分 */}
      </CardContent>
      <CardFooter>{/* メッセージ入力部分 */}</CardFooter>
    </Card>
  );
}

API Routes 側の streamText 解説

system

system は、生成 AI の応答スタイルを制御するパラメータです。例えば、語尾はニャンでお願いします。と設定すると、AI の応答がニャンで終わるようになります。

const stream = await streamText({
  model: openai("gpt-4o-mini"),
  messages,
  system: "語尾はニャンでお願いします。",
  temperature: 0.5,
  topP: 1,
  topK: 1,
});

システムプロンプト

temperature

temperature は、生成されるテキストの多様性を制御するパラメータです。

0 に近いほど出力が安定し予測可能になり、1 に近いほど創造的で多様な応答が生成されます

const stream = streamText({
  model: openai("gpt-4o-mini"),
  messages,
  temperature: 0.5,
});

topP

topP(nucleus sampling)は、トークン選択の確率分布を制御するパラメータです。

モデルは確率の高いトークンから順に、合計確率が topP の値に達するまでのトークンのみを候補として選択します。例えば、topP=0.9 の場合、確率上位のトークンで累積確率が 90%に達するまでのトークンだけが候補になります。

値が小さいほど出力が安定し、大きいほど多様な応答が生成されます。

const stream = streamText({
  model: openai("gpt-4o-mini"),
  messages,
  topP: 0.9,
});

topK

topK は、各ステップで考慮するトークンの数を制限するパラメータです。

モデルは確率の高い上位 K 個のトークンのみを候補として選択します。例えば、topK=40 の場合、確率上位 40 個のトークンだけが次のトークンの候補になります。

値が小さいほど出力が安定し、大きいほど多様な選択肢から選ばれるようになります。

const stream = streamText({
  model: openai("gpt-4o-mini"),
  messages,
  topK: 40,
});

providerOptions

providerOptions は、生成 AI プロバイダーのオプションを設定するパラメータです。

例えば、各モデルで推論を使う場合などに個別の設定を行えます。

const stream = streamText({
  model: openai("gpt-4o-mini"),
  messages,
  providerOptions: {
    openai: {
      reasoningEffort: "medium",
    },
  },
});

まとめ

この記事では、Vercel AI SDK の useChat を使って簡単にチャットボットを実装する方法を紹介しました!

クライアント側

  • useChat フックを使うだけで、チャット UI の状態管理が簡単に実装できる
  • コールバック関数(onResponseonFinishonError)を活用して、チャットの状態変化に応じた処理を追加できる
  • status プロパティを使って UI の状態を制御できる
  • stop 関数でレスポンスを中止できる
  • error プロパティでエラー処理が可能

サーバー側

  • streamText 関数を使ってストリーミングレスポンスを簡単に実装できる
  • system プロンプトで AI の応答スタイルを制御できる
  • temperaturetopPtopK などのパラメータで生成テキストの多様性を調整できる
  • providerOptions で各 AI プロバイダーの固有設定を行える

Vercel AI SDK は生成 AI を活用したアプリケーション開発で大活躍しています!特にストリーミングレスポンスの実装が簡単なのが非常に嬉しいです。

ぜひこの機会に、Vercel AI SDK を使ってみてください!

キカガク CTO 室とは?

株式会社キカガクの CTO 室は、生成 AI の活用と業務効率化の推進を担う中核部門です。
社内外の課題解決に向けた技術戦略を実行するため、以下の 2 つのチームで構成されています。

AI/LLM チーム

高度な自然言語処理や生成 AI 技術を駆使し、実用的なソリューションの開発・運用を担当しています。

Corporate ITチーム

社内 IT インフラの整備と、業務システムの統合・最適化を推進し、全社的なデジタルトランスフォーメーションを実現します。

エンジニア募集中!

株式会社キカガクは、教育を軸に人材領域で企業の DX を支援することで、未来の可能性を切り拓いています。
最先端の技術を駆使しながら、社会にインパクトを与えるプロジェクトに挑戦できる情熱あるエンジニアを募集しています!

求める人物像

  • 技術に情熱がある方
    最新の AI/LLM 技術、クラウド技術、Web 開発などに興味を持ち、常に学び続ける姿勢のある方
  • チャレンジ精神旺盛な方
    新しいアイデアや技術に積極的に取り組み、実装から改善まで自ら動ける方
  • チームプレイヤー
    多様なメンバーと協力し、課題解決に努められる方

ここが魅力!

  • 最新技術に触れられる環境
    生成 AI、自然言語処理、クラウドプラットフォームなど、業界最先端の技術に日常的に関われます。
  • 自律性と成長を促す文化
    自らのアイデアを形にできる自由な環境で、キャリアアップを支援します。
  • 社会にインパクトを与えるプロジェクト
    教育と人材の領域で企業の DX を推進するプロジェクトに参加し、社会に貢献できます。

応募方法

ご興味のある方は、ぜひ 採用情報ページ をご覧ください。
話を聞いてみたいという方も、カジュアル面談の応募をお待ちしております!

GitHubで編集を提案
株式会社キカガク

Discussion

ログインするとコメントできます