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(アシスタント)
    • AIアシスタントの設定を定義するオブジェクト
    • 特定の役割や振る舞いを指示するプロンプトを保持
    • 一度作成したAssistantは複数のThreadで再利用可能
  • Thread(スレッド)
    • 一連の会話を管理するコンテナ
    • Messages(メッセージ)とRuns(実行)を保持
    • ユーザーごとに別々のThreadを作成することで、会話の文脈を分離
  • Message(メッセージ)
    • ユーザーとアシスタント間でやり取りされる個々のメッセージ
    • Threadに紐づいて時系列で保存
    • コンテンツの種類(テキスト、画像など)を指定可能
  • Run(実行)
    • アシスタントの応答生成プロセスを管理
    • メッセージを送信した後、明示的にRunを作成する必要がある
    • 実行状態(pending, completed, failed等)の管理が可能
    • ストリーミング応答の受信にも対応

3. 弱点分析機能

処理フロー

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

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

プロンプトも愚直に書いています。

{ role: "user", content: "「#{topic.chapter.course.title}」「#{topic.chapter.title}」「#{topic.title}」というコンテキストにおいて、上記のやりとりからユーザーの理解度があいまいな点を整理して列挙してください。" }

また、ここもFunction Callingを使うことで安全にデータをDBに登録できるようにしています。

{
  type: "function",
  function: {
    name: "generate_learning_note",
    description: "Generate a learning note based on the user's weak areas.",
    parameters: {
      type: "object",
      properties: {
        learning_notes: {
          type: "array",
          items: {
            type: "object",
            properties: {
              topic: { type: "string" },
              model_answer: { type: "string" }
            },
            required: ["topic", "model_answer"]
          }
        }
      },
      required: ["learning_notes"]
    }
  }
}

まとめ

ざっくりではありますが今回はLMSを題材としてOpenAIのAPIを活用する方法をご紹介しました。

このPoCをさらに発展させてより実用的なプロダクトとするには以下のような追加機能が考えられるでしょう。

  1. 既存教材との連携
    • PDFや社内マニュアルをAIに読み込ませることで、実務に直結した問題を作成できる。
    • 組織特有の知識や背景を反映した、より現場感のある問題を作成できる。
  2. 問題生成プロセスの改善
    • AIが生成した問題を人間が細かく調整できる。
  3. 学習管理機能の充実
    • 学習者の進捗状況や理解度をひと目で確認できる。
    • 学習履歴をまとめて管理し、必要に応じて的確なサポートが行える。

OpenAIのAPIを活用することで、これまでの学習体験とは違った柔軟でパーソナライズされた学びの場を提供できるはずです。

一方で、現時点のAIが人間と同じレベルの完璧なアウトプットを出すのはまだ難しい部分もあります。 しかしながら「素早く70点のアウトプットを作る力」についてはAIは本当に優れていると感じます。 たとえば今回のプロトタイプでは

  • 数秒で基本的な問題を作成
  • リアルタイムで学習者の理解度を評価
  • 対話履歴をもとに弱点を自動的に分析

といった機能が実現できました。これらは人が一から行う場合と比べて、圧倒的な時間短縮につながります。

数年後はわかりませんが、少なくとも現時点においてAIと付き合っていく上で大切なのは、AIを「完璧な答えを出すためのツール」として捉えるのではなく、「素早く下書きを作ってくれる頼れるアシスタント」として活用することだと思います。

そう考えると「AIが生成した内容を人が自然に確認・微調整できる」というプロセスをいかにUXに反映させるか(AIファーストな設計)が今後のサービス作りに重要になってくるなと感じました。