ag2-structured-output について
AG2 の Agent の返信を、プレーンテキストではなく検証済みの型付き値として扱う方法を解説するガイドです。スキーマの選択、独自パーサー、プロンプトによる対応、検証失敗時の再試行を扱います。
主な機能
- 01Pydantic、dataclass、型付きスキーマ
- 02独自のレスポンス検証
- 03プロンプトによるプロバイダー対応
- 04検証失敗時の自動再試行
- 05ターン単位のスキーマ上書き
活用シーン
- 01後続コード向けの型付きフィールド抽出
- 02定義済みスキーマによる分類や採点
- 03ネイティブ構造化出力非対応時の返信解析
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ ag2ai/ ag2-skills --skill ag2-structured-outputインストール後、Claude Code で/ag2-structured-outputを入力して読み込まれたか確認します。
同梱ファイルassets/recipe_builder.py
詳細
構造化出力
使用する場面
- 文字列ではなく、Pydantic モデル、データクラス、辞書、プリミティブ、またはユニオンを返してほしい場合。
- 分類、抽出、スコアリング、正規化など、後続のコードで応答を解析する処理を行う場合。
- 検証に失敗したときの自動再試行が必要な場合。
60 秒でできるレシピ
from pydantic import BaseModel, Field
from typing import Annotated
from ag2 import Agent
from ag2.config import OpenAIConfig
class TicketTriage(BaseModel):
category: Annotated[str, Field(description="e.g. billing, bug, account_access")]
urgency: Annotated[str, Field(description="low, medium, or high")]
summary_one_line: Annotated[str, Field(description="Max 120 characters", max_length=120)]
agent = Agent(
"triage",
prompt="You triage support messages. Be conservative with urgency.",
config=OpenAIConfig(model="gpt-4o-mini"),
response_schema=TicketTriage,
)
reply = await agent.ask("I was charged twice and can't export reports. Quarter close blocked.")
triage = await reply.content() # → typed TicketTriage
print(triage.category, triage.urgency)
reply.body は引き続きモデルの生テキストです。await reply.content() を実行すると検証が行われ、解析済みの値が返されます。検証に失敗すると、content() は例外(例:pydantic.ValidationError)を発生させます。
渡せるスキーマの型
| 型 | 得られるもの |
|---|---|
プリミティブ(int、float、bool) | 値そのもの。フレームワークが API 用に {"data": ...} でラップします |
dataclass | データクラスのインスタンス |
Pydantic BaseModel | モデルのインスタンス |
ユニオン(int | str、(int, str)) | いずれかの候補 |
dict[K, V]、TypedDict | 検証済みの辞書 |
ResponseSchema(...) | 上記と同じ。プロバイダー向けに name / description を明示できます |
@response_schema callable | カスタムの検証/解析ロジック |
PromptedSchema(inner) | ネイティブの構造化出力に対応していないプロバイダー向けに、スキーマをシステムプロンプトに挿入します |
ResponseSchema — ペイロードに名前を付ける
プロバイダーが構造化出力を名前付きの契約として扱えるようにします。
from ag2 import Agent, ResponseSchema
schema = ResponseSchema(int | str, name="ByteWidth", description="Number of bits in one byte.")
agent = Agent("assistant", config=config, response_schema=schema)
@response_schema — カスタム検証
値の範囲制限、正規表現によるクリーンアップ、ラップされた JSON のデコード、フィールドの結合などに使います。
from ag2 import Agent, response_schema
@response_schema
def parse_rating(content: str) -> int:
"""Parse a rating and clamp to 1–5."""
return max(1, min(5, int(content)))
agent = Agent("assistant", config=config, response_schema=parse_rating)
複数パラメーター形式では、パラメーター名から JSON オブジェクトのスキーマを生成します。
from typing import Annotated
from pydantic import Field
from ag2 import response_schema
@response_schema
def extract_listing(
title: Annotated[str, Field(description="Product name")],
price_usd: Annotated[float, Field(description="Price in USD", ge=0)],
in_stock: Annotated[bool, Field(description="True if it ships now")],
) -> dict:
return {"title": title, "price_usd": price_usd, "in_stock": in_stock}
この関数は依存性注入にも対応しています。Context、Variable、Inject、Depends はツールと同じように機能し、JSON スキーマには含まれません。
非同期バリデーターにも対応しています。
import json
@response_schema
async def fetch_and_validate(content: str) -> dict:
data = json.loads(content)
data["validated"] = True
return data
PromptedSchema — ネイティブの構造化出力に対応していないプロバイダー向け
response_format を使う代わりに、JSON スキーマをシステムプロンプトに挿入します。
from ag2 import Agent, PromptedSchema
agent = Agent("assistant", config=config, response_schema=PromptedSchema(int))
任意の内部スキーマ(型、ResponseSchema、@response_schema callable)をラップします。検証ロジックはそのままで、ワイヤー形式のみが変わります。
カスタムプロンプトテンプレート:
PromptedSchema(int, prompt_template="Reply with JSON matching this schema:\n{schema}")
ターンごとのオーバーライド
agent = Agent("assistant", config=config)
turn = await agent.ask("How many seconds in a minute?", response_schema=int)
print(await turn.content()) # 60
turn2 = await turn.ask("Say hello.") # back to default (no schema)
response_schema=None を渡すと、その呼び出しに限り、エージェントに設定されたスキーマを解除できます。
再試行
検証に失敗したとき、モデルに自動で再度問い合わせます。
result = await reply.content(retries=3) # initial + up to 3 re-asks
result = await reply.content(retries=math.inf) # interactive only — could loop forever
検証エラーはフォローアップとしてモデルに送信され、モデルはそれを修正できます。
プリミティブの埋め込み(embed)
プリミティブ(int、float、bool、list[T]、プリミティブのユニオン)は、デフォルトで {"data": ...} にラップされます。ほとんどの構造化出力 API は、値そのものよりオブジェクトを安定して処理できるためです。content() は透過的にラップを解除します。無効にする場合:
ResponseSchema(int, name="RawInt", embed=False) # model must produce a bare 42
@response_schema(embed=False)
def parse_rating(value: int) -> int: ...
さらに詳しく
- すぐに使えるサンプル:
assets/recipe_builder.py(code_examples/02に対応)— Pydantic モデル +@tool+response_schema=。 - 完全なリファレンス:
website/docs/user-guide/structured_output.mdx— すべてのスキーマ型、複数パラメーターの@response_schema、Fieldの制約、PromptedSchema、再試行、埋め込みの動作を解説しています。
よくある落とし穴
- 型付き出力が必要なのに
reply.bodyを読む —reply.bodyは生テキストです。解析するにはawait reply.content()を使います。 content()のawaitを忘れる — 非同期処理なので、値ではなくコルーチンが返されます。- Pydantic フィールドに
descriptionがない — LLM が各フィールドに何を入れるべきか推測する場合があります。明白でないキーにはすべてField(description=...)を追加してください。 - プロバイダーがネイティブの構造化出力に対応していない — API と格闘するのではなく、
PromptedSchema(...)でラップしてください。 - 本番環境で
retries=math.infを指定する — 指示に従えないモデルでは無限にループします。有限回数を指定してください。 - ターンごとのオーバーライドは単一ターンのみ — 1 回の
ask()にresponse_schema=intを渡しても、エージェントのデフォルトは変わりません。次のターンでは、コンストラクターで設定した値に戻ります。