定義したスキーマ通りのAPIレスポンスは安心できる

# 概要
先日、OpenAIからStructured Outputsに関する機能がリリースされた、といった情報がありました。
早速それを確認して、ざっくり記事にしました。

https://qiita.com/eno49conan/items/5d7e8dfc3849880cb893

この記事では、**どういったところが開発上ありがたいのか**、具体例（英単語の学習をするケース）を交えながら改めて考えてました。

## 私なりの結論
**出力（APIのレスポンス）ブレを心配しなくていい**のが、一番ありがたいと感じました。
言い換えになるかもしれませんが、**レスポンスの構造が実行前からわかる**ということです。

chatGPTなどで実感されている方は多いと思いますが、
Prompt次第で、レスポンスの文章構成やフォーマットが変わってしまうと、どういうPromptにすれば臨んだ回答になるのか、試行錯誤が必要です。
この作業は大変ですし、不安定な出力になると実用的ではなくなる場合もありそうです。

（と書きましたが、`gpt-4o`あたりから、それほど指示しなくても整形して出力といったことは勝手にしてくれることが増えてきている印象を持っています）

また今回は扱わないですが、出力を構造化するのではなく、Promptを1から10まで自分で考えるのではなく、機械学習の力も借りよう！といった機能をもつライブラリ、
**DSPy**(Star:15k越え)が登場しています。

https://github.com/stanfordnlp/dspy

## 具体的な話をコードを交えて
##### どんな情報を取得する？
英単語（TOEIC730点をとるのに重要な10語）と各単語に対する例文を取得したいとします。具体的に欲しい情報は以下の通りとしました。
- 英単語
- 単語の意味
- 対象の単語を用いた例文
- 例文の和訳
##### どんな形で取得する？
取得の方法について、以下の3つを比較してみます。
モデル`gpt-4o-2024-08-06`は使用します。（openAIのAPIキーは用意された前提で話を進めていきます。）
また、[openai](https://github.com/openai/openai-python/releases/tag/v1.40.0)のバージョンは`v1.40.0`以上にアップデートします。
1. Promptのみ
1. JSONモードを使用
1. スキーマ指定（Structured Outputsを活かしたやり方）

先に、これら3つの方法についてまとめました。（主観的な部分も含まれていると思いますが、概ねこんな感じでは？と思ってます！）

| 観点 | Promptのみ | JSONモードを使用 |スキーマ指定 |
|:-----------:|:------------:|:------------:|:------------:|
| **出力形体**       | △（不明）        | 〇（json）         | 〇（pythonオブジェクト）         |
| **各情報に紐づくキー名の一貫性（※1)**     | △ （プロンプト中の単語を用いたキー名となる場合もあり）     | △（プロンプト中の単語を用いたキー名となる場合もあり）       |◎（指定したスキーマのキー（フィールド）名を使用）     
| **開発のしやすさ**     | △（出力形体が予測できない）      | 〇（キー名を使った処理をするのは難しいかもしれない）       |◎（pythonオブジェクトかつキー名が固定）

※1：「英単語」に紐づくキー名が`word`、「単語の意味」に紐づくキー名が`meaning_japanese`といいうように、各情報がどういった変数名とマッピングするか、変数名が変動か固定なのか、といった観点の話です。

### Promptのみ
**コード**
```python
from dotenv import load_dotenv
load_dotenv(dotenv_path='.env')
from pydantic import BaseModel
from openai import OpenAI
client = OpenAI()
completion = client.chat.completions.create(
    model="gpt-4o-2024-08-06",
    messages=[
        {"role": "system", "content": "You are a helpful toeic expert tutor."},
        {"role": "user", "content": "In the TOEIC Reading and Listening sections, please provide 10 important vocabulary words for achieving a score of 730. For each word, include its meaning by japanese, an example sentence, and a Japanese translation."},
    ],
)

message = completion.choices[0].message.content
print(message)
```
**レスポンス**
Markdownの形式で取得されたようです。

<details><summary>出力内容</summary>

```
1. **Assess**
   - Meaning: 評価する
   - Example Sentence: The manager will assess the team's performance at the end of the month.       
   - Japanese Translation: マネージャーは月末にチームの業績を評価します。

2. **Benefit**
   - Meaning: 利益、恩恵
   - Example Sentence: The new policy will benefit all employees.
   - Japanese Translation: 新しい方針はすべての従業員に利益をもたらします。

3. **Convey**
   - Meaning: 伝える、運ぶ
   - Example Sentence: Please convey my thanks to the entire team.
   - Japanese Translation: チーム全体に私の感謝を伝えてください。

4. **Diverse**
   - Meaning: 多様な
   - Example Sentence: The company offers a diverse range of products.
   - Japanese Translation: その会社は多様な製品を提供しています。

5. **Endeavor**
   - Meaning: 努力
   - Example Sentence: Our team’s endeavor led to the project’s success.
   - Japanese Translation: 私たちのチームの努力がプロジェクトの成功をもたらしました。

6. **Implement**
   - Meaning: 実行する
   - Example Sentence: The company plans to implement new technology by next year.
   - Japanese Translation: 会社は来年までに新しい技術を実行する予定です。

7. **Justify**
   - Meaning: 正当化する
   - Example Sentence: The manager had to justify the budget increase to the board.
8. **Mitigate**
   - Meaning: 軽減する、緩和する
   - Example Sentence: The new measures are designed to mitigate the risks.
   - Japanese Translation: 新しい対策はリスクを軽減するために設計されています。

9. **Proficient**
   - Meaning: 熟達した、堪能な
   - Example Sentence: She is proficient in several programming languages.
   - Japanese Translation: 彼女は複数のプログラミング言語に熟達しています。

10. **Viable**
    - Meaning: 実行可能な
    - Example Sentence: We need a viable solution to the current problem.
    - Japanese Translation: 現在の問題に対する実行可能な解決策が必要です。
```
</details>

### JSONモードを使用
**コード**
```python
completion = client.chat.completions.create(
    model="gpt-4o-2024-08-06",
    messages=[
        {"role": "system", "content": "You are a helpful toeic expert tutor. output result by using json"},
        {"role": "user", "content": "In the TOEIC Reading and Listening sections, please provide 10 important vocabulary words for achieving a score of 730. For each word, include its meaning by japanese, an example sentence, and a Japanese translation."},
    ],
    response_format={ "type": "json_object" }
)

message = completion.choices[0].message.content
print(message)
```
**レスポンス**
指定したのであたりまえですが、JSON形式で情報が出力されました。
<details><summary>出力内容</summary>

```json
{
  "vocabulary": [
    {
      "word": "Accommodate",
      "meaning_japanese": "収容する、適応させる",
      "example_sentence": "The hotel can accommodate up to 500 guests.",
      "sentence_japanese": "そのホテルは最大で500人の客を収容できます。"
    },
    {
      "word": "Allocate",
      "meaning_japanese": "割り当てる、配分する",
      "example_sentence": "The company has allocated $5000 for the new project.",
      "sentence_japanese": "その会社は新しいプロジェクトに5000ドルを割り当てました。"
    },
    {
      "word": "Comply",
      "meaning_japanese": "従う、遵守する",
      "example_sentence": "All employees must comply with the safety regulations.",
      "sentence_japanese": "全ての従業員は安全規則に従わなければなりません。"
    },
    {
      "word": "Enhance",
      "meaning_japanese": "向上させる、強化する",
      "example_sentence": "The new software will enhance the performance of our computers.",
      "sentence_japanese": "新しいソフトウェアはコンピュータの性能を向上させます。"
    },
    {
      "word": "Implement",
      "meaning_japanese": "実施する、導入する",
      "example_sentence": "The new policy will be implemented next month.",
      "sentence_japanese": "新しい方針は来月導入されます。"
    },
    {
      "word": "Negotiate",
      "meaning_japanese": "交渉する",
      "example_sentence": "We need to negotiate better terms with the supplier.",
      "sentence_japanese": "私たちは供給者とより良い条件を交渉する必要があります。"
    },
    },
    {
      "word": "Verify",
      "meaning_japanese": "確認する、検証する",
      "example_sentence": "Please verify your email address by clicking the link.",
      "sentence_japanese": "リンクをクリックしてメールアドレスを確認してください。"
    }
  ]
}
```
</details>

### スキーマ指定（Structured Outputsを活かしたやり方）
**コード**
```python
from dotenv import load_dotenv
load_dotenv(dotenv_path='.env')
from pydantic import BaseModel
from openai import OpenAI


class WordSet(BaseModel):
    word: str # 英単語
    word_meaning_in_japanese: str # 英単語の意味
    example_sentence: str # 例文
    example_sentence_in_japanese: str # 例文の日本語訳


class WordsResponse(BaseModel):
    words: list[WordSet]


client = OpenAI()

completion = client.beta.chat.completions.parse(
    model="gpt-4o-2024-08-06",
    messages=[
        {"role": "system", "content": "You are a helpful toeic expert tutor."},
        {"role": "user", "content": "In the TOEIC Reading and Listening sections, please provide 10 important vocabulary words for achieving a score of 730. For each word, include its meaning, an example sentence, and a Japanese translation."},
    ],
    response_format=WordsResponse,
)

message = completion.choices[0].message
if message.parsed:
    for word in message.parsed.words:
        print(word)
else:
    print(message.refusal)
```
**レスポンス**
for文で各要素を出力していますが、指定した構造（WordSetクラス）のキー（フィールド）通りに情報が設定されています。

<details><summary>出力内容</summary>

```
word='Administration' word_meaning_in_japanese='経営、管理' example_sentence='The administration decided to implement new policies to improve productivity.' example_sentence_in_japanese='管理部は生産性を向上させるために新しいポリ シーを実施することに決めました。'
word='Negotiate' word_meaning_in_japanese='交渉する' example_sentence='The sales team had to negotiate the terms of the contract with the client.' example_sentence_in_japanese='営業チームは顧客と契約条件を交渉しなければなりません でした。'
word='Optimize' word_meaning_in_japanese='最適化する' example_sentence='The company aims to optimize its supply chain to reduce costs.' example_sentence_in_japanese='会社はコストを削減するためにサプライチェーンを最適化することを目指しています。'
word='Revenue' word_meaning_in_japanese='収益' example_sentence="The company's revenue increased by 20% compared to last year." example_sentence_in_japanese='会社の収益は昨年と比較して20%増加しました。'
word='Confidential' word_meaning_in_japanese='機密の' example_sentence='Please ensure that all confidential documents are stored securely.' example_sentence_in_japanese='すべての機密書類が安全に保管されていることを確認してください。'
word='Deduct' word_meaning_in_japanese='控除する' example_sentence="The company deducts taxes directly from employees' salaries." example_sentence_in_japanese='会社は従業員の給与から直接税金を控除します。'
word='Forecast' word_meaning_in_japanese='予測する' example_sentence='The weather forecast predicts heavy rain tomorrow.' example_sentence_in_japanese='天気予報は明日大雨が降ると予測しています。'
word='Compensate' word_meaning_in_japanese='補償する' example_sentence='The company agreed to compensate the customers for the inconvenience.' example_sentence_in_japanese='会社は顧客に不便をかけたことへの補償をすることに同意しま した。'
word='Implement' word_meaning_in_japanese='実施する' example_sentence='The government plans to implement new regulations by the end of the year.' example_sentence_in_japanese='政府は年末までに新しい規制を実施する計画です。'

word='Allocate' word_meaning_in_japanese='割り当てる' example_sentence='Funds were allocated to improve the infrastructure of the city.' example_sentence_in_japanese='資金は都市のインフラを改善するために割り当てられました。'
```
</details>

## まとめ

実際に実行してみた上で、改めて各方法を比較した表が以下の通りです。（再掲）

| 観点 | Promptのみ | JSONモードを使用 |スキーマ指定 |
|:-----------:|:------------:|:------------:|:------------:|
| **出力形体**       | △（不明）        | 〇（json）         | 〇（pythonオブジェクト）         |
| **各情報に紐づくキー名の一貫性（※1)**     | △ （プロンプト中の単語を用いたキー名となる場合もあり）     | △（プロンプト中の単語を用いたキー名となる場合もあり）       |◎（指定したスキーマのキー（フィールド）名を使用）     
| **開発のしやすさ**     | △（出力形体が予測できない）      | 〇（キー名を使った処理をするのは難しいかもしれない）       |◎（pythonオブジェクトかつキー名が固定）

**※1**：「英単語」に紐づくキー名が`word`、「単語の意味」に紐づくキー名が`meaning_japanese`といいうように、各情報がどういった変数名とマッピングするか、変数名が変動か固定なのか、といった観点の話です。

openAIだけではないですが、このLLM界隈の技術について次々と機能が追加され、開発しやすくなっている印象を受けています。
冒頭に記載したDSPyも含め、新しい情報には定期的に目を通していきたいと思います。