21
19

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

記事投稿キャンペーン 「2024年!初アウトプットをしよう」

OpenAI APIであらゆるジャンルのクイズを無限に作る(Function Calling、JSON Mode)

Posted at

OpenAIのChat Completion APIを使って、あらゆるジャンルのクイズを無限に作るクイズAPI1を作ってみました。利用するためには、OpenAIのAPIトークンが必要です。

PythonのStreamlitで作ったデモアプリの中で使っています。

無限クイズの画面

クイズのデータ形式

ジャンルを指定すると、Chat Completion APIを使って、次のようなJSON形式の4択クイズデータを生成します。次の例はジャンルにPythonを指定した結果です。

{
  "questions": [
    {
      "question": "Pythonの特徴でないものはどれ?",
      "options": ["動的な型付けを採用している", "クラスベースのオブジェクト指向プログラミング言語である", "セミコロンが必須である", "インデントによるブロック表現が特徴的"],
      "answerIndex": 2
    },
    {
      "question": "Pythonで使用されるイテレーターを作成するための関数はどれ?",
      "options": ["enumerate()", "zip()", "range()", "iter()"],
      "answerIndex": 3
    },
    {
      "question": "Pythonの有名なウェブフレームワークはどれ?",
      "options": ["Django", "Ruby on Rails", "Express", "Flask"],
      "answerIndex": 0
    }
  ]
}

選択肢のうち、どれが正解かをanswerIndexで指定させるのがポイントです。こうすることで、選択肢の中に必ず正解があることを保証できます2

Chat Completion APIにJSON形式のデータを作らせるには、JSON ModeかFunction Callingを使います。それぞれ説明していきます。

なお、プロンプトインジェクションへの対策は施していません。あくまで参考としてご覧ください。

JSON Modeを使うクイズAPI

JSON Modeは、gpt-3.5-turbo-1106gpt-4-1106-previewといった新しいモデルで使える機能です。この後説明するFunction Callingよりも簡単に、JSON形式のデータを生成できます。

def create_quiz_by_json_mode(genre, num_questions, num_options):
    client = OpenAI(
        api_key=os.getenv("OPENAI_API_KEY"),
    )
    response = client.chat.completions.create(
        model=os.getenv("OPENAI_MODEL"),
        messages=[
            {
                "role": "system",
                "content": 'あなたは優秀なアシスタントです。あなたはあらゆるジャンルのクイズを作ることが出来ます。日本語で回答してください。lang:ja。{"questions: [{"question": "問題", "options":["回答1", "回答2", "回答3", "回答4"], "answerIndex": 0},...]}のJSON形式で返却してください。',
            },
            {
                "role": "user",
                "content": f"ジャンル「{genre}」に関するクイズを、{num_questions}問作ってください。{num_options}択クイズでお願いします.",
            },
        ],
        response_format={"type": "json_object"},
    )
    json_obj = json.loads(response.choices[0].message.content)
    print(json_obj)
    return json_obj["questions"]

JSON Modeを使うポイントは4つです。

1. 新しいモデルを使う

gpt-3.5-turbo-1106gpt-4-1106-previewを使います。

2. messages(プロンプト)に返却して欲しいデータ形式を指定する

今回は、Systemプロンプトに次のようなデータ形式を指定しています。

