今すぐ使える!Azure OpenAI × Zodで実現する高精度な構造化出力の実装方法

今すぐ使える!Azure OpenAI × Zodで実現する高精度な構造化出力の実装方法

ども!4月に年度が更新され、4年目のエンジニアになった龍ちゃんです。気づけば、また春の季節になっていますね。春といってもまだ寒いですね。

今回は、Azure OpenAI Serviceのお話になります。生成AIを活用したアプリを作成する際に、レスポンスがJSONで返答されるかどうかは重要な要素になります。DifyでもAOAIでも、方法を模索して実装していました。

AOAIのバージョンアップに併せて「構造化出力」という機能がリリースされていました。こちらを用いてTypescript環境で構造化出力を行うサンプルの実装を行います。公式のサンプルでは、PythonとREST APIの記載があります。

前回は、「json schema」での構造化出力を検証しました。Open AIでは、「Zod」を使用した構造化出力を検証し実装していきます。

前提条件

公式リファレンス:構造化出力の情報はこちらにあります。モデルによっては使えない機能なので、サポートされているモデルやバージョンなどを確認して使用してください。アップデートによっては使えなくなる可能性もあるので、公式の情報を確認して実装するのが一番です。

私の環境では、以下のモデルを使用してAPI Key認証で検証しています。

モデル名モデルバージョンAPIバージョン
gpt-4o-mini2024-07-182024-08-01-preview

Typescriptのライブラリに関する情報ですが、発見できた情報としては以下の二つです。

実装は、OpenAIの「Text Generation:Generate JSON data」を参考に進めていきます。

サンプルでは、「LINE×生成AI:チャットバトルゲームを作る!」で作成したAPIのレスポンスオブジェクトを作成します。こちらのサンプルでは、「キャラクター同士を戦わせ戦いの勝者と描写を出力するゲーム」になっています。出力したいJSONは以下になります。

{
    "winner":"user"|"system",
    "combatLogs": {
      "round":number,
      "combatLog":string
    }[]
}

構造化出力:Zodサンプル

AOAIへ問い合わせて、構造化出力を行うサンプルの全文を張ります。

import { zodResponseFormat } from 'openai/helpers/zod';
import { AzureOpenAI } from 'openai';
import { z } from 'zod';

// Zod定義
const PromptResultTypeSchema = z.object({
  winner: z
    .enum(['user', 'system'])
    .describe('戦いの勝者を記述する。ユーザー側が勝利した場合は「user」、システム側が勝利した場合は「sysytem」を代入'),
  combatLogs: z.array(
    z.object({
      round: z.number().describe('戦いの記録の順序を記述する。'),
      combatLog: z.string().describe('戦いの記録の内容を記述する。'),
    }),
  ),
});

// Zod ObjectからTypeを生成
type PromptResultTypeFromZod = z.infer<typeof PromptResultTypeSchemaZod>;


async battlePrompotFormatJSON_Zod(): Promise<PromptResultType> {
  const AOAIClient = new AzureOpenAI({
    endpoint: "xxxxxxxxxxxxxxxxxxxxx",
    apiKey: "xxxxxxxxxxxxxxxxxxxxx",
    apiVersion: "xxxxxxxxxxxxxxxxxxxxx",
    deployment: "xxxxxxxxxxxxxxxxxxxxx",
  });

  const systemPrompt = `
   ~~~~~~~~~略~~~~~~~~~
   `;
  
  const response_format = zodResponseFormat(PromptResultTypeSchemaZod, 'combat_schema');

  const res = await AOAIClient.chat.completions.create({
    messages: [
      {
        role: 'system',
        content: systemPrompt,
      },
      { role: 'user', content: '剣士 主に剣で戦う' },
    ],
    model: '',
    response_format: response_format,
  });

  const math_combat_schema = res.choices[0].message;

  if (math_combat_schema.refusal) {
    return {
      winner: 'user',
      combatLogs: [
        {
          round: 1,
          combatLog: 'JSON整形を正しく行うことができませんでした。よって開発者の負けです。',
        },
      ],
    };
  }

  const tmp = PromptResultTypeSchemaZod.safeParse(JSON.parse(res.choices[0].message.content));
  
  if (!tmp.success) {
    console.log(tmp.error);
    return {
      winner: 'user',
      combatLogs: [
        {
          round: 1,
          combatLog: 'JSON整形を正しく行うことができませんでした。よって開発者の負けです。',
        },
      ],
    };
  }

  return tmp.data;
}

手順としては、AOAIとの通信部分にresponse_formatとしてZodから作成したフォーマットの定義を渡します。結果として構造化出力で返答が返されます。

実行した結果として、以下のような情報が出力されます。

