LoginSignup
1
3

生成AIの力を借りて、爆速でChatGPTアプリを開発する

Posted at

はじめに

GPT4-Vによって設計図や構成図といったものが読み込めるようになりましたが、実際に動くWebシステム(今回はChatGPTアプリ)を生成AIの力によって爆速で開発してみます。なお、利用するリソースはAzureのものを使います。(OpenAIやデプロイ先など)。

公開サンプルを使えばそれこそazd up一発みたいなものではありますが、ゼロベースでの開発です。

なお、爆速といえばAzure Static Web Appsだろうということで、ビルド先はAzure Static Web Apps です。ちなみに、フロントにはSvelteを、バックエンドのFunctionsはNode.jsとしました。全部javascriptベースでいきます。

"爆速で"ということなので、コマンドでさくっといけるところは人間系でやってしまいます。ただし折角なので人間はコーディング自体はしない方針で行きます。

  1. (人間系) プロジェクトのテンプレなどはコマンドで作成する
  2. (生成AI) Svelteコンポーネントは画面設計書から生成する <-- GPT-4-V
  3. (生成AI) 気になる点があれば指示ベースで生成AIに修正させる <-- Github Copilot
  4. (生成AI) Functionsのコードは設計書から生成する <-- GPT-4
  5. (人間系) ローカル環境で動作を確認する
  6. (生成AI) 動作させてみて気になる点は指示ベースで生成AIに修正させる <-- GitHub Copilot
  7. (人間系) コードのリポジトリを用意して、成果物を上げる
  8. (人間系) Azure Static Web Appsを作成してデプロイする

という流れで行きたいと思います。

※注意
Azure OpenAI Servicesが使えるAzureサブスクリプションや、GitHub Copilot、またローカルのNode、github、VS Code、Azure Function Core Toolsなどはある前提です。足りないものがあれば適宜準備もしくは代替方法を利用ください。

1.(人間系) プロジェクトのテンプレなどはコマンドで作成する

まずはSvelteKitを使ってフロントエンドのプロジェクトを作成します。このあたりの土台の準備については必要に応じてこちらも参照ください。

npm create svelte@latest chatgpt-by-chatgpt

cd chatgpt-by-chatgpt

npm install

これでsvelteKitの最低限の準備ができます。
次にAzure Static Web Apps にデプロイするためにSSGの設定をいれます。
@sveltejs/adapter-staticをインストールして、設定ファイルを少し変えます。

npm install -D @sveltejs/adapter-static
svelte.config.js
import adapter from '@sveltejs/adapter-static';

export default {
    kit: {
        adapter: adapter({
            pages: 'build',
            assets: 'build',
            fallback: null,
            precompress: false,
            strict: true
        })
    }
};

あとはおまじない(SSG)をいれます。

echo export const prerender = true > src\routes\+layout.js

これでbuildフォルダにSvelte成果物がビルドされるようになります。

次に、このままFunctionsのapiフォルダを作成します。

func init api

Node.jsを指定してAzure Functionsを初期化します。
そのうえでapiフォルダに移動してから、Functionのテンプレートを作成します。

cd api
func new --template "Http Trigger" --name callgpt

あとopenaiを実行するためのライブラリをいれておきます。

npm install @azure/openai

ここまでで下準備は完了です。

2. (生成AI) Svelteコンポーネントは画面設計書から生成する

設計書からコードを生成するためGPT4-Vを使います。
以下の設計と以下のシステムプロンプトでSvelte成果物を生成させます。
なお、日本語OCRの精度が不安だったので、実際の生成は英語でやりました。後続に英語版も併記しますが、とりあえずまずは日本語版を記載します。

画面設計

