Next.jsでOpenAIAPIの応答を狙ったJSON形式にする

## はじめに
本記事はNext.jsでのOpenaiAPIを使った開発の際、進化版JSONモードであるStructured Outputを使ったときのことをまとめます。

あくまでも使い方を備忘録的にまとめたものになりますので、より細かい情報が欲しい場合は公式ドキュメントにアクセスしてください

https://platform.openai.com/docs/guides/structured-outputs

## 環境
- Next.js 13.4.5
- React 18.2
- openai 10.9.0

## JSON形式について
JavaScript Object Notationの略称で、可視性と処理の速さを両立したデータ形式。

JavaScriptにおけるオブジェクトのような構造を持つ

## 実装
公式ドキュメントの通りに実装していきます。

https://platform.openai.com/docs/guides/structured-outputs

Next.jsにおけるOpenAIAPIの使い方については過去の記事を参照してください。

https://qiita.com/araarara/items/e74c4f6f342c3828fc99

## コード
```typescript:app/api/openai/route.ts
import { NextResponse, NextRequest } from 'next/server';
import { OpenAI } from 'openai';
import { zodResponseFormat } from 'openai/helpers/zod.mjs';
import { z } from 'zod';
import type { OpenaiTypes } from '@/types';

export async function POST(req: NextRequest) {
    const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

    const Human = z.object({
        name: z.string(),
        age: z.number(),
    });

    const completion = await openai.beta.chat.completions.parse({
        model: "gpt-4o-2024-08-06",
        messages: [
            { role: "system", content: "入力される文章から名前と年齢を抜き出して" },
            { role: "user", content: "こんにちは、私は太郎です。20歳です。" },
        ],
        response_format: zodResponseFormat(Human, "human"),
    });

    const response = completion.choices[0].message.parsed;
    
    console.log(response);//{ name: '太郎', age: 20 }

    return NextResponse.json(response); 
}

```

## 解説
### 1. Zodのインストール
typescriptにおけるデータ構造を管理するライブラリであるZodを使用します。

```terminal
npm install zod
```
### 2. インポート
`OpenAI`のインポートに加えて、`zodResponseFormat`と`z`をインポートします。
```typescript
import {OpenAI} from 'openai';
import { zodResponseFormat } from 'openai/helpers/zod.mjs';
import { z } from 'zod';
```
### 3. 出力形式の定義
下の様に、出力形式を定義します。配列や、ネストされたオブジェクト構造での宣言も可能です。
```typescript
const Human = z.object({
    name: z.string(),
    age: z.number(),
});
```
出力は以下の形式をサポートしています(公式ドキュメントより引用)
- String  
- Number  
- Boolean 
- Integer 
- Object
- Array
- Enum
- anyOf

### 4.モデルの選択
公式ドキュメントには次のように書かれています。すべてのgpt-4o系のモデルで動作が確認できました。
>Supported models
Structured Outputs are available in our latest large language models, starting with GPT-4o:
gpt-4o-mini-2024-07-18 and later
gpt-4o-2024-08-06 and later
Older models like gpt-4-turbo and earlier may use JSON mode instead.

### 5.API呼び出し
追加のプロパティ、`response_format`を指定し、出力を調節します。

`zodResponseFormat`の引数はそれぞれ`(作成したスキーマ,スキーマの識別子)`となります
```typescript
const completion = await openai.beta.chat.completions.parse({
        model: "gpt-4o-2024-08-06",
        messages: [
            { role: "system", content: "入力される文章から名前と年齢を抜き出して" },
            { role: "user", content: "こんにちは、私は太郎です。20歳です。" },
        ],
        response_format: zodResponseFormat(Human, "human"),
});
```
### 6.応答を取り出す
↓のようにして応答をJSON形式で取り出します
```typescript
const response = completion.choices[0].message.parsed;

console.log(response);//{ name: '太郎', age: 20 }
```

ちなみに、`completion.choices[0].message`は次のようになっていることから、

`.content`で文字列、`.parsed`でJSON形式のオブジェクトを取り出せることが分かります

```json
{
  role: 'assistant',
  content: '{"name":"太郎","age":20}',
  refusal: null,
  tool_calls: [],
  parsed: { name: '太郎', age: 20 }
}
```
また、得られた出力は通常のオブジェクトの様にドットでプロパティの値を取り出せます。
```typescript
console.log(response.name);//=>太郎
console.log(response.age);//=>20
```

## まとめ
以上になります。生成AI関連のトレンドな技術は半年前のブログがもう古かったりするので公式docが最強ですね

まだまだ勉強中ですので慣習と違うとこがありましたらご指摘ください！！

## 参考

https://datamix.co.jp/media/datascience/introduction-to-json/

https://qiita.com/mitecha/items/1315c9707ef382cbebfc

https://platform.openai.com/docs/guides/structured-outputs

https://zenn.dev/ryoppippi/articles/b0d3e17322099b