OpenAI APIを活用したLMSプロトタイプの設計と実装

OpenAI APIを活用したLMSプロトタイプの設計と実装

はじめに

OpenAI APIの実践的な活用例を共有することを目的として、LMS(Learning Management System)をモチーフにしたプロトタイプを開発しました。本記事では、Assistant APIとChat Completion APIをどのように使い分け、実装したのかを具体的に解説していきます。

主な機能

  1. 自動問題生成機能
  2. インタラクティブな学習支援機能
  3. 弱点分析機能

それぞれ概要を説明していきます。

1. 自動問題生成機能

入力したトピックに関連する問題を自動で生成します。例えば「Ruby on Rails」というトピックを入力すると、それに関連する問題セットが自動的に作成されます。模範回答も同時に作成されます。

2. インタラクティブな学習支援機能

生成された問題に対して、AIとチャット形式で対話しながら理解を深めることができます。AIは学習者の回答に応じて適切な質問や解説を提供し、理解度を確認します。

ユーザーの回答が「模範回答(事前にLLMで作成しておいたもの)に近しい」あるいは「模範回答ではなくとも一般論として正しい」状態になったらAIから合格である旨の回答がきます。

3. 弱点分析機能

AIとの対話履歴を分析し、学習者の理解が不十分な箇所を自動的に特定・整理します。これにより、効率的な復習が可能になります。(動画右下の「弱点ノート」の部分の機能です)

システム構成

image

プロトタイプということでシンプルな構成を心がけ、フロントエンドとバックエンドは分けずにRuby on Railsのみを使って開発しました。

Hotwireの採用により、Reactなどを利用せずとも以下のような動的な機能を実現しています:

  • AIからのレスポンスのストリーミング表示
  • チャット送信中のフォームの自動無効化/有効化

機能詳細と実装例

1. 問題自動生成機能

処理フロー

この機能にはChat Completion APIを採用しました。理由は以下の通りです:

  1. 単発のレスポンスに最適
    • この機能は1回のリクエストで完結する処理
    • 継続的な対話を必要としない
    • Assistant APIは会話の継続を前提とした設計のため、やや過剰
  2. シンプルな実装
    • Assistant APIと比較して実装が容易
    • スレッド管理などの複雑な処理が不要
  3. 履歴管理の観点
    • Chat Completion APIは過去の対話履歴を必要に応じて送信
    • この機能では履歴管理が不要なため、Chat Completion APIが適切

プロンプトは愚直にこのように書いてます。

{ role: "user", content: "「#{title}」という分野に関する学習コンテンツを日本語で作成してください。chapterは3種類くらいでそのchapterの中にtopicを5種類くらい作りたいです。また、questionには質問内容を、model_answerには模範回答を設定するようにしてください。" }

また、問題データをDBに安全に保存するため、Function Callingを採用しました:

tools: [
          {
            type: "function",
            function: {
              name: "generate_e_learning_contents",
              description: "学習コンテンツを作成する",
              parameters: {
                type: "object",
                properties: {
                  learning_contents: {
                    type: "array",
                    items: {
                      type: "object",
                      properties: {
                        chapter: { type: "string" },
                        topic: { type: "string" },
                        question: { type: "string" },
                        model_answer: { type: "string" }
                      },
                      required: ["chapter", "topic", "question", "model_answer"]
                    }
                  }
                },
                required: ["learning_contents"]
              }
            }
          }
        ],
        # Optional, defaults to "auto"
        # Can also put "none" or specific functions, see docs
        tool_choice: "required"

実際のレスポンスはこんな形になります。

