ChatGPT_Structured Outputsの使い方(Javascript)


# ChatGPTのStructured Outputsについて

2024/08/06にJSONモードより正確性の高い、
Structured Outputsが追加されました。

この機能を用いれば、
こちらで指定したパラメータに完全一致した内容をレスポンスとして返してくれます。
内容に関しては今まで通り100%とはいきませんが、
keyや型については守ってくれるためその後の処理がしやすくなります。

https://openai.com/index/introducing-structured-outputs-in-the-api/

## 通常のChatGPTAPIの問い合わせを準備

ChatGPTへの問い合わせ方法はいくつかありますが、
今回はaxiosを使って下記の方法で問い合わせます。

```javascript
const axios = require("axios")

const SYSTEM_PROMPT =
  "あなたは優秀なAIです。status以外、日本語で答えてください。"
const USER_PROMPT =
  "5000円の会計を山本さんと田中さんの二人で分けると1人は何円支払いますか？ それぞれの人の名前と金額を教えてください。"

async function getChatGPTResponse(systemPrompt, userPrompt) {
  const response = await axios.post(
    "https://api.openai.com/v1/chat/completions",
    {
      model: "gpt-4o-2024-08-06",
      messages: [
        { role: "system", content: systemPrompt },
        { role: "user", content: userPrompt },
      ],
    },
    {
      headers: {
        Authorization: `Bearer YOUR_OPENAI_API_KEY`,
        "Content-Type": "application/json",
      },
    }
  )
  return response.data
}

async function main() {
  const responseData = await getChatGPTResponse(SYSTEM_PROMPT, USER_PROMPT)
  const message = responseData.choices[0].message.content

  console.log(message)
}

main()
```

ChatGPTからの返答
```
user % node index.js
山本さんと田中さんが5000円の会計を分ける場合、1人あたりの金額は2500円です。

- 山本さん: 2500円
- 田中さん: 2500円
```

## Structured Outputsの使い方

下記でRESPONSE_FORMATを定義してください。

```javascript
const RESPONSE_FORMAT = {
  type: "json_schema",
  json_schema: {
    name: "response",
    strict: true,
    schema: {
      type: "object",
      properties: {
        answer: {
          type: "integer",
          description: "最終的な回答を数値のみで出力",
        },
        additionalInfo: { type: "string", description: "追加情報" },
      },
      required: ["answer", "additionalInfo"],
      additionalProperties: false,
    },
  },
}
```

続いて、response_formatに先ほど定義した内容を追記してください。

```javascript
async function getChatGPTResponse(systemPrompt, userPrompt) {
  const response = await axios.post(
    "https://api.openai.com/v1/chat/completions",
    {
      model: "gpt-4o-2024-08-06",
      messages: [
        { role: "system", content: systemPrompt },
        { role: "user", content: userPrompt },
      ],
      response_format: RESPONSE_FORMAT, // --- 追記 ---
    },
    {
      headers: {
        Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
        "Content-Type": "application/json",
      },
    }
  )
  return response.data
}
```

あとはmain()でメッセージを確認できるようにします。

```javascript
async function main() {
  const responseData = await getChatGPTResponse(SYSTEM_PROMPT, USER_PROMPT)
  const message = responseData.choices[0].message.content
  const parsedContent = JSON.parse(message)
  console.log("回答:", parsedContent.answer)
  console.log("追加情報:", parsedContent.additionalInfo)
}
```

以上の追記が終わったのちに実行した結果がこちらです。
answerとadditionalInfoを分けて出力することができています。
こちらのkey名は自由でtypeで型を定義し、
key名またはdescriptionの説明でChatGPTが推測して、
パラメータに内容をセットしてくれています。

```
user % node index.js
回答: 2500
追加情報: 山本さんと田中さんがそれぞれ支払う金額は2,500円です。
```

# 各パラメータの説明

改めて各項目を確認します。

```javascript
const RESPONSE_FORMAT = {
  type: "json_schema",
  json_schema: {
    name: "response",
    strict: true, // Structured Outputsを使用するにはtrueにする必要がある
    schema: {
      type: "object", // propertiesに対する型定義
      properties: {
        answer: {
          type: "integer",
          description: "最終的な回答を数値のみで出力", // より正確に出力してもらうための追加条件
        },
        additionalInfo: { type: "string", description: "追加情報" },
      },
      required: ["answer", "additionalInfo"], // 必ず出力してほしい項目
      // 指定したプロパティ以外の許可の有無。Structured Outputsを使用するにはfalseにする必要がある
      additionalProperties: false,
    },
  },
}
```

コメント内容以外の補足は下記の通りです。

### type
下記の中から設定可能です。
object, string, number, integer, boolean, array,null, any

### nameとdescription
必ず必要な項目ではないですがよりChatGPTに正確に出力してほしい時は有用になります。