{
  "winner": "system",
  "combatLogs": [
    {
      "round": 1,
      "combatLog": "リングの中央で立ちはだかるボクシングチャンピオン、彼の拳はまるで鉄の塊のようだ。挑戦者である剣士は、激しい視線を向け、自身の剣をしっかりと構える。初めのラウンドが始まると、チャンピオンが力強いジャブを繰り出す。それは剣士の顔面をかすめ、観客から驚嘆の声が上がる。"
    },
    {
      "round": 2,
      "combatLog": "剣士は冷静を保ちながら、反撃のチャンスをうかがう。しかし再びチャンピオンが猛攻を仕掛け、フックが剣士の横っ面に見舞う。その強烈な一撃に、剣士は一歩後退し、動揺を隠せない。拳の力に打ちひしがれる剣士の表情が、リングの明るさに映える。"
    },
    {
      "round": 3,
      "combatLog": "猛攻を続けるチャンピオンは、剣士のペースを完全に奪い取っている。挑戦者は必死に防御しようとするが、チャンピオンの強烈なアッパーカットが剣士の顎を捉える。剣士はついに膝をつき、観客たちはその劇的な光景に息を呑む。"
    },
    {
      "round": 4,
      "combatLog": "剣士はなんとか立ち上がるものの、目の前には打撃の鬼が立ちはだかる。最後の力を振り絞って反撃しようとするが、チャンピオンのパンチが再度放たれ、その拳は剣士の顔を叩きつけてしまう。剣士は再び地面に崩れ落ち、審判のカウントが始まる。"
    },
    {
      "round": 5,
      "combatLog": "カウントが進む中、剣士の意識が薄れていく。彼はまだ戦う力が残っているものの、チャンピオンの強力な防御と攻撃を前に完全に出遅れている。カウントが10に達する時、審判は試合を止め、チャンピオンの勝利を宣言する。"
    }
  ]
}

response_format定義方法

import { zodResponseFormat } from 'openai/helpers/zod';

const PromptResultTypeSchema = z.object({
  winner: z
    .enum(['user', 'system'])
    .describe('戦いの勝者を記述する。ユーザー側が勝利した場合は「user」、システム側が勝利した場合は「system」を代入'),
  combatLogs: z.array(
    z.object({
      round: z.number().describe('戦いの記録の順序を記述する。'),
      combatLog: z.string().describe('戦いの記録の内容を記述する。'),
    }),
  ),
});

const response_format = zodResponseFormat(PromptResultTypeSchema, 'combat_schema');

Zodの形式で出力させたいオブジェクトを定義します。ZodオブジェクトからzodResponseFormatを通してフォーマットを生成します。Zodを学んでいれば、フォーマット定義に関してはスムーズに定義することができます。

zodResponseFormatを通すことでJSON Schemaを作成してくれています。説明(describe)を定義することで各項目の情報が保管されます。説明を詳細にしておくことで、構造化(JSON)の作成をサポートしてくれます。

構造化の検証

const math_combat_schema = res.choices[0].message;

// Azure OpenAI Serviceのレスポンスからの確認処理
if (math_combat_schema.refusal) {
  return {
    winner: 'user',
    combatLogs: [
      {
        round: 1,
        combatLog: 'JSON整形を正しく行うことができませんでした。よって開発者の負けです。',
      },
    ],
  };
}

// Zodのスキーマに対応できているかの確認処理
// Errorがある場合もレスポンスに含まれる
const tmp = PromptResultTypeSchemaZod.safeParse(JSON.parse(res.choices[0].message.content));

if (!tmp.success) {
  console.log(tmp.error);
  return {
    winner: 'user',
    combatLogs: [
      {
        round: 1,
        combatLog: 'JSON整形を正しく行うことができませんでした。よって開発者の負けです。',
      },
    ],
  };
}

構造化の検証は、Azure OpenAI Serviceからの返答とZodによるスキーマを用いた二つの方法で検証しています。エラーが発生した場合は、固定メッセージを返答しています。

AOAIからの返答には、モデルから返答された拒否メッセージ(choice.message.refusal)が含まれる場合があります。拒否メッセージが含まれる場合では、生成を開始したがうまく生成することができなかった場合に含まれます。

Zodのスキーマ判定では、AOAIからの返答をJSON構造化に変換して、Zodを通じて検証(safeParse)を行っています。検証の結果には、成功した場合はデータが失敗した場合はZodErrorが出力されます。

const stringSchema = z.string();

stringSchema.safeParse(12);
// => { success: false; error: ZodError }

stringSchema.safeParse("billie");
// => { success: true; data: 'billie' }

構造化出力の精度を高める方法

  • システムプロンプトで出力形式の詳細な指示を事前に設定する
  • Zodのオブジェクトの命名をシステムプロンプトの内容に合わせて適切に命名する
  • 複雑な出力が必要な場合は、処理を小さな単位に分割する

これらの方法を組み合わせることで、より高精度な構造化出力を実現できます。

あなたは決闘の審判です。二つのキャラクターの戦闘を見守り、勝敗までの流れを判定してください。
AI側がチャンピオン、ユーザー側が挑戦者です。
次の内容は必ず守ってください「チャンピオンのキャラクターが勝利した場合はsystem、挑戦者が勝利した場合はuserと明記してください。」
---
ボクシングチャンピオン主にこぶしで戦う
---
以下のType出力を守った内容を最後に付録として記載してください。
---
{
  "combatLogs": {
    "round":number,
    "combatLog":string
  }[]
}
---
例は以下のようになります。combatLogは小説家のように過大に脚色して演出してください。決闘の勝者を明確にしてください。
---
{
  "combatLogs": [
    {
      "round": 1,
      "combatLog": "訓練場の教官が鉄の剣で攻撃しました"
      },
    {
      "round": 2,
      "combatLog": "訓練場の教官が鉄の盾で防御しました"
    }
  ]
}
---

システムプロンプトでの詳細な指示は、AIモデルが期待される出力形式を正確に理解するために重要です。出力の例(few-shot)を含めることで、より具体的な指示となります。

おわり

以上、Azure OpenAI Serviceの構造化出力(JSON)をZodを用いて検証する方法について解説しました。システムプロンプトの適切な設定と、Zodによる厳密な型チェックをセットで行うことで、より信頼性の高いAIアプリケーションを構築することができます。みなさんも是非試してみてください!

ご覧いただきありがとうございます! この投稿はお役に立ちましたか?

役に立った 役に立たなかった

0人がこの投稿は役に立ったと言っています。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です