{
	"learning_contents": [
		{
			"chapter": "Ruby on Railsのテスト",
			"topic": "テスト駆動開発(TDD)とは",
			"question": "テスト駆動開発(TDD)の基本概念を説明してください。",
			"model_answer": "テスト駆動開発(TDD)は、まずテストを書くことから始め、その後でテストをパスするコードを実装する開発手法です。このアプローチにより、バグを減らし、リファクタリングを容易にします。"
		},
		{
			"chapter": "Ruby on Railsのテスト",
			"topic": "RSpecの基本的な使い方",
			"question": "RSpecを使ったテストの基本的な流れを説明してください。",
			"model_answer": "RSpecでは、まずテストの対象となるクラスやメソッドを定義し、'describe'ブロックでそのグループを作成します。次に、期待する結果を'it'ブロックで記述します。テストは'yarn rspec'や'rails test'で実行します。"
		},
		{
			"chapter": "Ruby on Railsのテスト",
			"topic": "フィーチャスペックとは",
			"question": "フィーチャスペックの目的を説明してください。",
			"model_answer": "フィーチャスペックは、アプリケーションの機能全体をテストするためのもので、ユーザーがアプリを使った際の動作をシミュレーションします。Capybaraなどのツールを用いてブラウザ操作を自動化します。"
		},
		{
			"chapter": "Ruby on Railsのテスト",
			"topic": "マイグレーションに対するテスト",
			"question": "データベースのマイグレーションに対するテストはどのように行いますか?",
			"model_answer": "マイグレーションに対するテストは、'rails db:migrate' コマンドを使用してマイグレーションを実行し、期待されるスキーマが得られているかを確認することで行います。"
		},
		{
			"chapter": "Ruby on Railsのテスト",
			"topic": "テストのカバレッジ",
			"question": "テストのカバレッジについて説明してください。",
			"model_answer": "テストのカバレッジは、テストがアプリケーションのコードベースにどれだけの範囲をカバーしているかを示す指標です。高いカバレッジは、コードにバグが少ないことを示唆しますが、カバレッジが高いからといって、すべてのバグが見つかる訳ではありません。"
		}
	]
}

これを使ってDBに登録します。

Function Callingを使用する利点:

  • 型安全性の確保(JSONスキーマによる型定義)
  • パース処理の簡略化
  • レスポンスフォーマットの統一

実装上の工夫:

  • tool_choice: "required"を指定し、必ずFunction Callingを使用するよう強制し、これにより、テキスト形式での予期しない応答を防止しています。 

2. AIチャット機能

処理フロー

この機能にはAssistant APIを採用しました。理由は以下の通りです:

  • 複数回のやり取りを前提とした設計
  • スレッド管理が組み込まれており、対話の文脈管理が容易
  • Chat Completion APIと比較して、履歴管理の実装負荷が低い

Assistant APIについてはChat Completion APIに比べて少し考え方が複雑であるため本章の後半で同APIの基本概念を説明します。

メッセージを送ってAIからのレスポンスを画面に反映させるまでのロジックは以下の通りです。

  1. ユーザーがテキストを入力してサブミットする
  2. サーバはワーカにジョブを登録する
  3. サーバはひとまず画面にレスポンスを返しローディングを表示する
  4. 並行してワーカはOpenAIのアシスタントAPIを叩く
    1. messageをcreateする
    2. runをcreateする
  5. runをcreateする時にレスポンスをstreamで受信するオプションを指定しておき、受信するたびに画面にブロードキャストする
  6. 処理が完了したことを検知してDBの更新(処理中ステータスの更新など)をし改めて画面にブロードキャストする

実装例です。

client.runs.create(thread_id:,
                   parameters: {
                     assistant_id: assistant.assistant_identifier,
                     stream: proc do |chunk, _bytesize|
                       case chunk["object"]
                       when "thread.message.delta"
                         new_content = assistant_message.content + chunk.dig(
                           "delta", "content", 0, "text", "value"
                         )
                         assistant_message.assign_attributes(content: new_content)
                         assistant_message.broadcast_chunk_received
                       when "thread.run"
                         run_id = chunk["id"]
                         user_thread.update!(latest_run_identifier: run_id)

                         status = chunk["status"]
                          if status == "in_progress"
                            assistant_message.update!(status: :processing)
                          end
                       when "thread.message"
                         status = chunk["status"]
                         if status == "completed"
                           user_thread.update!(status: :completed)
                           id = chunk["id"]
                           content = chunk.dig(
                             "content", 0, "text", "value"
                           )
                           assistant_message.update!(content: content, message_identifier: id, status: :completed)
                           if content.include?("合格です")
                             user_thread_progress = user_thread.user_thread_progress
                             user_thread_progress.update!(status: :completed)
                             user_topic_progress = UserTopicProgress.find_by!(user_id: user_thread.user_id, topic_id: user_thread.topic_id)
                             user_topic_progress.update!(status: :completed)
                           end
                           assistant_message.reload.broadcast_chunk_received
                         end
                       end
                     end
                   })

Assistant APIの基本概念

Assistant APIを理解する上で重要な4つの概念(Assistant、Thread、Message、Run)について説明します。

image

(引用: https://platform.openai.com/docs/assistants/overview

  • Assistant(アシスタント)
  • Thread(スレッド)
  • Message(メッセージ)
  • Run(実行)

3. 弱点分析機能

処理フロー

この機能にはChat Completion APIを採用しました。理由は問題自動生成機能の時と同様に以下の通りです:

  1. 単発のレスポンスに最適
  2. シンプルな実装