最近、ブログを飾って機能を作ることに努めた。
もう一度自分のコードを見てみると、どう作ったのか全く覚えていないことに気づいた。
TILの重要性を再認識した日だった。
そこで、もう一度記録することにした。

1. ChatGPT API公式ドキュメント
まず、基本的な設定は公式ドキュメントを確認すればいい。
順序は以下の通り。
API keyを環境変数に登録する。Nextjs環境ではルートフォルダに.env.local拡張子のファイルを作成した後、
OPENAI_API_KEY="api_key_here"とすればよい。npm install openaiでopenaiライブラリをインストールする。コードを書く。
公式ドキュメントで提案されているコードは以下の通り。
import OpenAI from "openai";
const openai = new OpenAI();
const completion = await openai.chat.completions.create({
model: "gpt-4o-mini",
messages: [
{ role: "system", content: "You are a helpful assistant." },
{
role: "user",
content: "Write a haiku about recursion in programming.",
},
],
});
console.log(completion.choices[0].message);
これをNextjsでyarn devで動作させると、うまくいくのが見える。
そのためapiルートとして作ればうまくいきそうだが、問題はそれほど簡単ではない。
2. 使ってみる
開発初期にはスタイルは無視して機能を作る方が早い。
単にformを一つ作り、FormDataとfetchで簡単なチャットウィンドウを作ってみよう。
"use client";
import { useRef, useState } from "react";
export default function Page() {
const [value, setValue] = useState("");
const messages = useRef<HTMLDivElement>(null);
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setValue("");
const me = document.createElement("p");
me.textContent = value;
messages.current?.appendChild(me);
const formData = new FormData();
formData.append("content", value);
const res = await fetch("/api/test", {
method: "POST",
body: formData,
});
const resJson = await res.json();
const p = document.createElement("p");
p.textContent = resJson.content;
messages.current?.appendChild(p);
}
return (
<div>
<form onSubmit={handleSubmit}>
<input
type="text"
value={value}
onChange={(e) => setValue(e.currentTarget.value)}
/>
<button type="submit"></button>
</form>
<div ref={messages} className="border-2">
<p>대화창</p>
</div>
</div>
);
}こうすると簡単なinputとチャットウィンドウが見える。
inputに内容を入力するとfetchを利用して'/api/test'にformdataを送信し応答を処理する。

次はapi側のコードを書いてみよう。
import { NextResponse } from "next/server";
import OpenAI from "openai";
const openai = new OpenAI();
export async function POST(req: Request) {
const formData = await req.formData();
const completion = await openai.chat.completions.create({
model: "gpt-4o-mini",
messages: [
{ role: "system", content: "You are a helpful assistant." },
{
role: "user",
content: formData.get("content") as string,
},
],
});
console.log(completion.choices[0].message);
return NextResponse.json(completion.choices[0].message);
}
リクエストからメッセージを解析後、openaiライブラリで応答を送信し受け取る。
その後、この応答をjsonで返せば終わりだ。
これで会話をしてみよう。

テストをしてみるとわかるが、応答が長いと長く待たなければならない。
ここで問題が発生する。
3. 問題点
問題は私たちの忍耐力がそれほど強くないことだ。
ウェブで応答が早く来ないと簡単に疲労感を感じる。

Chat GPTでタイピングするようにデータが送信されるのはAPIがchunkという分節した単位で応答し出力するためだ。
これを実装する方法を一生懸命クロールしてみた。
ソケットを利用してリアルタイムチャットを実装
openaiのストリームで応答オブジェクトを返す
VercelのAI SDKを利用して実装
この中で3番目が最も簡単に見えた。
4. VercelのAI SDKを試してみる
まず、わからないときは検索と公式ドキュメントだ。
上記のアドレスにアクセスすると、Nextjs用のコンポーネントが揃っている。
それなら私たちのプロジェクトにそのまま入れればストリーミングが可能だ。
まずAI SDKのインストールから始めよう。
yarn add ai @ai-sdk/openai zodここでzodは入力を検証するためのツールだ。
そして以下のパスのフォルダにルートを設定する。
import { openai } from "@ai-sdk/openai";
import { streamText } from "ai";
// ストリーミング応答を最大30秒間有効にする
export const maxDuration = 30;
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: openai("gpt-4o"),
messages,
});
return result.toDataStreamResponse();
}このコードでmaxDurationは最大応答時間である。
30秒以内に応答が完了しない場合、ストリーミングを中断する。
そしてクライアントにデータストリーミング用のオブジェクトを応答する。
今回はpageコンポーネントを見よう。
"use client";
import { useChat } from "ai/react";
export default function Chat() {
const { messages, input, handleInputChange, handleSubmit } = useChat({
api: "/api/aisdk",
});
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}
</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"
value={input}
placeholder="Say something..."
onChange={handleInputChange}
/>
</form>
</div>
);
}本来はuseStateやイベントハンドラーを利用して複雑に扱う必要があることがuseChat一つで済む。
useChatで構造分解割り当てされた変数がどこでどのように使われているかよく見よう。
useChat内部には様々なオプションをオブジェクトとして受け取る。
ここではAPIのアドレスを入れた。
もう実行してみよう。

本当にコピペ2回で簡単にchatgptをストリーミングで実装した。
公式ドキュメントでは天気関連のAIを作る過程もあるので参考にしてください。
5. useChatとstreamText
各関数のパラメータはやはり公式ドキュメントで調べると出てくる。
私が使ったいくつかだけ書いてみたいと思う。
1. useChat
useChat内部にinitメッセージを入力できる。
こうするとページにアクセスすると同時にメッセージが入力される。
const { messages, input, handleInputChange, handleSubmit } = useChat({
api: "/api/aisdk",
initialMessages: [
{
id: "first-message",
role: "assistant",
content: "하고싶은 말 입력해봐",
},
],
});
2. streamText
streamTextでは内部に必要な応答に必要な関数を定義できる。
chatgptに「힘센캥거루를 메롱으로 변환해줄래?」と頼むと正しく答えられない。

その場合はメロンという関数を定義しstreamText内部に入れることができる。
import { openai } from "@ai-sdk/openai";
import { streamText, tool } from "ai";
import { z } from "zod";
// ストリーミング応答を最大30秒間有効にする
export const maxDuration = 30;
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: openai("gpt-4o"),
messages,
tools: {
reverse: tool({
description: "(이름)를 메롱으로 변환하면?",
parameters: z.object({
person: z.string().describe("이름"),
}),
execute: async ({ person }) => {
const newPerson = person.split("").reverse().join("");
return {
newPerson,
};
},
}),
},
});
return result.toDataStreamResponse();
}tool関数内部でdescriptionは関数に関する説明、parametersは会話で名前に該当する内容を見つけてパラメータに入れ、実行で処理した後に返すのだ。
このようにコードを書いて質問すると、入力した文字列を逆にして返してくれる。

応答の最大長さを定義することもできる。
もしmaxTokensを以下のように指定すれば、応答の長さが非常に短くなる。
const result = streamText({
...
maxTokens:10,
...
});
6. 感想
chatgptを利用したサービスを作るのが非常に難しいと思っていたが、思ったほど難しくなかった。
良いライブラリが多いので、やる気さえあればいくらでも作ることができるだろう。
早く教育用チャットボットを作り、サービスしてみたい。
댓글을 불러오는 중...