一問一答アプリで問題を絞る機能・同じ問題が出題されない機能の実装〜ChatGPTと一緒に一問一答アプリを開発した話①〜

eyecatch-wchatgpt-1

「一問一答アプリ × ChatGPT」

先日、ChatGPTを活用して一問一答アプリケーション「情報1 一問一答トレーニングGYM」を開発したことをお伝えしました。

▶︎こちらの記事で紹介しています。

iqatlogo2【ChatGPTと開発】高校「情報1」一問一答アプリ

私が開発したこのアプリは、ChatGPTの力を借りながら、実装していきました。

この記事では、一問一答アプリ × ChatGPTの開発の舞台裏や、実際の活用方法について詳しく解説します。

今回は

  • 設定(出題範囲や難易度など)で出題される問題を絞ることができるができる
  • アプリケーションを閉じるまで、一度解いた問題は二度出題されない

という機能を実装するにあたって、どのように役立ったのか、その様子を紹介します!

ぜひこのシリーズの記事をチェックして、一問一答アプリの新たな魅力とChatGPTとの相乗効果を体感してください!

設定(出題範囲や難易度など)で出題される問題を絞ることができる

img-4372v2

アプリケーションを新規作成し、DBを作成(このあたりの操作についてはコードなどを割愛)。

ここから要件を満たす実装をしていきます。

まずは「設定(出題範囲や難易度など)で出題される問題を絞ることができる」です。

モデルが管理しているレコードを取得するにはさまざまなコードの記述方法がありますが

コトゼニ
コトゼニ

chatGPTに相談してみました

それがこちら。

chatGPTとの対話履歴

画像では提案してくれたコードが途切れているので、再度以下に記すと、

selected_genre_ids = params[:genre][:genre_ids].reject(&:empty?)
selected_genres = Genre.where(id: selected_genre_ids)
questions = Question.joins(:genres).where(genres: {id: selected_genre_ids}).order("RANDOM()").first

となりました。

コトゼニ
コトゼニ

なるほど。これだけのコードを瞬時に書いてくれるのは恐ろしいですね

使用しているSQLが私の場合はMySQLだったので「.order(“RANDOM()”)」の部分を修正する必要はあったのですが、土台となるコードをここまで的確に書いてくれると、開発がそりゃあもう捗る捗る。

コトゼニ
コトゼニ

実装したコードは後述でまとめます

実装:アプリケーションを閉じるまで、一度解いた問題は二度出題されない

programming

続いて、一度出題された問題が何度も出題されないようにするという点です。

この点もセッションをうまく使えばなんとかなるかなぁと考えていました。

そして例の如く、chatGPTに土台となるコードを依頼してみました。

chatGPTとの対話履歴2

提案してくれたコードが途切れているので依頼したプロンプトとともに以下に再表示します。

一問一答アプリケーションは以下のような画面遷移・要件を満たすように設計したいと思います。コントローラーのコードを作ってください

▼画面遷移:①トップページ(分野選択)②出題(解答)③正誤判定

▼要件:

●②において、①で選択した分野と一致する問題(インスタンス)を全て取得するが、表示するのはその中の一つのみ

●③において「次の問題へ」ボタンを押すと、すでに出題された問題以外かつ①で選択した分野と一致する問題から次に出題する問題を一つ選定する。

class QuestionsController < ApplicationController
  before_action :set_genres, only: [:index, :create]

  def index
    @question = Question.new
  end

  def create
    genre_ids = params[:question][:genre_ids].reject(&:empty?)
    @question = Question.where(genre_id: genre_ids).order("RANDOM()").first

    if @question.present?
      session[:asked_question_ids] ||= []
      session[:asked_question_ids] << @question.id

      redirect_to answer_question_path(@question)
    else
      redirect_to questions_path, alert: "出題できる問題がありません。"
    end
  end

  def answer
    @question = Question.find(params[:id])
  end

  def result
    @question = Question.find(params[:id])
    session[:asked_question_ids].delete(@question.id)

    if @question.answer == params[:answer]
      flash[:notice] = "正解です!"
    else
      flash[:alert] = "不正解です。"
    end

    genre_ids = params[:genre_ids].reject(&:empty?)
    @question = Question.where(genre_id: genre_ids).where.not(id: session[:asked_question_ids]).order("RANDOM()").first

    if @question.present?
      session[:asked_question_ids] << @question.id

      redirect_to answer_question_path(@question)
    else
      redirect_to questions_path, alert: "出題できる問題がありません。"
    end
  end

  private

  def set_genres
    @genres = Genre.all
  end
end

やはり以下のコードでセッションを利用していることがわかります。

  if @question.present?
      session[:asked_question_ids] ||= []
      session[:asked_question_ids] << @question.id

      redirect_to answer_question_path(@question)
  else