SvelteコンポーネントのHTML部分とバインド変数部分、またデザインに関する補足と動作に関する説明を一枚に詰め込んでいます。デザインと動作説明のところはテキストでプロンプトとして渡してもよかったんですが、せっかくなので画像に入れ込みました。
このデザインに関する部分をきちんと書けば書くほど正しいHTMLになります。逆に現状のGPT4-Vだと位置関係や大きさといったところがうまくとれなかったりします。今回は図中くらいの表記にとどめておきますが、ちゃんとやりたければこのあたりを拡充する必要があります。
またサクッと動作検証までさせるためにも、セキュリティに関しては一旦横におきたかったので、キーやエンドポイント、モデル名は画面から入力させる方法としています。本当ならここはManaged IDentityですよね。

image.png

GPT4-Vのシステムプロンプト

システムプロンプトは、Svelteのコンポーネントを画像をもとに作成してもらうようなものにしています。

システムプロンプト
あなたはSvelteのエキスパートエンジニアです。\
描かれているものは、これから作成すべきSveltekコンポーネントの仕様です。\
描かれている内容を吟味した上で、ChatBoard.svelteを作成してください。\
なお、あいまいな箇所はこれまでのあなたの経験のもと最善となる形で補完して実装してください。\
またデザインに関して指定がなければ、近代的で洗練されたおしゃれなデザインでスタイルシートは適宜用意してください。
実際に使った英語版はこちら

画面設計(英語)

日本語をそのままGPT-4に翻訳させました。

image.png

GPT4-Vのシステムプロンプト(英語)

日本語をそのままGPT-4に翻訳させました。

system prompt
  You are an expert Svelte engineer. \
  What is depicted is the specification for a Svelte component that you should create next.\
  After carefully considering the content depicted, please create ChatBoard.svelte.\
  If there are any ambiguous areas, please fill them in and implement them in the best way possible based on your past experience.\
  Additionally, if there are no specific design instructions, please prepare a stylesheet as needed for a modern, sophisticated, and stylish design.

生成されたSvelteコンポーネント

以下のSvelteコンポーネントが生成されます。なんとなくですが、とてもいい感じですね。

ChatBoard.svelte
<script>
  import { onMount } from 'svelte';

  let endpoint = '';
  let model = '';
  let apiKey = '';
  let message = '';
  let messages = [];

  async function sendMessage() {
    const newMessage = { role: 'user', content: message };
    messages = [...messages, newMessage];

    const sendData = { endpoint, model, apiKey, messages };
    const response = await fetch('/api/callgpt', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(sendData),
    });

    const receivedData = await response.json();
    const assistantMessage = { role: 'assistant', content: receivedData.message };
    messages = [...messages, assistantMessage];
    message = ''; // Clear the message input after sending
  }
</script>

<style>
  #chat-board {
    width: 800px;
    display: flex;
    flex-direction: column;
  }

  #settings {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    margin-bottom: 20px;
  }

  #message-input {
    display: flex;
    align-items: center;
    margin-bottom: 20px;
  }

  textarea {
    flex-grow: 1;
    margin-right: 10px;
  }

  button {
    background-color: #666;
    color: white;
    padding: 10px 20px;
    border: none;
    cursor: pointer;
  }

  #messages {
    display: flex;
    flex-direction: column;
  }

  pre {
    padding: 10px;
    margin: 5px 0;
    border: 1px solid #ccc;
  }

  .user {
    align-self: flex-start;
    border-color: gray;
  }

  .assistant {
    align-self: flex-end;
    background-color: black;
    color: white;
    width: 600px;
  }
</style>

