構造化された出力

まず、問題の形を理解しましょう。構造化された出力とは、コードが直接読み取ることができるデータをモデルに返させる実践のことです。手でパースしなければならない段落ではなく、分岐できるフィールドです。この章は、これを信頼できるようにする方法についてです。
これまで、モデルはテキストを返してくれていました。人間が読むときはいいのですが、ソフトウェアは段落ではあまり何もできません。コードでモデルの答えに対応するには、データとして必要です。読み取れるフィールド、分岐できる値、保存できるレコード。
この転換は小さいですが、下流のすべてを変えます。モデルが「顧客は不満に見える」という文の代わりに { "sentiment": "negative" } を返すようになると、コードはチケットをルーティング、ダッシュボードを更新、またはアラートを送信できます。興味深い部分は、テキストを予測するだけのモデルにきれいなデータを作らせる方法で、その答えはあなたが知らなかったレバーを明かします。
JSONをリクエストする
最初の本能はプロンプトで「**JSON**で返してください」とお願いすることです。JSON は、ソフトウェアがデータを渡すのに使う平文形式で、中括弧の中のフィールドと値として書きます。試してみるとほとんどの場合うまくいきます。これが罠です。モデルは相変わらずテキストを予測しているだけで、時々その予測されたテキストはコードフェンス、またはデータの前の親切な「もちろん、どうぞ!」、またはそれの後ろのトレーリングコメントになります。これらのいずれもがJSONを読むコードを壊します。丁寧に頼むのはJSONへの予測をバイアスさせます。それを強制しません。
だからプロバイダーは本当のレバーをあなたに与えます。response_formatでJSONモードをオンにするのは、尋ねること以上のことをします。それはモデルを制限して、構文が有効なJSONとして返ってくるようにします。
import json
response = client.chat.completions.create(
model=MODEL,
messages=[
{"role": "system", "content": '都市と気温を抽出してください。JSON で返してください: { "city": string, "tempC": number }。'},
{"role": "user", "content": "東京は今約18度です。"},
],
response_format={"type": "json_object"}, # 出力を有効なJSON構文に制限
)
data = json.loads(response.choices[0].message.content)
print(data["city"], data["tempC"]) # "東京" 18response_format={"type": "json_object"} は出力を有効なJSON構文にするので、json.loads は迷ったテキストで窒息しません。あなたはまだプロンプトで必要なフィールドを記述する必要があります。JSONモードは有効なJSONを約束しますが、特定のフィールドを約束しません。「JSON ではない」を排除します。「間違った形のJSON」ではなく。それでも約束できないこと:返信が途中で切れた場合、半分のJSONオブジェクトを受け取ることができるので、信頼できる構文ではなく、すべてのケースでの強い保証ではありません。
スキーマで形を固定する
JSONモードは構文を有効にしますが、モデルはまだフィールドの名前を変更、削除、または異なり方で入れ子にする可能性があります。その余裕を削除するために、**スキーマ**をそれに与えます。出力が持つ必要があるフィールドと型の正確な説明。モデルが埋める必要がある形。strict: true でその形に一致することが保証されます。
import json
response = client.chat.completions.create(
model=MODEL,
messages=[
{"role": "system", "content": "メッセージから連絡先の詳細を抽出してください。"},
{"role": "user", "content": "こんにちは。田中太郎です。[email protected] で連絡してください。"},
],
response_format={
"type": "json_schema",
"json_schema": {
"name": "contact",
"strict": True, # スキーマを正確に強制
"schema": {
"type": "object",
"properties": {
"name": {"type": "string"},
"email": {"type": "string"},
},
"required": ["name", "email"],
"additionalProperties": False,
},
},
},
)
contact = json.loads(response.choices[0].message.content)
# {"name": "田中太郎", "email": "[email protected]"}モデルがトークンを予測するだけのときに、スキーマがどのように強い保証になるのでしょうか?プロバイダーが予測することを許可されているトークンを制限するからです。各ステップでモデルはまだすべての可能な次のトークンのランクを付けますが、システムはスキーマを破るものを削除し、モデルは残ったものから選択します。
スキーマが次のフィールドが email である必要があると言う場合、他のフィールドを開始するトークンはモデルが選択する前にテーブルから削除されます。形の外側のトークンが削除されるので、モデルは形の外側に漂流できません。
知る価値のあるより柔らかい効果があります。選択したフィールド名は、値を予測するとき、モデルが読む一部になるので、明確な名前はより良い答えをガイドします。tempC を埋めるように求められると、モデルはセルシウス数を支持しがち。value を埋めるように求められると、それは進むのが遠いです。フィールドを明確に名前付けすることは、部分的には命令、部分的にはスキーマです。
とにかく検証する
スキーマは形を制限しますが、とにかく外の世界からのデータとして出力を扱ってください。形は有効で、内容は間違っています。実は誤字の抽出されたメール。モデルが推測した数字。入力が含まれていないため、フィールドは空白のままです。そして呼び出しは max_tokens で切られて、切り詰められたJSONとして到着するなど、通常の方法で失敗する可能性があります。防御的にパースしてください。有効な形は正しい値ではありません。
import json
def parse_contact(raw):
try:
data = json.loads(raw)
if not data.get("name") or not data.get("email"):
return None # 現在でも空
return data
except json.JSONDecodeError:
return None # 有効なJSONではない(例えば、切り詰められた返信)
contact = parse_contact(response.choices[0].message.content)
if not contact:
pass # 失敗を処理する:リトライ、再度尋ねる、または親切なエラーを表示これはLLMの仕組みから**幻覚**の教訓は新しい衣装で。形が正しいことは値が正しいことを意味しません。スキーマは name フィールドを取得することを保証します。それは名前が正しいことを保証しません。try/except をパースの周りに加えて、信頼する前に値をチェックしてください。これは安い保険です。返信は形成されて、それでも間違っています。
2つのパターン:分類と抽出
ほとんどの構造化出力の作業は、2つの形のいずれかです。
**分類**は入力を固定されたラベルのセットの1つにソートします。スキーマの enum は、出力を正確にそれらのラベルに制限します。同じトークンプルーニングのトリックを使用します。ラベルポジションで、許可された値のみが予測される可能性があります。だから、モデルはリストにない型を作成することはできません。
# 分類のためのスキーマフラグメント
{"type": "string", "enum": ["billing", "technical", "general"]}抽出は自由なテキストから特定のフィールドを引き出します。名前、日付、金額、商品名のリスト。一緒に、分類と抽出は現実のAI機能の大きなシェアをカバーします。サポートチケットのルーティング、コンテンツタグ付け、メールをレコードに変える、領収書を読む。両方は、ぼやけたテキストジェネレーターを依頼できるコンポーネントに変わります。
実際の使用
メッシーなサポートメールを構造化チケットに変わる。分類と抽出を1つのスキーマに結合:
import json
response = client.chat.completions.create(
model=MODEL,
messages=[
{"role": "system", "content": "サポートメールをチケットに変わってください。"},
{"role": "user", "content": email_text},
],
response_format={
"type": "json_schema",
"json_schema": {
"name": "ticket",
"strict": True,
"schema": {
"type": "object",
"properties": {
"category": {"type": "string", "enum": ["billing", "technical", "general"]},
"urgency": {"type": "string", "enum": ["low", "medium", "high"]},
"summary": {"type": "string"},
},
"required": ["category", "urgency", "summary"],
"additionalProperties": False,
},
},
},
)
ticket = json.loads(response.choices[0].message.content)
# {"category": "billing", "urgency": "high", "summary": "先月の購読で二重課金されました。"}1つのコールは、メールを2つの方法で分類して、要約を抽出し、コードがルートして保存できるレコードを返します。形は保証されます。値はまだ確認する価値があります。これまで、すべてがテキストを流入して流出させてきました。次は、モデルに完全に異なる感覚を与えます。埋め込みでは、テキストはあなた自身のドキュメントと機能する検索と財団の意味で比較できる数字になります。