### プロパティの名前
「answer」や「additionalInfo」は任意の名前で、
「finalAnswer」に変更したり、別の項目を追加することが可能です。

# プロパティ例
プロパティは他にも追加できるため、下記のように項目を追加してみます。
下記内容は初めから存在するkeyではないため、
欲しいものをこれ以外にも自由に追加することができます。

### status
回答が成功したかを出力。

### score
回答への自信の度合いを表現。

### message
ユーザーへのメッセージを出力。

### understandingPoint
ChatGPTが質問に関して理解できているか確認。

### nonUnderstandingPoint
ChatGPTが質問に対して矛盾や不足がある場合に出力。

### dataとperson, payment
それぞれの人物がいくらずつ払うか。

### steps
ChatGPTが解答の際に考えた順序。


上記を実装した内容が下記になります。

```javascript
const RESPONSE_FORMAT = {
  type: "json_schema",
  json_schema: {
    name: "response",
    strict: true,
    schema: {
      type: "object",
      properties: {
        answer: {
          type: "integer",
          description: "最終的な回答を数値のみで出力",
        },
        additionalInfo: { type: "string", description: "追加情報" },
        status: { type: "string", description: "success or error" },
        score: {
          type: "integer",
          description: "回答への自信の高さを0~100で評価",
        },
        message: { type: "string", description: "ユーザーへのメッセージ" },
        understandingPoint: { type: "string", description: "理解した点" },
        nonUnderstandingPoint: {
          type: "string",
          description: "理解できなかった点",
        },
        data: {
          type: "object",
          properties: {
            persons: {
              type: "array",
              description: "支払い情報",
              items: {
                type: "object",
                properties: {
                  name: { type: "string", description: "名前" },
                  payment: { type: "integer", description: "支払い金額" },
                },
                required: ["name", "payment"],
                additionalProperties: false,
              },
            },
          },
          required: ["persons"],
          additionalProperties: false,
        },
        steps: {
          type: "array",
          items: { type: "string" },
          description: "chatgptが答えを出すまでの手順",
        },
      },
      required: [
        "answer",
        "score",
        "understandingPoint",
        "nonUnderstandingPoint",
        "additionalInfo",
        "status",
        "message",
        "data",
        "steps",
      ],
      additionalProperties: false,
    },
  },
}
```

main()にログを追加します。

```javascript
async function main() {
  const responseData = await getChatGPTResponse(SYSTEM_PROMPT, USER_PROMPT)
  const message = responseData.choices[0].message.content
  const parsedContent = JSON.parse(message)
  console.log("回答:", parsedContent.answer)
  console.log("追加情報:", parsedContent.additionalInfo)
  console.log("ステータス:", parsedContent.status)
  console.log("スコア:", parsedContent.score)
  console.log("メッセージ:", parsedContent.message)
  console.log("データ:", parsedContent.data)
  console.log("手順:", parsedContent.steps)
  console.log("理解した点:", parsedContent.understandingPoint)
  console.log("理解できなかった点:", parsedContent.nonUnderstandingPoint)
}
```

ついでに、ChatGPTに回答を迷ってもらうために5001円と割り切れない金額を設定しました。

```javascript
const USER_PROMPT =
  "5001円の会計を山本さんと田中さんの二人で分けると1人は何円支払いますか？ それぞれの人の名前と金額を教えてください。"
```

準備ができたので出力してみます。

```javascript
user % node index.js
回答: 2500
追加情報: 5001円を同額で分けると、1人あたり2500円となり、1円が割り切れません。このため、山本さんが2500円、田中さんが2501円を支払うことで合計5001円を分けることができます。
ステータス: success
スコア: 90
メッセージ: 山本さんと田中さんが5001円をほぼ均等に分けます。
データ: {
  persons: [ { name: '山本', payment: 2500 }, { name: '田中', payment: 2501 } ]
}
手順: [
  '5001円を2で割る。',
  '結果は2500.5で、これは端数の1円が出ることを示している。',
  '山本さんが2500円を支払い、田中さんが2501円を支払うことで、合計5001円を達成する。'
]
理解した点: 合計金額を二人で均等に分ける方法。
理解できなかった点: 端数の1円をどちらが負担するか。
```

5001円の端数の1円は田中さんに払ってもらうことになりました。
理解できなかった点ではしっかり端数の１円をどちらが負担するかで迷ってくれています。
上記の判断をしたため、スコアは100ではなく90にはなっていますが、
完璧ではないことを理解してスコアを下げてくれていることが分かりました。

# まとめ
今回初めてStructured Outputsを使ってみましたが、
自身で設定したkeyでも型定義を守って出力してくれるため、
非常に扱いやすい機能だと感じました。
ChatGPTの結果を後処理に結び付けやすくなったので、
色々と試していきたいと考えています。