🐱
Next.jsでAI Chat UIを作る(ローカルLLMとGPT-4o mini)
はじめに
GPT-4o miniが発表されたので、APIを使ってNext.js App routerにて簡単なAIチャットページを実装してみました。Vercel AI SDKを使用しています。
コード修正:2025年1月22日
AI SDK 4.0に対応しました。(/api/chat/route.tsを修正)
GPT-4o mini
また、今回はローカルLLM(Ollama)でも利用できる内容としています。
※ マシンスペックが足りないなど環境がない方は、Ollamaの部分は読み飛ばしてください。
以前の記事
読者対象
- Next.js開発の経験者
 
事前準備
- 
OpenAI プラットフォームから API キーを作成&取得しておく。
https://platform.openai.com/ - 
Ollamaも使いたい場合は起動しておきます。
 
参考ドキュメント
コードを書く
ステップ
- Next.jsを作成する
 - .env.localにOPENAI_API_KEYを記述する
OPENAI_API_KEY=****** - app/api/chat/route.tsを作成する
 - app/page.tsxを作成する
 
Next.jsを作成
Typescript + App router + Tailwind
yarn create next-app sample-ai-chat --typescript
cd sample-ai-chat
yarn add ai @ai-sdk/openai zod
package.json(抜粋)
  "dependencies": {
    "@ai-sdk/openai": "^0.0.36",
    "ai": "^3.2.29",
    "next": "14.2.5",
    "react": "^18",
    "react-dom": "^18",
    "zod": "^3.23.8"
  },
  "devDependencies": {
    "@types/node": "^20",
    "@types/react": "^18",
    "@types/react-dom": "^18",
    "eslint": "^8",
    "eslint-config-next": "14.2.5",
    "postcss": "^8",
    "tailwindcss": "^3.4.1",
    "typescript": "^5"
  }
実装
- gpt-4o-miniの場合
 - app/api/chat/route.ts
 
import { openai, createOpenAI } from "@ai-sdk/openai";
import { streamText } from "ai";
// Vercel edgeを使う場合
export const runtime = "edge";
// ストリーミング応答を最大 30 秒まで許可
export const maxDuration = 30;
export const POST = async (req: Request) => {
  const { messages, ...data } = await req.json();
  // dataにはuseChat()でbodyで送ったsystemPromptとtemperatureが格納されています
  console.log(data);
  // OpenAI 使う場合
  const result = await streamText({
    model: openai("gpt-4o-mini"),
    system: data.systemPrompt,
    temperature: data.temperature,
    abortSignal: req.signal, // stopを使う場合に必要
    messages,
  });
  // ローカルLLMのOllamaをOpenAI互換で使う場合(例:モデルはgemma2:9b)
  // const openai = createOpenAI({
  //   baseURL: "http://localhost:11434/v1/",
  //   apiKey: "EMPTY",
  // });
  // const result = await streamText({
  //   model: openai("gemma2"),
  //   system: data.systemPrompt,
  //   temperature: data.temperature,
  //   abortSignal: req.signal, // stopを使う場合に必要
  //   messages,
  // });
  // AI SDK 3.Xの場合
  // return result.toAIStreamResponse();
  // AI SDK 4.0の場合
  return result.toDataStreamResponse();
};
- OllamaをOpenAI互換で使う場合
 - app/api/chat/route.ts
 
import { openai, createOpenAI } from "@ai-sdk/openai";
import { streamText } from "ai";
// Vercel edgeを使う場合
// export const runtime = "edge";
// ストリーミング応答を最大 30 秒まで許可
export const maxDuration = 30;
export const POST = async (req: Request) => {
  const { messages, ...data } = await req.json();
  // dataにはuseChat()でbodyで送ったsystemPromptとtemperatureが格納されています
  console.log(data);
  // OpenAI 使う場合
  // const result = await streamText({
  //   model: openai("gpt-4o-mini"),
  //   system: data.systemPrompt,
  //   temperature: data.temperature,
  //   abortSignal: req.signal, // stopを使う場合に必要
  //   messages,
  // });
  // ローカルLLMのOllamaをOpenAI互換で使う場合(例:モデルはgemma2:9b)
  const openai = createOpenAI({
    baseURL: "http://localhost:11434/v1/",
    apiKey: "EMPTY",
  });
  const result = await streamText({
    model: openai("gemma2"),
    system: data.systemPrompt,
    temperature: data.temperature,
    abortSignal: req.signal, // stopを使う場合に必要
    messages,
  });
  // AI SDK 3.Xの場合
  // return result.toAIStreamResponse();
  // AI SDK 4.0の場合
  return result.toDataStreamResponse();
};
- app/page.ts
 - systemPromptとtemperatureを設定しています。
検証でわかりやすくするのに、「にゃん」を言うように指示。 
"use client";
import { useChat } from "ai/react";
const Chat = () => {
  // システムプロンプトの設定
  const systemPrompt =
    "フレンドリーに会話してください。語尾は必ず「にゃん」をつけて。";
  // temperatureの設定
  const temperature = 0.5;
  // useChatはデフォルトでAPI ルート (/api/chat) を使用する
  const { messages, input, isLoading, stop, handleInputChange, handleSubmit } =
    useChat({
      body: {
        systemPrompt: systemPrompt,
        temperature: temperature,
      },
    });
  // チャット履歴をコンソール出力
  if (!isLoading && messages.length > 0) console.log(messages);
  return (
    <>
      <div className="mx-auto w-full max-w-md py-24 flex flex-col">
        <p className="font-bold text-lg">AI CHAT</p>
        {messages.map((m) => (
          <div key={m.id} className="w-96 mb-2 p-2">
            {m.role === "user" ? "Human: " : "AI: "}
            {m.content}
          </div>
        ))}
        <form onSubmit={handleSubmit}>
          <input
            name="box"
            className="w-96 flex rounded bottom-0 border border-gray-300 text-gray-700 mb-2 p-2"
            value={input}
            onChange={handleInputChange}
          />
          {isLoading ? (
            <button
              type="submit"
              className="opacity-50 cursor-not-allowed w-96 rounded bg-sky-500 hover:bg-sky-700 mb-2 p-2"
              disabled
            >
              Send message
            </button>
          ) : (
            <button
              type="submit"
              className="w-96 rounded bg-sky-500 hover:bg-sky-700 mb-2 p-2"
            >
              Send message
            </button>
          )}
        </form>
        <p className="w-96 text-slate-500 text-xs">
          Chromeブラウザで画面を右クリックして「検証」を選ぶと、DEVツールのコンソールからメッセージの値が確認できます。
        </p>
      </div>
    </>
  );
};
export default Chat;
動作確認
GPT-4o mini
レスポンス速いです。コストも安くていいですね。

gemma2:9b
ちょっと理解が弱いですね。

まとめ
Vercel AI SDKを使って、ストリーミング回答するシンプルなAIチャットページを実装してみました。今回は、Ollamaでも使えるようにしているのと、ページからsystemPromptやtemperatureの指定もできるようにuseChatでbodyから渡すようにしています。また、ボタンの実装はしていませんが、useChatのstopも動作するように、api側ソースにはabortSignalも記述してありますので、いろいろと改造して遊んでみてください。
Discussion