はじめに
この記事では最近話題のChatGPTによってプログラムを生成する際のコツについて解説します。
前編はこちら
https://qiita.com/autotaker1984/items/5b5ac8c01d11fbbbc4a7
コードを生成するのではなく、コードを生成する過程を生成する
ChatGPTは言語モデルベースのAIです。言語モデルとは、お題(プロンプト)に沿った文章を生成するモデルです。それ以上でもそれ以下でもありません。
従ってなんらかの機能を実装してもらう際もいきなり「機能」から「コード」の生成だとあまり満足いく結果は得られません。 もちろんChatGPTはかなり博識なのでそれっぽいコードは出してきます。ただ、そのような生成の仕方だとChatGPTが学習したコードにかなり依存したものが出力されるため、実際のユースケースとはズレたものが生成されますし、生成物の著作権リスクが大きくなります。
一方で実装しようとしているものの仕様や、ドメイン知識を十分に多く含めれば、ChatGPTは驚くほど賢くそれらの文脈を理解して適切な実装を生成してくれます。
そのため、コードを生成する際にはChatGPTとペアプロしているように、何回も対話を重ねながら少しずつ目的のものに近づけていくとうまくいきます。
デモ
今回ChatGPTを使って簡単なWEBアプリを作成してみることにしました。
最終的には以下の画面のようなプロトタイプをつくることができました。
フォームを表示し、POSTされたデータをDBに登録することができています。
詳細な対話の内容とコードは下記レポジトリにて公開しています。対話全体は少し長いため、この記事ではそのハイライトをお伝えします。
要件定義
まずは作りたいもののアイデアをChatGPTに伝えてアイデアを深めてもらいました。
以下、見出しで私が入力したプロンプト、note info
でその結果を表記します。
カジュアル面談を予約する機能を実装したいです。要件を整理してください
カジュアル面談を予約するための要件を整理します。
- 用意された時間帯から予約可能な時間を選択できるようにする。
- 予約する際に、利用者の名前、電話番号、メールアドレス、および予約したい時間を入力するフォームを用意する。
- 予約が完了したら、利用者に予約の確認メールを送信する。
- 予約が完了すると、管理者にも予約内容が通知されるようにする。
- 管理者は予約を確認、承認、またはキャンセルすることができるようにする。
- 予約がキャンセルされた場合は、利用者に通知する。
上記のような要件を実装することで、カジュアル面談の予約を管理することができます。
いきなり、それっぽいてユーザストーリーを生成してくれました。利用者、管理者がいるみたいですね。管理者は面談をホストする側ということのようです。
予約フォームの仕様について聞いてみましょう。
入力フォームではどのような情報を入力しますか
入力フォームでは、利用者の名前、電話番号、メールアドレス、および予約したい時間を入力する必要があります。これらの情報は、カジュアル面談の予約を管理するために必要です。また、予約が完了したときに、予約内容を利用者と管理者に通知するために利用されます。
ちょっと味気ないフォームですね。自由記述欄もつけてもらいます。
フォームでは面談の目的も入力したいです
それは良いアイデアです。カジュアル面談の目的をフォームに入力することで、管理者は予約を確認しやすくなります。また、面談についての情報を提供することで、利用者が予約を行う前に自分が望む面談の目的を確認できるようになります。
ただし、面談の目的を入力する場合は、利用者にわかりやすいようにフォームをデザインし、適切な入力項目を用意する必要があります。例えば、面談の目的を選択式で選択できるようにするか、テキスト入力で自由に入力できるようにするなどが考えられます。
また、面談の目的が不明確な場合や、利用者が自由に入力できる場合は、入力内容を管理者が確認し、適切な面談の目的を決める必要があります。
以上を踏まえると、面談の目的を入力することで、カジュアル面談の予約をよりスムーズかつ円滑に管理することができるようになります。
ChatGPT君が誉めてくれました。こんな気配りまでできるなんて普通に嬉しいですね。
しかも、入力方法として、選択式か自由記述かの二つの案を提案してくれています。確かに選択式の方が入力しやすい気もします。まあ一旦は自由記述で進めることにします。
テーブル設計
それでは予約フォームをPostgresqlデータベースに登録するテーブル定義を設計してください
予約フォームをPostgresqlデータベースに登録するためのテーブル定義を提案します。
まず、予約内容を保存するためのreservationsテーブルを作成します。このテーブルには、利用者の名前、電話番号、メールアドレス、予約したい時間、および面談の目的などの情報を保存します。
CREATE TABLE reservations (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
phone_number TEXT NOT NULL,
email TEXT NOT NULL,
time_slot TIMESTAMP NOT NULL,
purpose TEXT NOT NULL
);
次に、予約の管理を行うためのreservation_managersテーブルを作成します。このテーブルには、管理者の名前、電話番号、メールアドレス、および管理権限などの情報を保存します。
CREATE TABLE reservation_managers (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
phone_number TEXT NOT NULL,
email TEXT NOT NULL,
permission BOOLEAN NOT NULL
);
最後に、reservationsテーブルとreservation_managersテーブルを紐付けるためのreservation_manager_reservationsテーブルを作成します。このテーブルには、予約を行った管理者と予約内容を紐付ける情報を保存します。
CREATE TABLE reservation_manager_reservations (
id SERIAL PRIMARY KEY,
reservation_id INTEGER REFERENCES reservations(id),
reservation_manager_id INTEGER REFERENCES reservation_managers(id),
status TEXT NOT NULL
);
以上のようなテーブル定義を用いることで、カジュアル面談の予約内容を管理することができます。
サクッとDBのテーブル設計をしてくれました。カラム名も適切なものになっていますね。ただ、reservation_managers
とresevations
がN:M関係なのはやりすぎな気もしますのでリテイクをお願いしましょう。
reservationsとreservation_managersはN:1対応にしたいです
N:1対応を実現するには、reservationsテーブルにreservation_manager_idというカラムを追加し、これをreservation_managersテーブルのidと紐付けるようにします。また、reservation_manager_reservationsテーブルは不要となるため、削除します。
次のようなテーブル定義になります。
CREATE TABLE reservations (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
phone_number TEXT NOT NULL,
email TEXT NOT NULL,
time_slot TIMESTAMP NOT NULL,
purpose TEXT NOT NULL,
reservation_manager_id INTEGER REFERENCES reservation_managers(id)
);
CREATE TABLE reservation_managers (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
phone_number TEXT NOT NULL,
email TEXT NOT NULL,
permission BOOLEAN NOT NULL
);
以上のようにすることで、reservationsテーブルとreservation_managersテーブルはN:1の対応となります。
嫌味ひとつ言わずにリテイクに対応してくれました。結果も完全に期待通りです。優秀ですね。
フォーム設計
テーブル設計ができたので予約フォームの移りましょう。
次に予約フォームのHTMLはどのようになりますか?
予約フォームのHTMLは次のようになります。
<form action="[予約フォームを送信するURL]" method="post">
<label for="name">名前:</label>
<input type="text" id="name" name="name">
<br>
<label for="phone_number">電話番号:</label>
<input type="text" id="phone_number" name="phone_number">
<br>
<label for="email">メールアドレス:</label>
<input type="text" id="email" name="email">
<br>
<label for="time_slot">予約時間:</label>
<input type="datetime-local" id="time_slot" name="time_slot">
<br>
<label for="purpose">面談の目的:</label>
<input type="text" id="purpose" name="purpose">
<br>
<input type="submit" value="予約する">
</form>
上記のフォームでは、名前、電話番号、メールアドレス、予約時間、および面談の目的を入力することができます。また、フォームの送信ボタンを押すと、入力された内容が送信されます。
なお、上記のHTMLはあくまでも一例であり、実際に利用する際には、フォームのデザインや入力項目を適切に調整する必要があります。
先にテーブル設計を済ませていたので、空気を読んでカラム名とフォームのフィールド名を対応させてくれていますね。こういう気遣いがありがたい。
サーバ実装
フォームもできたので実際に画面を作って表示させてみましょう。まずはPOSTエンドポイントから
それではエンドポイント名は/reservationsにしましょう。サーバのエンドポイント実装の雛形をPythonのflaskフレームワークで実装してください
次のようなPythonのflaskフレームワークを用いたエンドポイント実装の雛形を提案します。
from flask import Flask, request
app = Flask(__name__)
@app.route('/reservations', methods=['POST'])
def create_reservation():
# リクエストから予約内容を取得
name = request.form['name']
phone_number = request.form['phone_number']
email = request.form['email']
time_slot = request.form['time_slot']
purpose = request.form['purpose']
# 予約内容をデータベースに保存
# TODO: 保存処理の実装
# 予約完了のレスポンスを返す
return '予約が完了しました'
if __name__ == '__main__':
app.run()
上記のコードでは、/reservationsエンドポイントに対してPOSTメソッドでリクエストが送信された場合、create_reservation関数が実行されます。
create_reservation関数では、リクエストから予約内容を取得し、データベースに保存します。また、予約完了のレスポンスを返します。
なお、上記のコードはあくまでも雛形であり、実際に利用する際には、必要な処理を追加して適切に実装する必要があります。
それっぽいコードが生成されています。「雛形」としたのでスタブだけの実装になっているのも意図通りです。次はGETエンドポイントを定義しましょう。
/にアクセスしたらフォームのHTMLを返すようにしてください。
次のように、/エンドポイントに対してGETメソッドでリクエストが送信された場合、予約フォームのHTMLを返すようにします。
from flask import Flask, request
app = Flask(__name__)
@app.route('/', methods=['GET'])
def get_reservation_form():
# 予約フォームのHTML
form = """
<form action="/reservations" method="post">
<label for="name">名前:</label>
<input type="text" id="name" name="name">
<br>
<label for="phone_number">電話番号:</label>
<input type="text" id="phone_number" name="phone_number">
<br>
<label for="email">メールアドレス:</label>
<input type="text" id="email" name="email">
<br>
<label for="time_slot">予約時間:</label>
<input type="datetime-local" id="time_slot" name="time_slot">
<br>
<label for="purpose">面談の目的:</label>
<input type="text" id="purpose" name="purpose">
<br>
<input type="submit" value="予約する">
</form>
"""
# 予約フォームのHTMLを返す
return form
完璧ですね・・・末恐ろしい子です。ただフォームを文字列でハードコードされると辛いので
別ファイルに切り出したいところです。
フォームのHTMLをjinja2テンプレートを使って外部ファイルから読み込んでください
次のように、jinja2テンプレートを用いて予約フォームのHTMLを外部ファイルから読み込むようにします。
まず、予約フォームのHTMLを、reservation_form.htmlという名前のファイルに保存します。
<form action="/reservations" method="post">
<label for="name">名前:</label>
<input type="text" id="name" name="name">
<br>
<label for="phone_number">電話番号:</label>
<input type="text" id="phone_number" name="phone_number">
<br>
<label for="email">メールアドレス:</label>
<input type="text" id="email" name="email">
<br>
<label for="time_slot">予約時間:</label>
<input type="datetime-local" id="time_slot" name="time_slot">
<br>
<label for="purpose">面談の目的:</label>
<input type="text" id="purpose" name="purpose">
<br>
<input type="submit" value="予約する">
</form>
次に、jinja2テンプレートを用いて、上記のreservation_form.htmlファイルを読み込むようにします。
from flask import Flask, request, render_template
app = Flask(__name__)
@app.route('/', methods=['GET'])
def get_reservation_form():
# jinja2テンプレートを用いてreservation_form.htmlを読み込む
form = render_template('reservation_form.html')
# 予約フォームのHTMLを返す
return form
@app.route('/reservations', methods=['POST'])
def create_reservation():
# リクエストから予約内容を取得
name = request.form['name']
phone_number = request.form['phone_number']
email = request.form['email']
time_slot = request.form['time_slot']
purpose = request.form['purpose']
# 予約内容をデー
マジで賢いですね。jinja2テンプレートなので継承もできるかお願いしてみましょう。
reservation_form.htmlはHTMLの構造が不足しています。baseテンプレートを継承するように修正してください
次のように、reservation_form.htmlを修正することで、HTMLの構造を完成させることができます。
まず、base.htmlという名前のテンプレートを作成します。このテンプレートでは、HTMLの基本構造や必要なCSSやJavaScriptなどを記述します。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>カジュアル面談予約フォーム</title>
<!-- 必要なCSSやJavaScriptなど -->
</head>
<body>
<h1>カジュアル面談予約フォーム</h1>
{% block content %}{% endblock %}
</body>
</html>
次に、reservation_form.htmlを修正します。このファイルでは、予約フォームのHTMLを記述し、base.htmlを継承するようにします。
{% extends "base.html" %}
{% block content %}
<form action="/reservations" method="post">
<label for="name">名前:</label>
<input type="text" id="name" name="name">
<br>
<label for="phone_number">電話番号:</label>
<input type="text" id="phone_number" name="phone_number">
<br>
<label for="email">メールアドレス:</label>
<input type="text" id="email" name="email">
<br>
<label for="time_slot">予約時間:</label>
<input type="datetime-local" id="time_slot" name="time_slot">
<br>
<label for="purpose">面談の目的:</label>
<input type="text" id="purpose" name="purpose">
<br>
<input type="submit" value="予約する">
素晴らしいですが、下の方が見切れてしまっています。こんなときは「続けて」というと続きを書いてくれます。
そうですね。次のように、reservation_form.htmlを修正します。
{% extends "base.html" %}
{% block content %}
<form action="/reservations" method="post">
<label for="name">名前:</label>
<input type="text" id="name" name="name">
<br>
<label for="phone_number">電話番号:</label>
<input type="text" id="phone_number" name="phone_number">
<br>
<label for="email">メールアドレス:</label>
<input type="text" id="email" name="email">
<br>
<label for="time_slot">予約時間:</label>
<input type="datetime-local" id="time_slot" name="time_slot">
<br>
<label for="purpose">面談の目的:</label>
<input type="text" id="purpose" name="purpose">
<br>
<input type="submit" value="予約する">
</form>
{% endblock %}
特に問題ないですね。実際に動かしてみましょう。
flask --debug run
あれ、エラーになりました。エラーの治し方も聞いてみます。
jinja2.exceptions.TemplateNotFound: reservation_form.html
というエラーが発生しました
このエラーは、reservation_form.htmlという名前のテンプレートが見つからないことを示しています。
このエラーが発生した場合は、次のことを確認してください。
reservation_form.htmlが保存されているディレクトリが、Flaskアプリケーションのtemplatesディレクトリの中にあるか
Flaskアプリケーション側で、templatesディレクトリをテンプレートの保存先として指定しているか
また、Flaskアプリケーション側で、次のように、render_template()関数を使ってreservation_form.htmlを読み込んでいるかも確認してください。
from flask import Flask, render_template
app = Flask(__name__)
@app.route('/')
def index():
return render_template('reservation_form.html')
上記の問題を修正することで、このエラーを解消することができます。
templates
以下にテンプレートを配置しないといけないのですね。ファイルの場所を修正したら画面が表示され、フォームの送信も成功しました。
デザイン
画面が味気ないのでいい感じにしてもらいましょう。
bootstrap4を使ってフォームのデザインをしてください
次のように、reservation_form.htmlを修正します。ここでは、Bootstrap 4を使って予約フォームのデザインをしています。
まず、base.htmlで必要なCSSとJavaScriptを読み込むようにします。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>カジュアル面談予約フォーム</title>
<!-- Bootstrap 4を読み込む -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous">
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js" integrity="sha384-9/reFTGAW83EW2RDu2S0VKaIzap3H66lZH81PoYlFhbGU+6BZp6G7niu735Sk7lN"
次のように、reservation_form.htmlを修正します。
{% extends "base.html" %}
{% block content %}
<!-- BootstrapのContainerクラスを用いて、フォームを中央寄せする -->
<div class="container">
<h1>カジュアル面談予約フォーム</h1>
<form action="/reservations" method="post">
<div class="form-group">
<label for="name">名前:</label>
<!-- BootstrapのForm Controlクラスを用いて、フォームの入力欄をデザインする -->
<input type="text" class="form-control" id="name" name="name">
</div>
<div class="form-group">
<label for="phone_number">電話番号:</label>
<input type="text" class="form-control" id="phone_number" name="phone_number">
</div>
<div class="form-group">
<label for="email">メールアドレス:</label>
<input type="text" class="form-control" id="email" name="email">
</div>
<div class="form-group">
<label for="time_slot">予約時間:</label>
<input type="datetime-local" class="form-control" id="time_slot" name="time
いやー画面のデザインってめんどくさかったんですが、ChatGPT君に任せると本当に楽です。てかCDNのURLとかintegrityまで含めてよくそらで言えるね。記憶力半端ない。
ただ後半が見切れているのがちょっと困りますね。何度かトライしたのですが文字数が多いせいかどうしても見切れてしまいます。しょうがないので裏技を使います。
buttonを一番上に持ってきて
次のように、reservation_form.htmlを修正します。
{% extends "base.html" %}
{% block content %}
<!-- BootstrapのContainerクラスを用いて、フォームを中央寄せする -->
<div class="container">
<!-- BootstrapのButtonクラスを用いて、送信ボタンをデザインする -->
<button type="submit" class="btn btn-primary">予約する</button>
<h1>カジュアル面談予約フォーム</h1>
<form action="/reservations" method="post">
<div class="form-group">
<label for="name">名前:</label>
<!-- BootstrapのForm Controlクラスを用いて、フォームの入力欄をデザインする -->
<input type="text" class="form-control" id="name" name="name">
</div>
<div class="form-group">
<label for="phone_number">電話番号:</label>
<input type="text" class="form-control" id="phone_number" name="phone_number">
</div>
<div class="form-group">
<label for="email">メールアドレス:</label>
<input type="text" class="form-control" id="email" name="email">
</div>
<div class="form-group">
<label for="time_slot">予約時間
これで送信ボタンのdivも確認できたので切り貼りしてフォームのHTMLを完成させました。
ビジネスロジック実装
create_reservationの実装に戻ります。予約情報を保持するReservationクラスを作成してください。
次のように、予約情報を保持するReservationクラスを実装します。
class Reservation:
def __init__(self, name, phone_number, email, time_slot, purpose):
self.name = name
self.phone_number = phone_number
self.email = email
self.time_slot = time_slot
self.purpose = purpose
このクラスは、次の5つのインスタンス変数を持ちます。
- name: 予約者の名前
- phone_number: 予約者の電話番号
- email: 予約者のメールアドレス
- time_slot: 予約枠
- purpose: 予約の目的
このクラスを使用することで、予約情報を1件ずつ保持することができます。
ここでDBに登録する際には予約管理者のIDを登録する必要があることに気づきました。困りましたね。ChatGPT君と相談しましょう。
予約を登録する際に、予約管理者のIDを指定する必要がありました。どのように指定するべきかアイデアを出してください。
予約管理者のIDを指定するには、次のような方法が考えられます。
予約フォームに、予約管理者のIDを入力する欄を追加する
予約フォームから送信されたデータを受け取る際に、リクエストのrequest.formから予約管理者のIDを取得する
例えば、予約管理者のIDを入力する欄を追加した場合、次のようなフォームが作成できます。
<form action="/reservations" method="post">
<label for="name">名前:</label><br>
<input type="text" id="name" name="name"><br>
<label for="phone_number">電話番号:</label><br>
<input type="text" id="phone_number" name="phone_number"><br>
<label for="email">メールアドレス:</label><br>
<input type="email" id="email" name="email"><br>
<label
うーんフォームで入力させるのはユーザに優しくない気がしますね。
他の案はありますか
他の案として、予約管理者のIDをURLのクエリパラメータとして渡す方法が考えられます。この場合、予約フォームのHTMLは次のようになります。
<form action="/reservations?manager_id=1" method="post">
<label for="name">名前:</label><br>
<input type="text" id="name" name="name"><br>
<label for="phone_number">電話番号:</label><br>
<input type="text" id="phone_number" name="phone_number"><br>
<label for="email">メールアドレス:</label><br>
<input type="email" id="email" name="email"><br>
<label for="time_slot">予約枠:</label><br>
<input type="text" id="time_slot" name="time_slot"><br>
<label for="purpose">予約の目的:</label><br>
<input type="text" id="purpose" name="purpose"><br><br>
<button type="submit">送信する</button>
</form>
この方法では、/reservationsエンドポイントにリクエストが送られる際に、manager_idという名前のクエリパラメータが含まれていることが前提になります。そのため、/reservationsエンドポイントの実装では、リクエストからmanager_idを取得し、それを使って予約管理者を特定することができます。
@app.route('/reservations')
def reservations():
# リクエストからmanager_idを取得
manager_id = request.args.get('manager_id')
# manager_idを使って予約管理者を特定する
manager = find_manager_by_id(manager_id)
# 予約管理者のIDを使って予約を登録する
create_reservation(manager, request.form)
return '予約が完了しました'
上記のようにして、予約管理者のIDを指定することができます。
これはよさそう。
いや本当に君ただの言語モデルなの?だんだん意識があるんじゃないかと思えてくる・・・
実際にはDAOを実装したり、まだもう少しやり取りが続きますが、残りは割愛します。
オチ
# ありがとうございます。機能が実装できました!
Your authentication token has expired. Please try signing in again.
仕事が終わったらChatGPT君はプライベートに戻っていきました・・・
所感
ChatGPTを使えば、実際のコード実装の大部分を自動化することができます。ただし対話によって生成したもののリテイク方法を指示していくスキルが必要なので、お絵描きAIと同じように既にコードを書ける人にとっては非常に強力なツールとなることが予想されます。著作権に関する問題はありますが、今回の例のように十分なコンテキストを与えれば、既存のコードにはあまり依存しないコードが生成できますので、使い方次第かなという感じがします。
一方で出力される内容の信頼性はあまり高くありません。嘘っぽいところ(あまりに自然に嘘をつくので見抜くのも大変です)は別途Google検索で裏を取る必要があります。未経験者がプログラミングの教師として使ったり、ノーコードツールとして使う用途にはまだ向かないでしょう。
使っていて改善を希望するところは部分的な修正をお願いしたときにファイル全体を出力するため、途中で見切れてしまうことが多いところです。diff形式で修正するようになればさらに使いやすくなると思われます。
これは確実に「プログラミング」を変えるツールです。早く正式公開が待ち遠しいです。