こんにちは、千葉(@yoshi_chibaaa)です。
今回はOpenAIのAPIを用いて、Fine Tuningモデルを自由に選択できる自動応答機能を作ってみたので紹介したいと思います。Fine Tuningモデルはトークン数によって費用が発生しますが、アカウント登録から3ヶ月は18ドル分のクレジットが付与されるため、その期間は無料で利用することができます。
Fine Tuningモデルとは
Fine-tuningモデルは、事前に大規模なデータセットで訓練された一般的な言語モデルを、特定のタスクやドメインに合わせて追加の訓練を行い、特定のタスクに特化したモデルを作成する手法です。
Fine-tuningモデルは、一般的な言語モデル(プリトレーニングモデル)が大規模なテキストデータセットで学習された後に、タスク固有のデータセットを使用して再訓練されます。この再訓練により、モデルは特定のタスクやドメインに関する知識を獲得し、そのタスクに特化した予測や解析を行うことができるようになります。
Fine-tuningモデルは、特定のタスクに対して高い性能を発揮し、タスク固有のデータや特徴に適合したモデルを構築することができます。例えば、質問応答、感情分析、文章生成などのタスクに対して、Fine-tuningモデルは高い精度や性能を発揮することができます。また、Fine-tuningモデルは、プリトレーニングモデルが持つ幅広い言語理解の能力を引き継ぎながら、特定のタスクに関するドメイン知識を獲得することができるため、効率的なモデル構築手法として広く利用されています。

利用料金

モデル | 学習 | 使用 |
Ada | $0.0004 / 1K tokens | $0.0016 / 1K tokens |
Babbage | $0.0006 / 1K tokens | $0.0024 / 1K tokens |
Curie | $0.0030 / 1K tokens | $0.0120 / 1K tokens |
Davinci | $0.0300 / 1K tokens | $0.1200 / 1K tokens |
実装手順
今回は以下のYouTube動画を元にアプリケーションを構築していきます。
新規アプリケーションの作成
以下のコマンドで新規アプリケーションを立ち上げます。
rails new
時に-j esbuild
オプションを指定し、JavaScriptのビルドツールをesbuild
に設定しています。また、-c bootstrap
オプションでBootstrapも使用できるように設定しています。
% rails new chatgpt-demo -d mysql -j esbuild -c bootstrap
% cd chatgpt-demo
% rails db:create
- Ruby: 3.2.2
- Rails: 7.0.7
- DB: MySQL
pagesコントローラの作成
% rails g controller pages home
faraday gemの導入
HTTP通信ライブラリであるGem faradayをインストールします。
% bundle add faraday

foreman Gemの追加
開発環境セットアップ用のGem foremanをインストールします。
# Gemfile
gem "foreman", github: "ddollar/foreman"
% bundle install
gem "foreman", github: "ddollar/foreman"
は、開発環境においてプロセス管理を行うためのGemです。複数のサーバーやプロセスを一度に起動・管理し、開発環境のセットアップやデバッグを簡便にするために使用されます。
例えば、foreman Gemを使用することでWebサーバーを起動すると同時に、バックグラウンドで動作するプロセスも一緒に起動できます。これによりアプリケーションのセットアップが簡単になります。
参考: Forman
AiRequestJobの作成
バックグラウンド処理を行うAiRequestJob
というジョブを作成します。
% rails g job AiRequestJob
動作確認1
ここまでで一度サーバを起動し、Railsの初期画面が表示されるか確認します。
% bin/dev

routesの修正
ルーティングを以下のように修正します。
Rails.application.routes.draw do
post 'ai_request', to: 'pages#ai_request'
root 'pages#home'
end
ビューの作成
ai
ディレクトリを作成し、その中に2つの部分テンプレートを作成します。
% mkdir app/views/ai
% touch app/views/ai/_output.html.erb
% touch app/views/ai/_request_form.html.erb