<div id="chat-board">
  <div id="settings">
    <input type="text" bind:value={endpoint} placeholder="Endpoint" />
    <input type="text" bind:value={model} placeholder="Model" />
    <input type="text" bind:value={apiKey} placeholder="API Key" />
  </div>

  <div id="message-input">
    <textarea bind:value={message} placeholder="Type your message here..."></textarea>
    <button on:click={sendMessage}>Send</button>
  </div>

  <div id="messages">
    {#each messages as msg}
      <pre class={msg.role}>{msg.content}</pre>
    {/each}
  </div>
</div>

生成されたChatBoard.svelteをsrc\routes\ChatBoard.svelteとしてファイルをつくって中身を丸ごとコピーします。

型のチェックで問題が出ているかもしれません。その場合はVS Codeで治しておきます。(Infer type of 'messages' from usageでとりあえず修正できます。本当ならちゃんと型定義をいれないとですが)ちなみにこれ修正しないでもビルド自体はできてしまうはずです。気にならない方はそのままでも大丈夫です。
image.png

3. (生成AI) 気になる点があれば指示ベースで生成AIに修正させる

2.で生成したSvetleコンポーネントを表示するSvelteの記載を忘れていました。ので、そこはGitHub Copilotで追加生成させます。(ここも含めて2.でやってももちろんいいんですが、せっかくなので)

ようは、src\routes\+page.svelteからCharBoard.svelteを横中央で表示するように、+page.svelteをVS Code開きながら GitHub Copilotに依頼します。

image.png

できました。これを+page.svelteにいれたらフロントは完成です。

4. (生成AI) Functionsのコードは設計書から生成する

次にFunctionを作成します。Functionの処理の説明には画像は不要なので、素のGPT-4-Turboでテキストベースで生成させます。ここも実際は少しでも精度を上げるために英語版でやりました。

GPT-4-Turboのシステムプロンプト

定数の宣言方法やエラーハンドリングについて補足しています。このあたりに開発規約とか標準があれば入れ込む感じですね。

システムプロンプト
あなたはAzure Functions(Node.js) のエキスパートエンジニアです。
入力された仕様とサンプルコードに基づき callgpt.jsを作成してください。
なお、極力定数はメソッド外に記載してください。
また記載がなければ、例外処理を追加してください。\
エラー発生時にはHTTPコード501でアプリケーションでエラーが発生した旨通知します。\
メッセージにはエラーのメッセージを利用してください。

GPT-4-Turboのユーザプロンプト

ユーザプロンプトには、参考情報としてコード#1~#3をいれています。それぞれの役割となぜ必要だったかは以下の通りです。(いずれも今年に入ってから出たライブラリバージョンを使うためですね)

  • コード#1 公式サイトにあるキー認証の実装サンプル
    • @azure/openai ライブラリでキー認証を実装する方法をGPTが学習していないため
  • コード#2 公式サイトにあるstreamChatCompletionの実装サンプル
    • @azure/openai ライブラリを用いたChatCompetionの実装方法をGPTが学習していないため。本当はストリームの必要はなかったのだが、公式サイトにはstreamChatCompeletionしか記載がなかったのでこれを利用。
    • ストリーミングの結果をまとめたり、気になる点はプロンプトで修正させる土台として利用
  • コード#3 Function Core Toolが吐き出すテンプレートのFunctionコード
    • 新しいバージョンのFunctionsの実装方法をGPTが学習していないため
ユーザプロンプト
# callgpt.js の仕様 (Azure Functions)

1. endpoint, model, apiKey, messagesを含んだJSONオブジェクトがrequestとして受信
2. コード#1を参考にして、@azure/openaiからclientを生成します。定数は極職メソッド外部で宣言してください。
3. systemプロンプトは、“あなたは有能なアシスタントです。”を使います。
4. 受信したmessagesの先頭に{“role” : ”system”, “content”: systemプロンプト}を入れます。
5. コード#2を参考にして、回答を取得します。なお、以下の点に注意してください。
 - コード#2は@azure/identityを使っているが、今回はAPIキーの認証を使う
 - MaxTokensが128になっているが、今回は指定しない
 - deltaはストリーミングの回答となっているが、すべて結合した上で1つの回答(answer)として取りつかう
 - deploymentId にはmodelを利用する
6. “message”: answer のJSONを返す
**標準的なAzure Functionsのコードはコード#3です。参考にしてください。 **

# サンプルコード
## コード#1: 
const { OpenAIClient, AzureKeyCredential } = require("@azure/openai");
const client = new OpenAIClient("<endpoint>", new AzureKeyCredential("<API key>"));

## コード#2:
const { OpenAIClient } = require("@azure/openai");
const { DefaultAzureCredential } = require("@azure/identity");

async function main(){
  const endpoint = "https://myaccount.openai.azure.com/";
  const client = new OpenAIClient(endpoint, new DefaultAzureCredential());

  const deploymentId = "gpt-35-turbo";

  const messages = [
    { role: "system", content: "You are a helpful assistant. You will talk like a pirate." },
    { role: "user", content: "Can you help me?" },
    { role: "assistant", content: "Arrrr! Of course, me hearty! What can I do for ye?" },
    { role: "user", content: "What's the best way to train a parrot?" },
  ];

  console.log(`Messages: ${messages.map((m) => m.content).join("\n")}`);

  const events = await client.streamChatCompletions(deploymentId, messages, { maxTokens: 128 });
  for await (const event of events) {
    for (const choice of event.choices) {
      const delta = choice.delta?.content;
      if (delta !== undefined) {
        console.log(`Chatbot: ${delta}`);
      }
    }
  }
}

main().catch((err) => {
  console.error("The sample encountered an error:", err);
});

## コード#3: 
const { app } = require('@azure/functions');

app.http('callgpt', {
    methods: ['GET', 'POST'],
    authLevel: 'anonymous',
    handler: async (request, context) => {
        context.log(`Http function processed request for url "${request.url}"`);

        const name = request.query.get('name') || await request.text() || 'world';

        return { body: `Hello, ${name}!` };
    }
});
実際に使った英語版はこちら

GPT-4-Turboのシステムプロンプト(英語)

日本語をChatGPTにそのまま翻訳させました。

You are an expert engineer in Azure Functions (Node.js). \
Based on the input specifications and sample code, please create callgpt.js. \
Please place constants outside of methods as much as possible. \
Also, if not mentioned, add exception handling. In case of an error, notify that an error occurred in the application with HTTP code 501. \
Use the error message for the message.

GPT-4-Turboのユーザプロンプト(英語)

日本語を以下略

# Specifications of callgpt.js (Azure Functions)

1. A JSON object containing endpoint, model, apiKey, and messages is received as a request.
2. Refer to Code #1 to generate a client from @azure/openai. Please declare constants outside the method as much as possible.
3. The system prompt will be, "You are a capable assistant."
4. Insert {"role": "system", "content": system prompt} at the beginning of the received messages.
5. Refer to Code #2 to obtain the response. Please note the following points:
 - Code #2 uses @azure/identity, but this time we will use API key authentication.
 - MaxTokens is set to 128, but we will not specify it this time.
 - Delta is presented as a streaming response, but we will combine all and use it as one answer (answer).
 - Use model for deploymentId.
6. Return a JSON of {“message": answer}.
**Standard Azure Functions code is Code #3. Please refer to it. **

# Sample Code
## Code #1:
const { OpenAIClient, AzureKeyCredential } = require("@azure/openai");
const client = new OpenAIClient("<endpoint>", new AzureKeyCredential("<API key>"));

## Code #2:
const { OpenAIClient } = require("@azure/openai");
const { DefaultAzureCredential } = require("@azure/identity");

async function main(){
  const endpoint = "https://myaccount.openai.azure.com/";
  const client = new OpenAIClient(endpoint, new DefaultAzureCredential());

  const deploymentId = "gpt-35-turbo";

  const messages = [
    { role: "system", content: "You are a helpful assistant. You will talk like a pirate." },
    { role: "user", content: "Can you help me?" },
    { role: "assistant", content: "Arrrr! Of course, me hearty! What can I do for ye?" },
    { role: "user", content: "What's the best way to train a parrot?" },
  ];

  console.log(`Messages: ${messages.map((m) => m.content).join("\n")}`);

  const events = await client.streamChatCompletions(deploymentId, messages, { maxTokens: 128 });
  for await (const event of events) {
    for (const choice of event.choices) {
      const delta = choice.delta?.content;
      if (delta !== undefined) {
        console.log(`Chatbot: ${delta}`);
      }
    }
  }
}

main().catch((err) => {
  console.error("The sample encountered an error:", err);
});

## Code #3:
const { app } = require('@azure/functions');

app.http('callgpt', {
    methods: ['GET', 'POST'],
    authLevel: 'anonymous',
    handler: async (request, context) => {
        context.log(`Http function processed request for url "${request.url}"`);

        const name = request.query.get('name') || await request.text() || 'world';

        return { body: `Hello, ${name}!` };
    }
});


生成されたAzure Functionsコード

パット見このままでよさそうです。

callgpt.js
const { OpenAIClient, AzureKeyCredential } = require("@azure/openai");
const { app } = require('@azure/functions');

// 定数
const SYSTEM_PROMPT = "You are a capable assistant.";

app.http('callgpt', {
    methods: ['POST'], // 仕様に基づきPOSTリクエストのみを処理すると仮定
    authLevel: 'anonymous',
    handler: async (request, context) => {
        try {
            context.log(`Http function processed request for url "${request.url}"`);

            // リクエストボディからJSONオブジェクトを抽出
            const { endpoint, model, apiKey, messages } = await request.json();

            // システムプロンプトをメッセージ配列の先頭に追加
            const modifiedMessages = [{ role: "system", content: SYSTEM_PROMPT }, ...messages];

            // OpenAIクライアントを初期化
            const client = new OpenAIClient(endpoint, new AzureKeyCredential(apiKey));

            // 応答を取得
            const events = await client.streamChatCompletions(model, modifiedMessages);
            let answer = '';

            for await (const event of events) {
                for (const choice of event.choices) {
                    const delta = choice.delta?.content;
                    if (delta !== undefined) {
                        answer += delta;
                    }
                }
            }

            // 応答を返す
            return { body: JSON.stringify({ "message": answer }), headers: { 'Content-Type': 'application/json' } };
        } catch (error) {
            context.log.error(`An error occurred in the application: ${error.message}`);
            return { status: 501, body: `An error occurred in the application: ${error.message}` };
        }
    }
});

module.exports = app;

生成されたコードを chatgpt-by-chatgpt\api\callgpt.jsにまるっと貼り付けます。

5. (人間系) ローカル環境で動作を確認する

開発環境(ローカルPC)で実行するためにchatgpt-by-chatgpt\vite.config.jsを設定します。

vite.config.js
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';

export default defineConfig({
	plugins: [sveltekit()],
    //server部分を追加
	server: {
		proxy: {
			'/api': {
				'target': 'http://127.0.0.1:7071',
				changeOrigin: true,
			},
		}
	},
});

これによりローカル起動したSvelteが、Functionsのエミュレータとつながります。

実行するには、以下の2つをやります。

  • まず chatgpt-by-chatgpt\apiフォルダで、以下を実行してFunctionsのエミュレータを起動します
func start
  • 次に、chatbpt-by-chatgptフォルダで、以下を実行してSvelteのデバッグ起動をします
npm run dev -- --open

なんか、エラーもなく画面が出ましたね。
ボタンの位置が下部ではなくて横になってしまっているのが不満な点でしょうか…(そういえばデザインに補足説明入れてませんでした)

image.png

そして、実行できました。すごい。

image.png

と、以下の点が気になります。

  • メッセージの入力エリアが縦に狭い
  • 入力メッセージの表示部分(pre)の幅が指定されていない
  • API Key が平文で表示されてしまう
  • 対話の表示(pre)の部分が改行されていない

6. (生成AI) 動作させてみて気になる点は指示ベースで生成AIに修正させる

というわけで、以下の点についてGitHub Copilotを使って直していきます。(他は(も)スタイルでサクッと指定できるので割愛)

  • 対話の表示(pre)の部分が改行されていない

該当のスタイルを選択しながらGitHub Copilotに適切な改行をするように変更を依頼します。

image.png

サンキューGitHub Copilot。(Internet Explorer対応は不要だけど…)
ということで、修正完了です。

再度実行してみます。

image.png

ちゃんと動きました!いいですね!!
今回はストリームには対応していないので回答を待つ間暇だとか、フォントが見づらいとか、色味がどうなんだとかありますが、上記と同様に設計図や指示文に記載したり、もしくは依頼することでうまくできます。

7. (人間系) コードのリポジトリを用意して、成果物を上げる

GitHubのリポジトリを用意して、git initしてremote addして、addしてcommitしてpushします。(README.mdだけ書き換えます)

ここまでの内容そのままですが、一応今回つくったものをサンプルとして公開してます↓
https://github.com/shyamagu/chatgpt-by-chatgpt

8. (人間系) Azure Static Web Appsを作成してデプロイする

Azure Static Web Appsに7.のGitHub Repositoryからデプロイさせていく構成にします。CLIでやろうとも思ったんですが、tokenとってくるのかちょっとめんどくさかったので、ポータルからぽちぽち構成します。

Azure Static Web Appsを作成の上で、Svelteを選んで、github のURLとブランチを指定して、以下の設定をいれます。

property value
アプリの場所 /
APIの場所 api
出力先 build

と、GitHub Actions でビルド時にエラーになりました。nodeのバージョンを指定しないとならないですね。。。

npm ERR! code EBADENGINE
npm ERR! engine Unsupported engine
npm ERR! engine Not compatible with your version of node/npm: @sveltejs/kit@2.5.5
npm ERR! notsup Not compatible with your version of node/npm: @sveltejs/kit@2.5.5
npm ERR! notsup Required: {"node":">=18.13"}
npm ERR! notsup Actual:   {"npm":"8.19.4","node":"v16.20.2"}

npm ERR! A complete log of this run can be found in:

chatgpt-by-chatgpt\package.jsonでnodeのバージョンを指定します。("engines": {"node": ">=18.13"},の部分)

package.json
{
	"name": "chatgpt-by-chatgpt",
	"version": "0.0.1",
	"private": true,
	"engines": {
		"node": ">=18.13"
	},

これで再度Git addしてcommitしてpushします。(Azure Static Web Appsのリソースをつくったことで、.githubにymlができています。git pullをお忘れなく…)

無事に動作確認完了です!!
(不思議な遊びになりましたw)

image.png

まとめ

今回基盤やリソースは人間系で用意しつつ、コードはすべて生成AIによってのみ生成させる方式でChatGPTライクなWebアプリを爆速で開発してみました。

基本的には、上手く動いたという点で大満足です。
ただ一方で、指定していなかったところ、例えばスタイルシートの指定がクラスではなくてIDになっていたり、微妙に依頼したデザインになっていないところや、細かく見ると不要な記載があるなど、まだまだ満点とはいきませんね。

また「爆速」とか「人間はコーディングしない」などとしていましたが、正直設計や仕様書とかを書く時間があれば実装できてしまった感はあります。ある程度実装を理解していないとGPTがコード生成できるレベルの設計や仕様もかけないでしょうし、手放しでハッピー感はありませんが、このレベルで生成してくれるとなるとある程度の規模の開発であれば生産性向上に使えることは間違いなしかなと思いました。

今後生成AIの精度がさらに上がればもう1段まかせることができますし、現状のレベルでも使いようによっては開発生産性をググっと上げれそうですね、というところで今回の爆速検証は終わりです。(爆速だったかなぁ…)

1
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
3