{"questions: [{"question": "問題", "options":["回答1", "回答2", "回答3", "回答4"], "answerIndex": 0},...]} のJSON形式で返却してください。

ただし、データ形式の指定が甘いと、返却されるデータの構造がぶれることがあります。

例えば、プロンプトで期待するデータ形式を{"question": "問題", "options":["回答1", "回答2", "回答3", "回答4"], "answerIndex": 0}とだけ指定していると、

{
  "questions":[
    {
      "question": "問題",
      "options":["回答1", "回答2", "回答3", "回答4"],
      "answerIndex": 0
    },
    ...

と返却される場合だけでなく、

{
  "quiz1": {
    "question": "問題",
    "options":["回答1", "回答2", "回答3", "回答4"],
    "answerIndex": 0
  },
  "quiz2": {
    ...

と返却される場合もありました。

3. messages(プロンプト)にjsonという文字列を入れる

もし、jsonという文字列を入れ忘れると、400エラーレスポンスが返却されます。

openai.BadRequestError: Error code: 400 - {'error': {'message': "'messages' must contain the word 'json' in some form, to use 'response_format' of type 'json_object'.", 'type': 'invalid_request_error', 'param': 'messages', 'code': None}}

4. response_formatを指定する

呼び出すときにresponse_format={"type": "json_object"}を指定します。

Function Callingを使うクイズAPI

Function Callingを使って、JSON形式のデータを返却させることもできます。古いモデルで動かせることができるのですが、指定が少々面倒です。

def create_quiz_by_function_calling(genre, num_questions, num_options):
    client = OpenAI(
        api_key=os.getenv("OPENAI_API_KEY"),
    )
    response = client.chat.completions.create(
        model=os.getenv("OPENAI_MODEL"),
        messages=[
            {
                "role": "system",
                "content": "あなたは優秀なアシスタントです。あなたはあらゆるジャンルのクイズを作ることが出来ます。日本語で回答してください。lang:ja。",
            },
            {
                "role": "user",
                "content": f"ジャンル「{genre}」に関するクイズを、{num_questions}問作ってください。{num_options}択クイズでお願いします.",
            },
        ],
        tools=[
            {
                "type": "function",
                "function": {
                    "name": "show_questions",
                    "description": "show given questions",
                    "parameters": {
                        "$schema": "http://json-schema.org/draft-07/schema#",
                        "type": "object",
                        "properties": {
                            "questions": {
                                "type": "array",
                                "items": {
                                    "type": "object",
                                    "properties": {
                                        "question": {"type": "string"},
                                        "options": {
                                            "type": "array",
                                            "items": {"type": "string"},
                                        },
                                        "answerIndex": {
                                            "type": "integer",
                                            "minimum": 0,
                                        },
                                    },
                                    "required": ["question", "options", "answerIndex"],
                                    "additionalProperties": False,
                                },
                            }
                        },
                        "required": ["questions"],
                        "additionalProperties": False,
                    },
                },
            }
        ],
        tool_choice={"type": "function", "function": {"name": "show_questions"}},
    )
    message = response.choices[0].message
    args = json.loads(message.tool_calls[0].function.arguments)
    print(args)
    return args["questions"]

Function Callingにクイズデータを作らせるために、show_questions関数を持つクイズ表示アプリが存在すると仮定します。show_questions関数にクイズデータ渡すと、それを表示してくれます。利用イメージは次の通りです。

Appは存在しないshow_questions関数を呼び出すことはしませんが、クイズデータ(questions)は取得できています。

Function Callingを使うポイントは2つです。

1. tool_choiceで呼び出す関数名を指定

今回はshow_questions関数を呼び出すと仮定しているので、次のようにしています。

tool_choice={"type": "function", "function": {"name": "create_questions"}}

2. toolsに呼び出す関数の引数をJSON Schemaで指定

今回はshow_questionsの関数の引数はquestionsです。このquestionsが先に提示したクイズデータになります。

引数questionsのデータ形式はJSON Schemaで定義します。ChatGPTにquestionsの例を提示すると、それをJSON Schemaに変換してくれます。

ChatGPTとのやりとり

You

次のJSONのJSON Schemaを教えて

{
  "questions": [
    {
      "question": "Pythonの特徴でないものはどれ?",
      "options": ["動的な型付けを採用している", "クラスベースのオブジェクト指向プログラミング言語である", "セミコロンが必須である", "インデントによるブロック表現が特徴的"],
      "answerIndex": 2
    },
    {
      "question": "Pythonで使用されるイテレーターを作成するための関数はどれ?",
      "options": ["enumerate()", "zip()", "range()", "iter()"],
      "answerIndex": 3
    },
    {
      "question": "Pythonの有名なウェブフレームワークはどれ?",
      "options": ["Django", "Ruby on Rails", "Express", "Flask"],
      "answerIndex": 0
    }
  ]
}

ChatGPT

提供されたJSONデータのためのJSON Schemaを作成する際、以下のような構造を考慮します:

  • questionsはオブジェクトの配列です。
  • 各オブジェクトにはquestion(文字列型)、options(文字列の配列)、そしてanswerIndex(整数型)が含まれます。

この情報を基に、対応するJSON Schemaは以下のようになります:

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "properties": {
    "questions": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "question": {
            "type": "string"
          },
          "options": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "answerIndex": {
            "type": "integer",
            "minimum": 0
          }
        },
        "required": ["question", "options", "answerIndex"],
        "additionalProperties": false
      }
    }
  },
  "required": ["questions"],
  "additionalProperties": false
}

このスキーマでは以下の点を確認しています:

  • 最上位レベルはオブジェクトで、questionsというプロパティを持ちます。
  • questionsはオブジェクトの配列で、各オブジェクトはquestion(文字列)、options(文字列の配列)、answerIndex(0以上の整数)を持つ必要があります。
  • requiredフィールドを使用して、これらのプロパティが各オブジェクトに必須であることを指定しています。
  • additionalProperties: falseを設定して、スキーマに定義されていない追加のプロパティがオブジェクトに含まれないようにしています。

このスキーマは、提供されたJSONデータの構造を適切に反映しています。

toolsに呼び出す関数を指定します。

  • 関数名"name": "show_questions"
  • 関数の説明"description": "show given questions"
  • 関数の引数properties(JSON Schemaで定義)
        tools=[
            {
                "type": "function",
                "function": {
                    "name": "show_questions",
                    "description": "show given questions",
                    "properties": { # ChatGPTに提示されたJSON Schema
                    ...

まとめ

OpenAIのChat Completion APIを使って、ジャンルに応じた無限のクイズを生成する方法を紹介しました。

使えるモデルに応じて、JSON ModeとFunction Callingを使い分けてもらえればと思います。ただし、プロンプトインジェクションへの対策が施されていない点と、生成されるクイズに嘘(ハルシネーション)がある点には注意が必要です。4択クイズであっても「該当なし」の選択肢を作ってあげるのが良さそうです。

クイズはエンターテイメントとして楽しいだけでなく、教育に応用すること可能です。例えば、自分が何かを学んだ後、力試しとしてクイズを解いてみれば、理解度をすぐに確認できます。他にも色々な応用がありそうです。

このAPIを使ってみて、ご意見や改善案があればぜひ教えてください!

  1. 英語だとOpen Trivia DB APIThe Trivia APIといったクイズAPIがあるんですが、日本語のものが見つかりませんでした...。

  2. 最初は、選択肢と正解をそれぞれ文字列でGPTに作らせていたのですが、選択肢の中に正解の文字列がない、という状況になったことがありました。

21
19
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
21
19

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?