具体的には

  session[:asked_question_ids] ||= []
  session[:asked_question_ids] << @question.id

1行目の「session[:asked_question_ids] ||= []」でセッション変数がなければ取得し、2行目「session[:asked_question_ids] << @question.id」でセッションにモデルから取得したインスタンス(question)のidを格納しています。

これにより

    @question = Question.where(genre_id: genre_ids).where.not(id: session[:asked_question_ids]).order("RANDOM()").first

において、「where.not(id: session[:asked_question_ids])」の部分で、「セッションに格納されているid以外で検索する」という意味を持たせてSQL文を実行することができるようになりました。

ちなみにchatGPTが提案してくれたコードにおいて

    session[:asked_question_ids].delete(@question.id)

とあり、セッションに格納したidを削除したら同じ問題が再度出題される可能性があることに気付いたため修正を加えました。

これらをもとに実装したのが以下のコードです

class QuestionsController < ApplicationController

  def show    
    # 定義しているだけ
  end

  def choose
    reset_session
    @genres = Genre.all
  end

  def ask
    # まずはセッション変数を取得する
    # next_questionメソッドで使うため
    # session[:asked_question_ids] ||= [] でセッションが未定義なら新しく作るという意味
    session[:asked_question_ids] ||= []
    session[:selected_genre_ids] ||= []

    # 出題範囲が選択されているか否かで条件分岐
    # 出題範囲が選択されていない場合
    if params[:genre].blank?
      # Genreモデルからカラム「id」の値だけを取得し、セッションに格納する(これで全ての分野のidを取得できる)
      genre_ids = Genre.pluck(:id)
      session[:selected_genre_ids] << genre_ids
    else
      # 選択された分野をparamsから受け取り、セッションに格納する
      genre_ids = params[:genre][:genre_ids].reject(&:empty?)
      if !session[:selected_genre_ids].include?(genre_ids)
        session[:selected_genre_ids] << genre_ids
      end
    end

    # genre_idsに入っている分野idと一致するインスタンスを1つ取得する
    @question = Question.where(genre_id: genre_ids).order("RAND()").first

    if @question.present?
      # session[:asked_question_ids]に取得したインスタンスの問題idを格納する。
      if !session[:asked_question_ids].include?(@question.id)
        session[:asked_question_ids] << @question.id
      end

      #取得した問題インスタンスに付随する選択肢を取得する。
      @choices = @question.choices.includes(:question) #questionとアソシエーション関係なので()の中にはカラム名ではなく関係のあるモデル名にする「_id」は不要

    else
      redirect_to root_path, alert: "出題できる問題がありません。"
    end
    
  end

  def result
    #paramsで送付されたquestion_idを利用して当該インスタンスを取得する。
    @question = Question.find(params[:question_id])
    @choices = @question.choices.includes(:question) 
    @answer = Answer.find(@question.id)
    @chosen_choice = Choice.find(params[:choice_id])
  end

  def next_question
    #当メソッドを呼び出す際には最低1回はask~resultメソッドを実行している。そのため、セッションを参考にして分野を指定しながら出題済みの問題を避けて次の問題(インスタンス)を取得できる。
    @question = Question.where(genre_id: session[:selected_genre_ids]).where.not(id: session[:asked_question_ids]).order("RAND()").first

      # session[:asked_question_ids]に取得したインスタンスの問題idを追加格納する。
      if @question.present?
        session[:asked_question_ids] << @question.id
        @choices = @question.choices.includes(:question) #questionとアソシエーション関係なので()の中にはカラム名ではなく関係のあるモデル名にする「_id」は不要
        # 呼び出すビューファイルはaskアクションのビューファイル
        render :ask
    else
        redirect_to root_path, alert: "出題できる問題がありません。"
    end 
  end

end

これで実装した挙動がこちら。

実装した挙動(ページ遷移あり正誤判定)

とりあえずこのコードで実装できました!

まとめ

今回の開発作業で

  • 設定(出題範囲や難易度など)で出題される問題を絞ることができるができる
  • アプリケーションを閉じるまで、一度解いた問題は二度出題されない

の2つを実装することができました。

ChatGPTを利用すると開発がグンと楽になりますね!ChatGPTの可能性を体験してみてはいかがでしょうか?

最後まで読んでいただきありがとうございました。

この記事が少しでも参考になれば幸いです。

それではまたここで会いましょう!

ブログランキング・にほんブログ村へにほんブログ村

ブログ村に参加中です。上のバナーをクリックいただくだけで当ブログにポイントが入ります。いつも応援クリックありがとうございます。

Twitter

Twitter やってます。

新着記事のお知らせをしていますのでぜひフォローしてください!

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA