ChatGPTを利用したWebサイトを作る最短手順

まえおき

ChatGPTとリバーシで遊べるサイトを作りました!
公開時は誰でも自由に遊べましたが、今はOpenAIのAPI Keyをユーザーに準備してもらう仕組みになっています。
(GPT-3.5が安いとはいえ、1クリックごとにAPIアクセスするサービスはマズかった・・・みたいな仕様の反省点はまた別の機会に。)

このサイトを作るために何をしたのか、
ChatGPTを利用したWebサイトを作りたい人のために記録を残しておきます。

この記事では、セキュリティリスクや費用に対しても最低限の配慮しかしていません。
エラー処理も言わずもがな。
遊びの域を出たら、もっと真面目にやりましょう。

必要な能力

簡単なHTMLとJavaScriptの読み書きができる

大枠はChatGPT(GPT-4)に書いてもらえますが、
ちょっとしたバグ修正や機能変更のために、多少は読み書きできないと不便です。

AWSちょっとわかる

今回使うのはLambdaだけですが、AWSの最低限の知識は必要です。
(例えば、IAMロールなどの説明は省いています。)
あと、ログが見たければCloudWatch Logs

何はともあれ、動くものを作る

まずは何も考えず、動くものを作りましょう。
OpenAIのAPI Keyはこちらのページから取得できます。

例えば、GPT-4に

ChatGPTと、リバーシのゲームをプレイできるWebサイトを作ってください。
プレイヤーが先手を打ち、OpenAIのAPIを使ってChatGPTに先手の位置とコメントを出力させます。
html、css、jsを別々に出力してください。

みたいにザックリ投げるだけでも、それっぽいものを出力してくれます。
そこから「これも実装して、あれも実装して」とお喋りしながら、1時間ちょっとで動く物ができます。
(ここの速度感が凄すぎる!!!)

残念なことに、GPT-4はOpenAIのAPIをtext-davinci-003までしか知りません。
GPT-3.5にアクセスするには、以下のようなコードに差し替える必要があります。
ドキュメントはこちら。

const body = {
    model: "gpt-3.5-turbo",
    messages: [{"role": "user", "content": "こんにちわ!"}],
};