uuidの生成
PagesController
のhome
メソッド内でランダムなUUIDを生成します。
class PagesController < ApplicationController
require 'securerandom'
def home
@uuid = SecureRandom.uuid
end
end
require 'securerandom'
でsecurerandom
という標準ライブラリを読み込んでいます。SecureRandom
モジュールは、ランダムな文字列や数値を生成するために使用されるセキュアな乱数生成機能を提供します。
home.html.erbの修正
トップページであるhome.html.erb
を以下のように編集します。
このページには、質問の入力欄(ai/request_form
)とその出力結果を表示する欄(ai/output
)を部分テンプレートを用いて実装しています。
<div class="container mt-5">
<h1>Pages#home</h1>
<p>Find me in app/views/pages/home.html.erb</p>
<%= render partial: "ai/request_form", locals: { uuid: @uuid } %>
<%= turbo_stream_from "channel_#{@uuid}" %>
<div id="ai_output">
<%= render partial: "ai/output", locals: { generated_idea: "" } %>
</div>
</div>
<%= render partial: "ai/request_form", locals: { uuid: @uuid } %>
ai/request_form
という部分テンプレートをレンダリングし、その際にuuid
というローカル変数が渡しています。この変数はPagesController
に定義した@uuid
インスタンス変数の値を参照しています。<%= turbo_stream_from "channel_#{@uuid}" %>
Turbo Streamsを使用してリアルタイムな更新を可能にするためのコードです。指定されたchannel_#{@uuid}
というチャンネルからのストリームを受信し、自動的に更新します。これにより、ページの一部を非同期で更新することができます。<%= render partial: "ai/output", locals: { generated_idea: "" } %>
ai/output
という部分テンプレートをレンダリングし、その際にgenerated_idea
という空のローカル変数を渡しています。
_request_form.html.erbの修正
質問入力フォームの部分テンプレートになります。
使用するモデルもここで選択できるようセレクトボックスを実装しています。
<%= form_for :ai_request, url: ai_request_path, method: :post, remote: true do |f| %>
<%= f.hidden_field :uuid, value: @uuid %>
<div class="form-group">
<%= f.text_area :prompt, class: "form-control" %>
</div>
<%= f.select :ai_model, options_for_select([
[ "Ada $0.0016 / 1K tokens", "ada" ],
[ "Babbage $0.0024 / 1K tokens", "babbage" ],
[ "Curie $0.0120 / 1K tokens", "curie" ],
[ "Davinci $0.1200 / 1K tokens", "davinci" ],
], "davinci"), {}, class:"form-control" %>
<%= f.submit "Send", class: "btn btn-primary" %>
<% end %>
<%= form_for :ai_request, url: ai_request_path, method: :post, remote: true do |f| %>
ai_request_path
というパスに対してHTTP POSTリクエストを行っています。remote: true
はAjaxを使用して非同期通信を行うことを示しています。<%= f.hidden_field :uuid, value: @uuid %>
:uuid
という属性名に対して、インスタンス変数@uuid
の値をhidden_field
を用いて送信しています。<%= f.select :ai_model, options_for_select([...], "davinci"), {}, class:"form-control" %>
:ai_model
という属性名に対して、選択肢のリストを配列として渡しています。初期選択値として"davinci"
を指定しています。
_output.html.erbの修正
出力結果の部分テンプレートになります。
generated_idea
値の有無で、表示・非表示を分けるため以下のように修正します。
<div class="card">
<% if generated_idea %>
<%= content_tag(:div, generated_idea) %>
<% end %>
</div>
APIキーの設定
OpenAIのAPIキーを設定します。
APIキーを未取得の場合は、こちらから新規登録と取得を行ってください。
% EDITOR=vi rails credentials:edit --environment development
openai:
api_key: <OpenAI APIキー>

pagesコントローラの修正
先ほど設定したOpenAIのAPIキーと入力された質問内容を元にAIRequestJobを実行するため、以下のようにPagesControllerを編集します。
class PagesController < ApplicationController
require 'securerandom'
before_action :set_api_key, only: [:ai_request]
def home
@uuid = SecureRandom.uuid
end
def ai_request
AiRequestJob.perform_later(ai_request_params, @api_key)
end
private
def ai_request_params
params.require(:ai_request).permit(:prompt, :ai_model, :uuid)
end
def set_api_key
@api_key = Rails.application.credentials.dig(:openai, :api_key)
end
end

