こんにちは、LayerX Ai Workforce事業部でR&Dチームマネージャーをしている澁井(しぶい)と申します。これはLayerX AI Agent ブログリレー19日目の記事になります。
OpenAIがGPTを提供開始した2022年末から、LLM(大規模言語モデル)やそのAPIをソフトウェアに組み込む大きな流れが始まりました。その中で、私が最も感動したエンジニアリングが、2023年6月に登場したFunction callingです。Function calling(Tool callとも)ではOpenAI APIへのリクエスト時に期待する出力形式をJSONで定義でき、LLMの応答をそのJSONに強制できます。これにより、LLMの出力をプログラムで安定して扱うことが格段に容易になりました。当初は外部の関数を呼び出すための仕組みでしたが、現在ではStructured Outputとして発展し、LLMの出力に構造化を強制するための重要なプラクティスとなっています。
解決したい課題
Structured Outputを活用するためには常にプロンプトの出力形式を定義する必要があります。LLMが組み込まれたソフトウェアで、どれほどの実装でプロンプトとそのStructured Outputが定義されているでしょうか? たとえばシステムの一部でエンドユーザーが自由にプロンプトを入力できる場合、そのプロンプト入力と一緒にStructured Outputを定義することは困難でしょう。LLMの価値は多様な自然言語に対する課題を柔軟に解くことができる一方で、出力が自由であることはソフトウェアにLLMを組み込む課題になります。
もちろんプロンプトに自然言語で出力形式を書くことは可能です。しかしそれはシステム的な強制とはならず、ソフトウェアの中でLLMを活用するうえでは不十分です。たとえば、ECサイトの商品レビューから「感情(ポジティブ、ネガティブ、ニュートラル)」「言及されているキーワード」「要約」を抽出する機能を開発しているとします。プロンプトを工夫しても、LLMからの応答は「感情: ポジティブ、キーワード: [価格, デザイン]」という形式だったり、「このレビューは好意的です。特に価格とデザインについて言及がありました。」という文章形式だったりと、安定しません。
解決策の提案
こうした課題を解決する一案として、LLMでプロンプトを解析して、自動的にStructured Outputを生成するライブラリauto-structured-outputを個人開発しました。本ライブラリではOpenAI Python SDKの利用にあたり、プロンプトから期待される出力スキーマを推論して、Pydanticモデルとして定義します。本ライブラリを利用することで、LLMの出力にStructured Outputを簡単に適用できるようにすると同時に、出力形式が決まることで情報をMECE(モレなくダブりなく)に保つことを支援します。
auto-structured-outputでは、OpenAI APIのStructured Outputに準拠した出力構造を生成することができます。プロンプトを入力として、期待する出力形式が指定されている場合はそれをStructured Outputとして抽出し、pydantic.BaseModelを継承したデータモデルとして返します。期待する出力形式が指定されていない場合は、Reasoning性能の高いモデル(GPT-5等)を用いてプロンプトを解析して、出力に適したPydanticモデルを推論します。Pydanticの強力な型判定を用いることで、LLMの出力を型安全に扱うことができ、後続の処理で安心して利用できるようになります。
前述のECサイトのレビュー分析の事例を考えてみましょう。多様なレビューを分析するため、以下のようなプロンプトを用意したとします。
あなたは優れたデータアナリストです。
以下のレビューを分析し、次の3つの情報をJSON形式で出力してください。
1. sentiment: レビューの全体的な感情を "positive", "neutral", "negative" のいずれかで表現してください。
2. keywords: レビュー内で言及されている重要なキーワードをリスト形式で抽出してください。
3. summary: レビュー内容の短い要約を提供してください。
4. 投稿日: レビューが投稿された日付を提供してください。
レビュー:
"{review_text}"
こうしたプロンプトをauto-structured-outputに渡すと、以下のようなPydanticモデルが生成されます。
import os from auto_structured_output import StructureExtractor openai_client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) # Initialize the structure extractor extractor = StructureExtractor(openai_client) # Analyze the prompt to extract the expected output structure ReviewModel = extractor.extract_structure(prompt) """生成されたStructured OutputのPydanticモデル class ReviewModel(BaseModel): sentiment: Literal["positive", "neutral", "negative"] = Field(..., description="レビューの全体的な感情") keywords: list[str] = Field(..., description="レビュー内で言及されている重要なキーワード") summary: str = Field(..., description="レビュー内容の短い要約") posted_at: str = Field(..., description="レビューが投稿された日付") """ response = openai_client.chat.completions.parse( model="gpt-4o", messages=[{"role": "user", "content": prompt}], response_format=ReviewModel, )
さらには、データ構造を解析してネストされた構造を生成することも可能です。たとえば、レビューに対する複数の返信が含まれる場合、以下のようなプロンプトを用意します。
あなたは優れたデータアナリストです。
以下のレビューとその返信を分析し、次の情報をJSON形式で出力してください。
1. sentiment: レビューの全体的な感情を "positive", "neutral", "negative" のいずれかで表現してください。
2. keywords: レビュー内で言及されている重要なキーワードをリスト形式で抽出してください。
3. summary: レビュー内容の短い要約を提供してください。
4. posted_at: レビューが投稿された日付を提供してください。
5. replies: 各返信について、以下の情報を含むリストを提供してください。
- comment: 返信内容
- sentiment: 返信の感情を "positive", "neutral", "negative" のいずれかで表現してください。
- posted_at: 返信が投稿された日付
レビュー:
"{review_text}"
返信:
"{replies_text}"
このプロンプトをauto-structured-outputに渡すと、以下のようなネストされたPydanticモデルが生成されます。
"""生成されたStructured OutputのPydanticモデル class ReplyModel(BaseModel): comment: str = Field(..., description="返信内容") sentiment: Literal["positive", "neutral", "negative"] = Field(..., description="返信の感情") posted_at: str = Field(..., description="返信が投稿された日付") class ReviewModel(BaseModel): sentiment: Literal["positive", "neutral", "negative"] = Field(..., description="レビューの全体的な感情") keywords: list[str] = Field(..., description="レビュー内で言及されている重要なキーワード") summary: str = Field(..., description="レビュー内容の短い要約") posted_at: str = Field(..., description="レビューが投稿された日付") replies: list[ReplyModel] = Field(..., description="各返信についての情報") """
仕組み
auto-structured-outputは以下のようなアーキテクチャで実現されています。

