OpenAI DevDay 2024 | 信頼性の高いアプリケーションのための構造化出力

15,967 文字

OpenAI DevDay 2024 | Structured outputs for reliable applications
Learn how to increase reliability with precise JSON schema adherence

こんにちは。皆さんこんにちは。今日は構造化出力についてお話しします。これは今年8月にOpenAI APIで公開された素晴らしい新機能です。LLMを扱う開発者にとって大きなブレイクスルーとなり、わずか数週間で何十万もの開発者がアプリケーションに組み込んでいます。
私はAtty Eletiで、OpenAIでAPIデザインを統括しています。
そして私はMichelle Pokrassです。APIのテクニカルリードを担当しています。今日は3つのことについてお話しします。まず、なぜ構造化出力が必要なのかを説明し、次にこの機能の仕組みについて、そして最後にどのように構築したのかをお話しします。
では早速始めましょう。
最初に戻って2020年、OpenAIがGPT-3をリリースした時から始めましょう。GPT-3はメールを書き、ブログ記事を下書きし、映画の脚本を生成することができました。テキスト生成に優れていたのです。すぐに開発者の皆さんは、この技術の面白い新しい使い方を見つけ始めました。
AI Dungeonのようなエンドゲームスクリプトの生成や、copy.aiのようなマーケティング資料の作成などです。3年後の2023年、私たちはGPT-4をリリースしました。これはLLMの知性における新たなブレイクスルーでした。モデルは初めて高度な推論が可能になり、複雑な指示に従い、長文から情報を抽出し、ユーザーに代わってアクションを起こすことができるようになりました。
開発者はさらに一歩先へと進み、CURSORのようなAI駆動の生産性ツール、Klarnaのようなカスタマーサービスエージェント、Duolingoのような言語学習アプリを構築しました。これらの製品には共通点がありました。それはLLMを外部の世界と接続していたことです。コードベースから外部APIやデバイス上のアクションまで。
これを実現するには、出力が構造化される必要がありました。典型的にはJSONです。例を見てみましょう。パーソナルアシスタントを構築していて、ユーザーのメッセージをAPIコールに変換したいとします。ユーザーが「ビートルズを再生して」と言った場合、期待する出力はこのようなものです。APIにplay musicを指定し、アーティストにビートルズを指定したJSONオブジェクトです。
しかし、よく得られるのはこのような出力です。「はい、音楽再生APIを呼び出すJSONはこちらです」という前置きから始まります。これは開発者にとってあまり役に立ちません。JSONだけが必要なのに、それを囲むテキストまで付いてきてしまうからです。これは問題です。LLMの出力は必ずしも信頼できず、アプリケーションへの統合を難しくしています。
開発者として私たちはこれを解決するためにいろいろな工夫を試みてきました。このようなプロンプトを書いた人もいるでしょう。あるいはこのようなものも。時には上手くいきますが、カスタムマークダウンパーサーを書かなければならないこともあります。当然、これは理想的な方法ではありません。そこで昨年6月、この問題を解決するためにfunction callingをリリースしました。
JSONスキーマを使ってモデルが呼び出せるツールを定義する方法を示しました。関数のスキーマを定義すると、モデルはそれに従ったJSONを出力します。しかし、function callingは完璧ではありませんでした。モデルが末尾のカンマを付けてしまうなど、無効なJSONを出力することがありました。誰でも経験したことがあるでしょう。
そこで昨年11月のDevDayでJSONモードをリリースしました。JSONモードは有効なJSON出力を保証します。構文エラーを見ることはもうありません。しかしこれでもまだ十分ではありませんでした。モデルは間違った型を出力することがありました。ここに示すように、floatの代わりに文字列を出力したり、パラメータを勝手に追加したりしました。信頼できる出力を得るためのこのいたちごっこは本当にストレスフルでした。
根本的に、AIアプリケーションには信頼できる出力が必要です。そこで、これらの問題を一度に解決するため、8月にAPIに構造化出力を導入しました。詳しくはMichelleが説明します。
ありがとうございます。構造化出力は、モデルが生成する出力が開発者の皆さんが提供するJSONスキーマと完全に一致することを保証する新機能です。
構造化出力は、モデルにスキーマの使用を提案することと強制することの違いと考えることができます。もう丁寧にお願いする必要はなく、必要なJSONスキーマを提供するだけでよいのです。そして、もしこれが問題を最終的に解決する正しい解決策だったのなら、なぜここまで時間がかかったのかと疑問に思われるかもしれません。答えは2つあります。
まず、function callingは多くの場合この機能に対して適切な抽象化であると私たちは考えています。そして、大規模な推論時に出力を制約しながらパフォーマンスの高い解決策を構築するのに少し時間がかかりました。後ほど、これを実現するために行ったエンジニアリングとリサーチについてお話ししますが、まずは使い方を見ていきましょう。
APIでは構造化出力は2つのモードで利用できます。1つ目はAttyが示したfunction callingです。おそらくすでにお馴染みのことでしょう。この機能によりモデルはツール呼び出しのパラメータを生成でき、開発者はLLMをアプリケーションの機能と接続できます。
2つ目はresponse formatパラメータです。このパラメータは、モデルが関数呼び出しを出力するのではなく、直接ユーザーに応答する場合に便利です。まずはfunction callingから始めましょう。以前使ったことがある方は、ここに示すリクエストの形式にお馴染みでしょう。
ツールセクションでJSONスキーマを提供し、関数のパラメータがあります。これはモデルにツールの呼び出し方を伝えます。この場合、関数はget weatherで2つのパラメータ、locationとunitを受け取ります。locationは文字列型で、unitも文字列型ですが、enumパラメータで指定された値に制限されています。
APIでこのような関数を提供すると、システムはモデルにこの仕様を示し、適切な場合にツール呼び出しを生成するために使用します。ここで構造化出力を有効にするのは非常に簡単です。1行のコードでstrictをtrueに設定するだけで、構造化出力が有効になります。
これにより提供されたスキーマが使用され、モデルの応答が常にそれに従うことが保証されます。これがfunction callingでの構造化出力です。では、プレイグラウンドで簡単な例を見てみましょう。その前に、私が構築しているスタートアップについてお話しします。
実はAIグラスの製品を作っています。今とても注目を集めています。非常に未来的で、OpenAI APIを基盤としています。このグラスにはステムにスピーカーが組み込まれており、アシスタントの回答を読み上げることができ、グラスにはARスクリーンもあります。実は、チームが受注した注文やその配送状況について質問に答えるための内部管理ダッシュボードを作りたいと思っています。
注文情報を含むデータベースはすでにあり、このアシスタントをその情報に接続したいと思います。では、プレイグラウンドで始めましょう。実はSQLデータベースに問い合わせるための関数をすでに作成してあります。見てみましょう。これが私の関数queryです。
期待される通りのプロパティを持っています。まずtable nameがあり、ordersという1つのテーブル名しかサポートしていません。次にテーブルのすべての列があります。これらは直接データベースから取得したものです。そして問い合わせをサポートする条件があります。
演算子があることがわかります。データベースは等号、大なり、小なり、不等号をサポートしています。そしてorder byがあります。データベースに期待される通りのものです。アシスタントを使用する際は、今日の日付を設定してシステムメッセージを送り、役立つようにします。そしてユーザーが9月1日以降に出荷された注文をすべて見つけたいと質問してきます。
早速試してみましょう。関数呼び出しが返ってきて、良さそうに見えます。ロジックは正しいです。shipped atが9月1日以上かをチェックしています。しかし、注意深い開発者ならアプリケーションが破綻する可能性があることに気付くでしょう。
その理由は、greater than or equal to演算子を使用しているからです。しかし、私たちのORMは何らかの理由でこれらの演算子しかサポートしておらず、greater than or equalは動作しません。構造化出力ではstrictをtrueに設定でき、これによりモデルは提供されたものだけを使用するよう制約されます。
保存して再試行してみましょう。素晴らしい!今回はgreater than演算子を使用し、モデルは9月1日以上を確認する代わりに8月の最終日より大きいかどうかをチェックするロジックを行っています。これを通じて、構造化出力によってアプリケーションの一連のエラーを排除できることがわかります。
では、response formatsに戻りましょう。以前、モデルにJSONで応答させたい場合はJSONモードを使用していました。AIグラスのスタートアップに戻ると、ステムにスピーカーがあるので、アシスタントの応答の一部を音声で読み上げ、レンズには要約版を表示したいと思います。
構造化出力以前は、これらの指示をシステムメッセージに入れてJSONモードを使用していました。これはかなり良かったです。常にJSONが返ってきました。しかし、ユーザーが特定のものを要求した場合、余分なキーが返ってきたり、モデルが間違った型を使用したりすることがありました。
構造化出力では、これらの指示をresponse formatパラメータに移動できます。ここに示すように。descriptionを使ってモデルに応答方法を説明し、voiceoverとdisplayパラメータを使用します。これによりモデルは常にフォーマットに従い、これら2つのキーを使用します。これも試してみましょう。
別のタブを用意してスキーマを作成しました。見てみましょう。voiceoverは最初の文字列プロパティで、TTSで読み上げられることをアシスタントに伝えています。また、数字や頭字語は完全に書き出すように指示しています。次にdisplayプロパティがあります。グラスには多くのスペースがないので、5単語に制限しています。
これをプレイグラウンドに入れてみましょう。右側にresponse formatオプションがあります。スキーマを貼り付けると、strictがtrueに設定され、構造化出力が有効になっています。では、典型的なクエリでグラスをテストしてみましょう。「キリンの身長はどれくらい?」と聞いてみます。実行すると、求めた形式で出力が得られます。
TTSモデルが読みやすいように単語がスペルアウトされたvoiceoverパラメータがあります。また、グラスにぴったり収まる4単語のdisplayフィールドもあります。これがresponse formatsでの構造化出力です。これは簡単なデモでしたが、Attyがもっと興味深いものを用意しています。
はい。機能を見てきましたが、より興味深いデモと、アプリケーションでの構造化出力の使用方法を見ていきましょう。架空の会社Convexを想像してみましょう。ConvexはAI駆動の採用ツールです。採用担当者が求人を作成し、推薦を提出し、面接をスケジュールすることができます。
裏側では、ConvexはNodeとReactのアプリケーションで、履歴書から情報を抽出するためにresponse formatsを使用し、候補者データに対するクエリを実行するためにfunction callingを使用しています。実際に見てみましょう。OpenAIで募集しているMLエンジニアのポジションの求人を作成しました。
求人の説明、採用マネージャー、そしてすでに応募のあった候補者が表示されています。ステージに上がる前に、有望な候補者から履歴書を受け取りました。見てみましょう。Gregはストライプでの経験があり、AIの分野でも少し経験があるようです。
Pythonでのコーディングを含む多くのスキルを持っているので、有望な候補者のように見えます。フォルダに入れてみましょう。候補者追加ボタンをクリックして、Gregの履歴書を選択すると、PDFから抽出した情報がリアルタイムで表示されるのがわかります。
モデルの応答を裏側で見てみましょう。名前、役職、場所、連絡先情報、職歴など、すべての情報が履歴書から抽出されたJSONオブジェクトが見えます。コードを見て、どのように動作しているか確認してみましょう。裏側では、resumeというresponse formatを使用しています。
先ほど見た名前、役職、場所などのフィールドがあります。特に職歴については、配列を使用していることに注目してください。構造化出力はJSONスキーマのサブセットをサポートしており、配列も含まれています。これによりモデルは複数の職歴を出力できます。部屋のJavaScript開発者の方々は、スキーマ定義にZodライブラリを使用していることにも気付くでしょう。
Zodは素晴らしいです。コードでスキーマを定義し、実行時の型安全性を得ることができます。OpenAIのnode SDKはZodをネイティブにサポートしています。Zodでスキーマを定義でき、モデルが応答を開始すると、その応答をZodオブジェクトにパースし返すことができ、ストリーミングもサポートしています。同様に、OpenAIのPython SDKもPydanticをネイティブにサポートしています。
では、フォーマットにもう1つフィールドを追加してみましょう。GitHubのユーザー名を追加します。保存してページを更新し、もう一度追加してみましょう。素晴らしい、モデルが応答し、GitHubのユーザー名も抽出されているのがわかります。これが1つ目のデモでした。response formatsを使用して非構造化データから情報を抽出する機能です。
次に、function callingでの構造化出力の使用方法を見てみましょう。すべて表示をクリックして候補者分析画面を開きます。データを分析するための便利なAIアシスタントがあります。候補者はサンフランシスコ、ニューヨーク、オースティンなど全国から応募していますが、このポジションはサンフランシスコオフィスでの採用なので、候補者をフィルタリングしてみましょう。
「サンフランシスコを拠点とする候補者にフィルタリングして」と入力してみましょう。モデルがfind candidates関数を呼び出し、criteriaフィールドに場所がサンフランシスコという条件を指定しているのがわかります。これがバックエンドAPIにヒットし、フィルタリングされたサンフランシスコ在住の候補者リストが返されました。
左側のUIもこれらの候補者を反映して更新されているのに気付きます。このように、function callingを使用してアプリケーションのUIを制御することができます。裏側を見てみましょう。コードでfind candidatesというツールまたは関数を定義しています。find candidatesは条件のリストを含むスキーマを持ち、各条件にはフィールド(役職、場所など)と、フィルタリングする値があります。
これがfunction callingでの構造化出力の簡単な例でした。もっと難しいことをしてみましょう。「これらの候補者を経験年数でグラフ化して」と言ってみましょう。モデルがUIを生成しているのがわかります。ヘッダー付きのカードと、すべての候補者とその経験年数を示す棒グラフ、そして行のあるテーブルです。
これは事前に構築されたUIではありません。実際にモデルが動的にReactコンポーネントを組み合わせています。スキーマも見てみましょう。最上位のプロパティがcomponentで、cardと指定されており、cardはヘッダー、棒グラフなどを含む子要素のリストを持っています。裏側でスキーマがどのように定義されているかを見てみましょう。
generate UIというツールがあります。generate UIにはcomponentというプロパティが1つあります。componentはcard、header、bar chartなどのany ofです。そしてこれらの各スキーマは後のdefsパラメータで定義されています。これは構造化出力の非常に興味深い機能で、defsを使用してスキーマを1つの場所で定義し、複数回使用することができ、再帰的なスキーマ定義も使用できます。
カードスキーマにはchildrenがあり、これは再びcomponentを参照していることがわかります。このように、コンポーネントは子コンポーネントのリストを持つことができ、構造化出力はこれを問題なく処理します。素晴らしい、これで候補者を経験年数でソートできたので、面接をスケジュールしましょう。
「経験年数で上位3名の候補者とMichelleとOlivierの面接をスケジュールして」と言ってみましょう。これは、まずMichelleとOlivierのカレンダーで空き状況を確認します。良い時間帯をいくつか選んだら面接をスケジュールし、最後に面接が予約されたことを候補者にメールで通知します。
エンターを押してみましょう。素晴らしい。モデルがfetch availability APIを呼び出してデータを取得し、次にscheduled interviews APIを呼び出してそれが成功し、最後に各候補者にカスタマイズされたメールを送信するsend emailsを呼び出し、それも成功しました。これはfunction callingを使用した複数ステップのワークフローの例です。
各ステップが構造化出力の恩恵を受けています。この機能がなければ、これらのステップのいずれかが失敗すると、ワークフロー全体が失敗していたでしょう。本番環境で、これらのそれぞれに1%のエラー率があった場合、ワークフロー全体のエラー率は約3%になります。エージェントワークフローの信頼性にとって構造化出力がいかに重要かがわかります。
これが実際のアプリケーションでの構造化出力の使用方法のデモでした。response formatsを使用して非構造化データから情報を抽出し、function callingを使用してUIを生成し、最後に100%の信頼性でエージェントワークフローを構築することができます。これがどのように動作するのか、裏側について説明するためにMichelleに代わります。
ありがとうございます。とても素晴らしいですね。後でGregと面接するのが楽しみです。では、構造化出力が裏側でどのように動作するのか見ていきましょう。3つのことについて説明します。エンジニアリングの実装、モデルのフォーマット追従能力を向上させるために行ったリサーチ、そしてこの機能をリリースするために行った興味深いAPI設計の決定について説明します。
これをリリースするために、リサーチとエンジニアリングを組み合わせたアプローチを取り、包括的な解決策を作ることにしました。どちらか一方だけでも良い製品になりますが、組み合わせることで相乗効果が得られます。構造化出力は単にモデルに異なるプロンプトを与えること以上のものです。
開発者にとって興味深いトレードオフを生み出すので、その選択について説明したいと思います。エンジニアリング面では、constrained decodingと呼ばれるアプローチを取り、モデルが開発者が提供したスキーマに必ず従うようにしました。constrained decodingには3つのコンポーネントがあり、これから説明します。
1つ目はLLM推論の仕組みです。次にトークンマスキングについて説明し、最後に私たちがサポートするJSONスキーマのサブセット、つまりすべての文法について説明します。まずはLLM推論から始めましょう。LLMはトークンで動作します。トークンは大規模言語モデルの語彙です。
これはモデルが出力または生成できるすべてのラベルです。小さな語彙を持つモデルの簡単な例を見てみましょう。これはAIモデルの古典的な例です。手書きの3を認識し、どの数字かを判定するモデルを訓練したいとします。
このモデルの語彙は0から9までの10個のラベルだけです。これが実際のモデルの動作です。まず入力をコンピュータが理解できる表現に変換します。この場合、手書きの3のすべてのピクセルを取得してモデルに入力します。
次にモデルの内部層があり、最後にモデルは10個の値を生成します。これらは0から9までのラベルの予測です。ここで数字3が97%の確率を持っているので、この数字は3である可能性が高いことがわかります。これは大規模言語モデルの推論の動作を大まかに示しています。
0から9までの数字を予測する代わりに、大規模言語モデルは言語を予測します。これを行うために、言語を表すラベルを使用し、その最も良い方法の1つはトークンを使用することです。トークンは単語や単語の断片のようなものです。ここに英語のテキストがトークンに分割されているのがわかります。
これは実際にブログ記事からのテキストで、トークンの境界は様々です。「the」のように1つの単語全体の場合もあれば、「struct」のように単語の断片の場合もあります。これらのトークンが大規模言語モデルの語彙を構成し、モデルはいつでもこれらのいずれかを生成できます。これらはすべて有効です。
これが制約のない復号化で、生成されるものに制約がありません。画面に表示されているのは、実際のGPT-4トークナイザーのトークンの一部です。感嘆符のような個別の文字から、下部の「conveyer」のような完全な単語まで、あらゆる種類の品詞があります。
では、このスキーマを使用して出力を生成する構造化出力を使用しているとしましょう。このスキーマには1つのプロパティvalueがあり、数値型です。すでに生成途中で、中括弧、空白、そしてvalueまで生成されています。デフォルトでは、語彙のどのトークンもサンプリングできます。
つまり、文字列や真偽値も可能です。しかし真偽値を生成するとスキーマに合致しないので、42のような数値は有効です。これは、次に生成できるトークンを制限する必要があり、モデルの語彙全体を使用できないことを示しています。これを行うためにトークンマスキングという技術を使用し、サンプリングの最後でトークンを制約します。
数字の例に戻りましょう。トークンマスキングの例を示します。これらの数字に関する追加情報があるとします。何らかの理由ですべてが素数だとわかっています。そのため0、1、4、6など素数でない数字は復号化したくありません。これが実際の方法です。
これらの予測をマスクアウトします。予測を生成した後、フォワードパスで指定されていないラベルを削除し、有効なトークンからのみサンプリングするようにします。これはマスキングと呼ばれ、生成したくない値を効果的に無視します。
大規模言語モデルは自己回帰的で、1つずつトークンを生成します。トークンをサンプリングするたびに、その出力は次の推論ステップの入力として戻されます。これは、スキーマに従ってJSONをサンプリングする際、リクエストの開始時に1回だけでなく、推論の各ステップでマスクされたトークンを更新する必要があることを意味します。
理解しづらい場合は、この例を見てください。最初に中括弧トークンを許可し、サンプリングした後、ステップ2ではマスクに中括弧が表示されなくなっているのがわかります。このようにステップごとにマスクを更新する必要があります。これは推論の各ステップで発生するため、この操作は超高速である必要があります。
皆さんのアプリケーションが動作する規模では、処理を極めて高速に保ち、推論をできるだけ速くする必要があります。おそらくご存知の通り、トークンのサンプリングはGPUで行われます。これはバッチ単位で行われます。各バッチに対して、そのバッチ内のすべての確率を計算し、トークンマスクを適用し、最終的な確率分布からサンプリングします。
そのサンプリングがオレンジの部分です。ご覧の通り、確率の計算とマスクの決定は並列で行うことができ、マスクの決定をCPUで行うことで貴重なGPUリソースを解放できます。これは処理を高速に保つのに非常に役立ちます。また、マスクは確率の計算とほぼ同じ時間で完了する必要があることにも注目してください。
これはトークン間の時間としても知られ、モデルのサイズによって異なります。そのため、これを10ミリ秒以下に抑える必要があります。この問題に対する単純な解決策は、推論の各ステップで現在の状態から有効なトークンを決定することですが、先ほど述べたように10ミリ秒という制限があるため、その予算内に収めるのは難しいでしょう。
そこで、できるだけ多くの作業を事前に計算し、サンプリング時にマスク計算を単なる検索のようにし、多くの作業を必要としないようにしたいと考えました。SQLデータベースでクエリを高速化するためにインデックスを構築できるように、提供される特定のJSONスキーマに対してインデックスを構築し、推論時のマスクの取得を非常に高速にすることができます。
JSONスキーマからこのインデックスを生成するには実際に多くの作業が必要です。まず、JSONスキーマを文法に変換します。文法は言語の形式的な仕様です。文法ができたら、パーサーを作成します。パーサーは文字列を受け取り、それがこの言語の一部かどうかを確認するプログラムです。
パーサーは、JSONブロブを受け取り、それがスキーマに一致するかどうかを告げるプログラムと考えることができます。最後に、パーサーができたら、語彙内のすべてのトークンとパーサーで可能なすべての状態を反復処理し、どのトークンが有効かを決定します。
この作業によりインデックスを構築できます。インデックスは実際にはツリーで、O(1)での検索を可能にするプレフィックスベースのデータ構造です。推論時にはパーサーを実行し、状態を構築し、毎回このツリーを走査してリーフを見つけます。リーフノードが次のステップのマスクを教えてくれます。
このインデックスの生成は計算的に非常にコストがかかります。可能なすべての状態を処理する必要があるからです。そのため1回だけ行い、後で高速な検索のためにキャッシュします。これが構造化出力の最初のクエリに少し時間がかかる理由で、通常10秒以内です。その後のクエリは通常通りの速さになります。
最後に、構造化出力でサポートする文法の種類について説明しましょう。オープンソースコミュニティでは、トークンマスクを決定するために正規表現を使用するアプローチが一般的です。このアプローチは単純なスキーマや深さが制限されたスキーマでは非常にうまく機能します。例えば、以前のvalueスキーマの正規表現を想像できます。
実際には正規表現で実装可能で、ここに示すようなものになります。この正規表現は期待通りのものです。まず中括弧があり、次に空白、valueがあり、最後に数値型の正規表現があります。しかし、実際にはJSONスキーマのすべての表現力を正規表現で実装することはできません。
過去の検索に関する情報を保存するために必要なメモリが欠けているからです。なぜそれが重要なのか説明しましょう。ブログ記事からの再帰的なスキーマの例を示します。このスキーマは生成的UIに役立ち、Attyが先ほど示したものと似ています。
各コンポーネントは子要素のリストを持ち、それらも最上位のスキーマに一致する必要があります。ここでスキーマはrefを使用して親を参照しているのがわかります。再帰的なネストの可能性があるため、これを検証する正規表現を実装する方法はありません。
これは正規表現の根本的な制限で、過去の出力に関する情報をエンコードするための任意のメモリがないためです。より理解しやすくするため、非常に単純な言語の例を示します。私たちの言語は、開き括弧と閉じ括弧が一致するすべての文字列として定義され、ここですべての括弧が一致している簡単な例を示しています。
説明は非常に簡単ですが、正規表現では実装できません。なぜそうなのか示すため、ここに2つの試みがあります。1つ目は非常に単純で、任意の開き括弧と閉じ括弧を許可するだけです。しかしこれがうまくいかない理由は明らかで、1つの開き括弧と2つの閉じ括弧を持つことができます。
そのため私たちの言語を検証せず、以前に何が起こったかのメモリが欠けています。2つ目の試みはより複雑で、3層までのネストで機能しますが、それを超えると正規表現は適切に動作しません。これは単なる例ですが、再帰的で深くネストされたスキーマは開発者にとってしばしば重要だと考えているため、それらをサポートする方法を見つけたいと考えました。
スタックを追加することでこれを実現でき、過去の出力に関する情報をエンコードするために必要なメモリを得ることができます。このスタックは、これまでに何個の開き括弧を見たかなどを追跡できます。このアプローチはCFG(文脈自由文法)アプローチとして知られています。実装は少し難しいですが、はるかに表現力が高くなります。
これが私たちが選んだアプローチで、推論用のツリーの構築に時間がかかる理由です。また、再帰と深いネストをサポートできる理由でもあります。これが構造化出力のエンジニアリング面です。LLM推論の仕組み、トークンマスキングが有用な構成要素である理由、そしてこれらすべてがサポートするJSONスキーマの広範なサブセットでどのように組み合わさるかについて説明しました。
この実装は開発者にとって最良のトレードオフをもたらすと考えています。推論をできるだけ高速に保ち、JSONスキーマの非常に広いサブセットをサポートしました。開発中に最初のリクエストで少し長く待つというトレードオフは、ほとんどの開発者にとって価値があると考えています。
では、リサーチによって実際にモデルをどのように改善し、構造化出力で動作するようにしたのか説明しましょう。
素晴らしい。
エンジニアリングの背景にある構造化出力について説明しました。次にリサーチ面について説明しましょう。リサーチ面では、指定された通りにフォーマットに従うことがはるかに優れたモデルをリリースしたいと考えました。
モデルの出力を有効な出力に制約するだけでは十分ではありません。それらの出力がモデルにとって分布外または低確率である場合、モデルはしばしば不安定に振る舞うからです。以前のresponse formatsで訓練されていない古いモデルで、無限の改行問題として見られたかもしれません。
モデルの自然な傾向はJSONで動作するテキストを生成することでしたが、JSONを出力するよう強制されていたため、唯一の十分に確率の高い有効なトークンは改行文字でした。そのためモデルは、最大トークン数に達するまで1つずつ改行トークンを生成し続けました。この問題を避けるため、モデルに特にJSONスキーマを理解させ、特に複雑なスキーマをより良く理解できるよう訓練しました。
しかし、モデルがスキーマを理解するだけでは十分ではありません。そのフィールドの品質を知る必要があります。キーの背後には意味的な意味があります。例えば、このアクションアイテムスキーマを考えてみましょう。各アクションアイテムには説明、期日、担当者があります。良い出力を生成するには、descriptionが文字列で ownerが文字列であることを知るだけでは十分ではありません。
モデルはどのような文字列がdescriptionに入り、どのような文字列がownerに入るのかを知る必要があります。そこで、モデルがこれを得意とするよう、ネストされたスキーマを含む多くの複雑なスキーマで訓練しました。訓練プロセスの結果を示します。このグラフは、x軸にモデル、y軸に私たちの評価の1つでの精度を示しています。
左から最初の3つの棒グラフはGPT-4、GPT-4 turbo、そして元のGPT-4oです。精度は昨年の約26%から86%に向上したことがわかります。最後に、最新モデルの結果を示します。オレンジの棒グラフはプロンプトのみのモデルの結果で、85%の精度です。
新しく訓練されたresponse formatを追加すると、黄色の棒グラフで示すように精度は93%に向上します。これはかなり良くなりましたが、まだ100%ではありません。そこで、Michelleが先ほど説明した通り、constrained decodingを有効にすることで、緑の棒グラフに示すように完璧なスコアを得ることができます。このように、モデルを改善するリサーチと、constrained decodingを実装するエンジニアリングを組み合わせることで、可能な限り最良の結果が得られます。
最後に、この機能を作る際に行った興味深い設計のトレードオフについて説明しましょう。追加のプロパティ、必須プロパティ、プロパティの順序について説明します。まず追加のプロパティから始めましょう。私たちが行わなければならなかった議論を呼んだAPI設計の決定の1つは、スキーマで定義されていないプロパティをどうするかでした。
デフォルトでは、JSONスキーマは常に追加のプロパティを許可します。これは一般的に開発者が期待する動作ではありません。このような例を想像できます。get weather関数が2つの引数を受け取る場合、LLMが追加の引数を生成すると実行時エラーが発生します。
そこでOpenAI APIではデフォルトで追加のプロパティを禁止することにしました。ただし、これはAPIのデフォルトがJSONスキーマのデフォルトと異なることを意味し、特に開発者がアプリケーションの他の場所から事前定義されたスキーマを持っている場合には良くありません。一般的に私たちのAPI設計の原則の1つは、暗黙的よりも明示的であることを好みます。
そこで、開発者がスキーマでadditional properties falseを必ず指定しなければならないことにしました。これによりAPIの使用が少し難しくなり、このプロパティを毎回設定する必要がありますが、開発者との期待値をより良く設定できます。次に必須プロパティについて説明しましょう。デフォルトではJSONスキーマですべてのプロパティはオプションです。
これもまた、私たち開発者が期待する動作ではありません。先ほどのget weather関数の例に戻ると、LLMがパラメータの1つをスキップすることを決めた場合、再び実行時エラーが発生します。そこでこの機能をより直感的にするため、すべてのプロパティをデフォルトで必須とし、再び期待値を設定するため、開発者にrequiredディレクティブでこれを指定することを要求しました。
オプションのパラメータに対する回避策として、それらをnullableにすることができます。これにより、パフォーマンスのトレードオフを伴う両方の世界の良いところを得られます。最後にプロパティの順序について説明しましょう。デフォルトではJSONスキーマには順序制約がなく、LLMはどの順序でもプロパティを生成できます。
しかしLLMのコンテキストでは、順序は本当に重要です。プロパティの厳密な順序付けは非常に有用です。例えば、スキーマの後のフィールドの値を条件付けるために、より早いフィールドを使用できます。スキーマに思考連鎖フィールドを追加するような場合です。モデルはまず思考連鎖を生成し、その思考を説明し、その後で答えを生成します。
これはしばしば答えの品質を大幅に向上させます。このユースケースをサポートするため、スキーマで定義した順序と同じ順序でフィールドを生成することにしました。以上がAPI設計です。APIが良いデフォルトを持ちながら、制約を開発者に透明にすることを確認したかったのです。まとめとしてMichelleに代わります。
素晴らしいですね。エンジニアリングとリサーチの両方のアプローチがこの製品の意味のある改善をもたらしましたが、組み合わせることで最高の結果が得られます。これがOpenAI APIでの私たちの考え方です。開発者にとって最も使いやすいAPIを作るために、エンジニアリングとリサーチの作業を私たちの側で行いたいと考えています。
構造化出力のような問題を皆さんのために解決し、アプリケーションで最も重要なことに時間を費やせるようにしたいと考えています。構造化出力はAIアプリケーションの可能性を完全に引き出すための最後のパズルピースだと考えています。データ抽出は信頼できるものになりました。関数呼び出しには必要なパラメータがあります。
そして最後に、エージェントのフローが100%の確率で動作するようになりました。8月に構造化出力をリリースして以来、大企業からスタートアップまで、皆さんのような開発者が素晴らしいアプリを構築してきました!Shopifyのような企業が構造化出力を使用してハルシネーションを減らし、アプリケーションの信頼性を向上させています。
また、皆さんの興奮の声も聞こえています。構造化出力が開発者の多くの問題を解決するのを見るのは素晴らしいことです。これが私たちがやっていることの理由です。OpenAIの使命は、すべての人のために安全なAGIを構築することです。私たちがAPIを構築するのは、開発者の皆さんと協力することがその使命を達成するために重要だと考えているからです。
皆さんは誰よりも早く未来を見ており、一緒に世界の隅々までこの技術を広げることができます。仲間のエンジニアとして、皆さんに貢献できることを幸運に思います。一緒に構築していただきありがとうございます。何を実現されるのか楽しみにしています。ありがとうございました。

コメント

タイトルとURLをコピーしました