const requestOptions = {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${apiKey}`
    },
    body: JSON.stringify(body)
};

let rawText = "";
try {
    const response = await fetch("https://api.openai.com/v1/chat/completions", requestOptions);
    rawText = await response.text();
    const data = JSON.parse(rawText);
    const output = data.choices[0].message.content;
    console.log(output);
} catch (error) {
    if (rawText.includes("Rate limit reached"))
    {
        // Rate Limitにかかったとき
    }
    console.error(error);
}

ちなみに、OpenAIのAPI Keyをユーザーに入力してもらうバージョンは、
この時点のコードを元に公開しています。

ここからが本番

動くようになった!公開しよう!
という気持ちを抑えて、どうにかAPI Keyを隠さなければなりません。

「なんでAPI Keyを隠さないといけないの?」の説明を書こうかと思ったのですが、
その説明がいる人向けに記事を書いていたら、長さが5倍になってしまうので省きます。
言わずもがなですが、プロンプトをそのままPOSTで送るとかもやめてください。
ちゃんと分かってる人だけ続きを読んでください。

Lambdaの準備

何はともあれLambda関数を作ります。

今回はNode.js 18.x、arm64で。
armの方が安いらしいですよ。へ〜

API Keyを保存する!

今回最も守りたいAPI Keyを環境変数に入れちゃいましょう。
設定 → 環境変数に、OPENAI_API_KEYという名前で保存します。

ガリガリコードを書く!

このコードも大体ChatGPTに書いてもらったので、変なところあればツッコミください。
変更したいところはChatGPTに聞きましょう。
JavaScriptにもnode.jsにも詳しくないのでノリで書いてます。

試しに、「任意の国の料理を教えてくれるサービス」を作るとします。

  • parametersからname(国名)を受け取り
  • 「${name}の料理を教えて」とGPT-3.5に聞いて
  • 結果を返す

というシンプルながら必要な要素は抑えてるであろうコード例です。

const OPENAI_API_KEY = process.env.OPENAI_API_KEY;

export const handler = async(event) => {
    const parameters = JSON.parse(event.body);

    const body = {
        model: "gpt-3.5-turbo",
        messages: [{"role": "user", "content": `${parameters.name}の料理を教えて`}],
    };

    const requestOptions = {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'Authorization': `Bearer ${OPENAI_API_KEY}`
        },
        body: JSON.stringify(body)
    };

    let rawText = "";
    try {
        const response = await fetch("https://api.openai.com/v1/chat/completions", requestOptions);
        rawText = await response.text();
        const data = JSON.parse(rawText);
        const output = data.choices[0].message.content;
        return {
            statusCode: 200,
            body: output
        };
    } catch (error) {
        if (rawText.includes("Rate limit reached"))
        {
            console.error(`"Rate limit reached" ${error}, ${rawText}`);
            return {
                statusCode: 200,
                body: "アクセスが集中しています。"
            };
        }

        console.error(error);
        return {
            statusCode: 200, // 200を返すなっていいたいよね、わかるよ。
            body: `Error calling OpenAI API`
        };
    }
};

テストする

テストイベントに適当な名前をつけて

{
    "body": "{\"name\": \"日本\"}"
}

こんな感じのJSONをテストデータとして登録して実行してやると・・・タイムアウトします。

タイムアウト時間を伸ばす

設定 → 一般設定 からタイムアウトを30秒ぐらいにしてやります。
(デフォルトは3秒。)

再度テストする

なんかそれっぽい結果が返ってきた!!

Lambda関数を公開

Lambda Function URLsの設定

API Gatewayを使う気だったのですが、最近はLambda Function URLsなんていう便利な物があるらしいですね。
設定 → 関数URLからURLを生成してやります。
(CORSでPOSTだけに制限してもGET通っちゃうのなぜなんだろう)

アクセスしてみる

クライアント側からは

async function request() {
    const options = {
        method: 'POST',
        body: JSON.stringify({
            name: "日本"
        })
    };

    const response = await fetch(url, options);
    const data = await response.text();
    console.log(data);
}

こんな感じでアクセスできます。

ここまで出来れば、あとはLambda側にロジックを移し、
クライアントからはAPI Keyを削除するだけ!

公開まで後一歩!

OpenAIのUsage limitsを設定しよう

このままサービスインすると、青天井にOpenAIのAPIが使われてしまいます。
(実はデフォルトでは$120のリミットはありますが。)
Usage limitsの設定ページで上限を設定しておきましょう。
この上限、ピッタリそこで止まることは保証されていないので余裕を持って設定しましょう。
(自分は$5ほどオーバーしてました。)

また、$120の上限を上げたいときはこちらのページから申請できます。
(1週間ぐらいかかるらしい。)

OpenAIのRate limitsに注意しよう

OpenAIのAPIは、1分間に呼び出せる回数に制限があります。
例えば、gpt-3.5-turboなら3,500RPM (requests per minute) / 90,000TPM (tokens per minute)、
自分はTPMのlimitにめちゃくちゃ引っかかりましたが、どっちに引っ掛かるかはサービスの性質によりそうですね。
これも申請すれば上限を上げることができます。

プロンプトインジェクション対策 & API悪用対策

サンプルコードは文字列をそのまま送っちゃってますが、
プロンプトインジェクションには気をつけて最低限のパラメーターを送ったり、
Lambda側でちゃんと検証したりしましょう。

例えば「任意の国の料理を教えてくれるサービス」でいえば、入力は国コードとし、
Lambda側で国コードであることを検証して、それを正式名称に変換、プロンプトにする。など。

ログの準備

みんながどんな風に遊んでるかを見たかったら、Cloud Watch Logsからログを漁ろう!
console.logがそのまま垂れ流されます。

おわり

あとはWebサイトを公開するだけです!
ChatGPT (OpenAI API) を利用した、面白いサービスを作りましょう!
・・・と、気軽には言えないぐらいお金が溶けてしまうので、マネタイズも大事ですね。

早くGPU性能が100倍になってGPT-3.5のコストが1/100になって欲しい〜〜〜〜