AiRequestJobの修正
AiRequestJobの中身を以下のように修正します。
ここでは与えられたパラメータに基づいてTurbo Streamsを使用して、特定のチャンネルにデータを送信し、ビューの一部を非同期で更新しています。
class AiRequestJob < ApplicationJob
queue_as :default
def perform(ai_request_params, api_key)
uuid = ai_request_params[:uuid]
generated_idea = ai_request_params[:prompt]
Turbo::StreamsChannel.broadcast_replace_to("channel_#{uuid}",
target: 'ai_output',
partial: 'ai/output',
locals: { generated_idea: }
)
end
end
Turbo::StreamsChannel.broadcast_replace_to("channel_#{uuid}"
Turbo Streamsを使用して、"channel_#{uuid}"
というチャンネルにデータを送信しています。uuid
はチャンネルの識別に使用しています。target: 'ai_output'
target
でデータを送信するターゲットを指定しています。partial: 'ai/output'
partial
では更新される部分のビューファイルを指定しています。locals: { generated_idea: }
locals
では部分ビュー内で使用されるローカル変数を指定します。
動作確認2
ここまでで入力内容が正常に反映されるかチェックしてみます。
AiRequestJobの修正
現状、新たに投稿内容を更新しても更新内容は反映されません。
そこで編集内容が反映されるよう、以下のようにbroadcast_replace_to
をbroadcast_update_to
に置き換えます。
class AiRequestJob < ApplicationJob
queue_as :default
def perform(ai_request_params, api_key)
uuid = ai_request_params[:uuid]
generated_idea = ai_request_params[:prompt]
Turbo::StreamsChannel.broadcast_update_to("channel_#{uuid}",
target: 'ai_output',
partial: 'ai/output',
locals: { generated_idea: }
)
end
end
動作確認3
入力内容が更新されることを確認します。
AiRequestJobの修正
最後に以下のようにFaraday Gemを使用して、OpenAI APIのリクエストを行います。
class AiRequestJob < ApplicationJob
queue_as :default
def perform(ai_request_params, api_key)
connection = Faraday.new(url: 'https://api.openai.com')
response = connection.post do |req|
req.url "/v1/engines/#{ai_request_params[:ai_model]}/completions"
req.headers['Content-Type'] = 'application/json'
req.headers['Authorization'] = "Bearer #{api_key}"
req.body = {
prompt: ai_request_params[:prompt],
max_tokens: 250,
temperature: 0.5,
n: 1
}.to_json
end
json_response = JSON.parse(response.body)
generated_idea = json_response['choices'][0]['text']
uuid = ai_request_params[:uuid]
Turbo::StreamsChannel.broadcast_update_to("channel_#{uuid}",
target: 'ai_output',
partial: 'ai/output',
locals: { generated_idea: })
end
end
ここではOpenAI APIへのリクエストを行い、生成されたテキストを取得し、Turbo Streamsを使用してクライアントにデータを非同期に送信しています。
connection = Faraday.new(url: 'https://api.openai.com')
Faraday Gemを使用してOpenAi APIへの接続を確立しています。response = connection.post do |req| ... end
Faradayを使用してPOSTリクエストを送信しています。APIのエンドポイント、ヘッダー、ボディなどをブロック内で設定しています。json_response = JSON.parse(response.body)
APIから受け取ったレスポンスボディをJSON形式からパースして、json_response変数に格納しています。generated_idea = json_response['choices'][0]['text']
json_responseから生成されたテキストを取得しています。APIの応答データの中にあるchoices配列から、最初の要素のtextフィールドを抽出しています。

動作確認4(画像のみ)
今回の質問内容でベンチマークを計測すると、約8秒かかってしまうため、実際に開発で用いる際は何らかの対策が必要になりそうです。
class AiRequestJob < ApplicationJob
queue_as :default
def perform(ai_request_params, api_key)
result = Benchmark.measure do
connection = Faraday.new(url: 'https://api.openai.com')
response = connection.post do |req|
req.url "/v1/engines/#{ai_request_params[:ai_model]}/completions"
req.headers['Content-Type'] = 'application/json'
req.headers['Authorization'] = "Bearer #{api_key}"
req.body = {
prompt: ai_request_params[:prompt],
max_tokens: 250,
temperature: 0.5,
n: 1
}.to_json
end
json_response = JSON.parse(response.body)
generated_idea = json_response['choices'][0]['text']
uuid = ai_request_params[:uuid]
Turbo::StreamsChannel.broadcast_update_to("channel_#{uuid}",
target: 'ai_output',
partial: 'ai/output',
locals: { generated_idea: })
end
result.real # ベンチマーク計測
end
end
まとめ
- Fine-tuningモデルは、事前に大規模なデータセットで訓練された一般的な言語モデルを、特定のタスクやドメインに合わせて追加の訓練を行い、特定のタスクに特化したモデルを作成する手法のこと
- 利用料金はモデルによって変わる
- 回答結果を表示するには約8秒かかる(今回の場合)
参考
今回はOpenAIのAPIを用いて、Fine Tuningモデルを自由に選択できる自動応答機能を作ってみたので紹介しました。パラメータを色々いじって、どのような回答が得られるかを見てみても面白そうですね。