🐱

Next.jsでAI Chat UIを作る(ローカルLLMとGPT-4o mini)

2024/07/19に公開

はじめに

GPT-4o miniが発表されたので、APIを使ってNext.js App routerにて簡単なAIチャットページを実装してみました。Vercel AI SDKを使用しています。

以前にも記事を書きましたが実装内容が古くなりましたので新たに記事にしてみました。(今回はLangChain JSは使っていません)

GPT-4o mini
https://openai.com/index/gpt-4o-mini-advancing-cost-efficient-intelligence/

また、今回はローカルLLM(Ollama)でも利用できる内容としています。
※ マシンスペックが足りないなど環境がない方は、Ollamaの部分は読み飛ばしてください。
https://zenn.dev/nari007/articles/e7da01c5907854

以前の記事
https://zenn.dev/nari007/articles/a4546b8b02492e

読者対象

  • Next.js開発の経験者

事前準備

  • OpenAI プラットフォームから API キーを作成&取得しておく。
    https://platform.openai.com/

  • Ollamaも使いたい場合は起動しておきます。

参考ドキュメント

https://sdk.vercel.ai/docs/getting-started/nextjs-app-router

コードを書く

ステップ

  • 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,
  // });

  return result.toAIStreamResponse();
};
  • 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,
  });

  return result.toAIStreamResponse();
};
  • 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