プロンプトをSchemaGeneratorに渡し、OpenAI APIを通してプロンプトの期待するStructured Outputのスキーマを生成します。スキーマはSchemaValidatorで成否を検証します。エラーがある場合はそのエラーメッセージをSchemaGeneratorのプロンプトに付与して、再度SchemaGeneratorを実行します。スキーマが生成されたらPydanticモデルに変換し、OpenAI APIのresponse_formatに渡すことで、LLMの出力を型安全に扱えるようになります。
応用編
LLMの活用したソフトウェアにおいて、プロンプトのStructured Outputを定義することが難しいケースは案外少なくありません。先述のようにエンドユーザーがプロンプトを自由に入力できるケースや、さらにはLLMにプロンプトを動的に生成させる場合、期待する出力形式も動的に変わります。こうしたケースでは、プロンプトとともに最初からStructured Outputを定義することが困難です。
たとえばプロンプトを連ねたAI Agentを、LLMのReasoning能力を駆使して動的に生成することを考えてみましょう。AI Agentは処理グラフの各ノードがプロンプトを有します。そのプロンプトはLLMが課題や状況に応じてReasoningして生成するとします。こうした場合、プロンプトの期待する出力形式は「ある程度は想定されるが、確実に決まることはない」状況が発生します。さらにいうと、AI Agentのようにロジカルにプロンプトを連ねる場合、各ノードの出力は後続のノードの入力として利用されるため、出力が安定しないことはシステム全体の信頼性を大きく損ないます。こうしたケースでauto-structured-outputを活用することで、各ノードのプロンプトから期待される出力形式を固定して安定化し、AI Agentの堅牢性を確保することができます。
参考
LLMのワークフロー自動生成でプロンプトと同時に出力形式を生成する例:

【R&D】 X人月を削減せよ ーLLMで業務ワークフローを自動生成するー
注意点とトレードオフ
Structured Outputは強力ですが、いくつかの注意点とトレードオフが存在します。最も大きなトレードオフは、LLMの創造性が制約される可能性があることでしょう。厳格なスキーマを課すことは、LLMに「決められた箱の中に答えを埋める」ことを強制するのに等しく、自由な発想や予期せぬ洞察が得られにくくなるかもしれません。たとえば、新しい製品のキャッチコピーをブレインストーミングさせるようなタスクでは、あえて構造化を緩やかにするか、全く行わない方が、より創造的で多様なアイデアを引き出せる可能性があります。
また、スキーマが過度に複雑化することによる弊害にも注意が必要です。要求するフィールド数が多くなったり、ネストが深くなったりすると、LLMがスキーマ全体を正しく理解し、すべての制約に従うことが難しくなります。結果として、スキーマに準拠しない出力が生成されたり、一部のフィールドが欠落したりする確率が高まります。さらに、複雑なスキーマはプロンプト全体の長さを増加させ、APIの利用コストや応答時間の増大に繋がるため、本当に必要なフィールドだけに絞り込む設計が求められます。
まとめ
LLMは強力なツールですが、同時に不確実性を内包した技術です。そして不確実性に対処する鉄則は、不確実な要素を局所化して制限し、評価することです。Structured OutputはLLMの不確実性を制御する重要なプラクティスであり、LLMをソフトウェアに組み込む信頼性を大きく向上させます。auto-structured-outputがStructured Outputを有効活用し、LLMを活用したソフトウェア開発の一助となれば幸いです。
Xの@LayerX_techアカウントではLayerXの様々な取り組みを発信していますので、是非こちらもフォローしてください。