挨拶
ども!ノリでブログを書いていると、今週は月曜日から毎日ブログを書いている龍ちゃんです。最近は、Dify入門ガイドシリーズに力を入れています。
今回は、Azure OpenAI Serviceのお話になります。生成AIを活用したアプリを作成する際に、レスポンスがJSONで返答されるかどうかは重要な要素になります。DifyでもAOAIでも、方法を模索して実装していました。
- Difyでの構造化:「Dify入門ガイド:LLM出力を構造化する!JSONデータ作成の具体的手順」
- AOAIでの構造化:「AOAI:Gpt-4oでJSON出力に失敗する対症療法」
AOAIのバージョンアップに併せて「構造化出力」という機能がリリースされていました。こちらを用いてTypescript環境で構造化出力を行うサンプルの実装を行います。公式のサンプルでは、PythonとREST APIの記載があります。
今回は、動くかの確認を目標に進めていきます。
前提条件
公式リファレンス:構造化出力の情報はこちらにあります。モデルによっては使えない機能なので、サポートされているモデルやバージョンなどを確認して使用してください。アップデートによっては使えなくなる可能性もあるので、公式の情報を確認して実装するのが一番です。
私の環境では、以下のモデルを使用してAPI Key認証で検証しています。
モデル名 | モデルバージョン | APIバージョン |
---|---|---|
gpt-4o-mini | 2024-07-18 | 2024-08-01-preview |
Typescriptのライブラリに関する情報ですが、発見できた情報としては以下の二つです。
実装は、OpenAIの「Text Generation:Generate JSON data」を参考に進めていきます。
サンプルでは、「LINE×生成AI:チャットバトルゲームを作る!」で作成したAPIのレスポンスオブジェクトを作成します。こちらのサンプルでは、「キャラクター同士を戦わせ戦いの勝者と描写を出力するゲーム」になっています。出力したいJSONは以下になります。
{
"winner":"user"|"system",
"combatLogs": {
"round":number,
"combatLog":string
}[]
}
構造化出力:Typescript json schemaサンプル
JSON返答を実現するサンプルの全文を貼ります。
const client = new AzureOpenAI({
endpoint: "******************",
apiKey: "*******************",
apiVersion: "***************",
deployment: "***************",
});
const systemPrompt = `
あなたは決闘の審判です。二つのキャラクターの戦闘を見守り、勝敗までの流れを判定してください。
AI側がチャンピオン、ユーザー側が挑戦者です。
次の内容は必ず守ってください。
「チャンピオンのキャラクターが勝利した場合はsystem、挑戦者が勝利した場合はuserと明記してください。」
---
ボクシングチャンピオン主にこぶしで戦う
---
以下のType出力を守った内容を最後に付録として記載してください。
---
{
"combatLogs": {
"round":number,
"combatLog":string
}[]
}
---
例は以下のようになります。combatLogは小説家のように過大に脚色して演出してください。決闘の勝者を明確にしてください。
---
{
"combatLogs": [
{
"round": 1,
"combatLog": "訓練場の教官が鉄の剣で攻撃しました"
},
{
"round": 2,
"combatLog": "訓練場の教官が鉄の盾で防御しました"
}
]
}
---
`;
const result = await client.chat.completions.create({
model: '',
messages: [
{
role: 'system',
content: systemPrompt,
},
{ role: 'user', content: "剣士 主に剣で戦う" },
],
response_format:{
type:"json_schema",
json_schema:{
name:"combat_schema",
schema:{
type:"object",
properties:{
winner:{
description:"戦いの勝者を記述する。ユーザー側が勝利した場合は「user」、システム側が勝利した場合は「system」を代入",
type:"string"
},
combatLogs:{
description:"戦いの記録を記述する。roundには記録の順序を記述する。combatLogには記録の内容を記述する。",
type:"array",
items:{
type:"object",
required:["round","combatLog"],
properties:{
round:{
type:"number"
},
combatLog:{
type:"string"
}
}
}
}
}
}
}
}
});
// text形式で返答される
console.log(result.choices[0].message.content)
// JSONパースで丸め込む
const choice: PromptResultType = JSON.parse(result.choices[0].message.content);
手順としては、AOAIとの通信部分にresponse_formatとしてjson_schemaの定義を渡すことで、構造化出力で返答が返されます。
{
"winner": "system",
"combatLogs": [
{
"round": 1,
"combatLog": "リングの中央、チャンピオンであるボクシングチャンピオンが、俊敏な身のこなしで挑戦者の剣士に挑みかかります。鋭い目つきで、彼の拳はまるで猛獣の爪のように速く、恐れを知らない剣士を襲います。"
},
{
"round": 2,
"combatLog": "剣士がその長い剣を振るい、防御の構えを取ります。しかし、チャンピオンは緩急自在に動き、右フックが剣士の面に直撃!衝撃で剣士は後ろに仰け反り、観衆の息を呑む音が響きます。"
},
{
"round": 3,
"combatLog": "剣士が立ち直り、再び心を整えます。彼は素早く踏み込み、切りつけるチャンスを狙うも、ボクシングチャンピオンはその動きを見逃さず、すかさずカウンターのジャブを放つ!剣士は打撃を受けて体勢を崩します。"
},
{
"round": 4,
"combatLog": "次第にチャンピオンの優位が明白になる中、剣士は最後の力を振り絞り、一閃の剣撃を放つ。しかし、予測されたその攻撃をかわしたチャンピオンは、一瞬の隙をついて剣士の腹部に強烈なアッパーカットを叩き込みます!剣士はその場に崩れ落ち、観客は歓声と共にその瞬間を見守ります。"
},
{
"round": 5,
"combatLog": "ダウンした剣士は力なく立ち上がることができず、レフェリーが試合終了を告げる。チャンピオンの圧倒的な強さに、剣士は無情にも敗北を認めざるを得ませんでした。リングの中、勝利の拳を掲げるボクシングチャンピオンの姿が、まるで神々しい光に包まれているかのようです。"
}
]
}
response_format定義方法
今回のサンプルでは、json_schemaを自力で作成しました。Node.jsでresponse_formatを記述する方法は、「自力」と「Zod」を使う方法の二つがあります。
自力でjson_schemaを指定した場合は、出力がテキスト形式で返答されます。そのため、最終的にJSONをパースして構造を取得しています。基本はjson schemaに準拠されていますが、場合によっては使用できないものもあるようです。
Zodを使う場合は、検証済みのデータが返答されます。
二つの方法に共通して、「安全上の理由」で有効なJSON構造の情報を吐き出さない可能性もあるためエラーハンドリングは必要です。
今回は、検証目的だったのでjson schemaを自力で試しました。 アプリケーションに組み込む場合はzodで組んだ方が良いかと思います。こちらは検証してまとめます。
構造化出力の精度を高める方法
JSON構造とjson schemaを貼ります。
{
"winner":"user"|"system",
"combatLogs": {
"round":number,
"combatLog":string
}[]
}
{
type:"json_schema",
json_schema:{
name:"combat_schema",
schema:{
type:"object",
properties:{
winner:{
description:"戦いの勝者を記述する。ユーザー側が勝利した場合は「user」、システム側が勝利した場合は「system」を代入",
type:"string"
},
combatLogs:{
description:"戦いの記録を記述する。roundには記録の順序を記述する。combatLogには記録の内容を記述する。",
type:"array",
items:{
type:"object",
required:["round","combatLog"],
properties:{
round:{
type:"number"
},
combatLog:{
type:"string"
}
}
}
}
}
}
}
}
- システムプロンプトで出力形式の詳細な指示を事前に設定する
- json schemaの名前をシステムプロンプトの内容に合わせて適切に命名する
- 複雑な出力が必要な場合は、処理を小さな単位に分割する
これらの方法を組み合わせることで、より高精度な構造化出力を実現できます。
システムプロンプトでの詳細な指示は、AIモデルが期待される出力形式を正確に理解するために重要です。出力の例(few-shot)を含めることで、より具体的な指示となります。
json schemaの適切な命名は、システムプロンプトの例と一致させることで特に効果を発揮します。また、英語として不自然でないことも重要です。定義している情報が出力したい情報と一致していることで高い精度での情報抽出が行えます。
複雑な出力を小さな単位に分割することは、各部分の精度を個別に向上させることができます。複数の戦闘を行わせる場合では、戦闘ごとの描写の抽出が難しくなる可能性があります。この場合は、戦闘ごとにプロンプトを分割するべきです。
これらの方法を実装することで、より信頼性の高い構造化出力を実現し、アプリケーションの品質向上につながります。
終わり
今回は、Azure OpenAI Serviceを使用してJSON形式の構造化出力を実装する方法について解説しました。システムプロンプトの工夫やjson schemaの適切な設計により、より精度の高い出力を得ることができます。