MNTSQ Techブログ

リーガルテック・カンパニー「MNTSQ(モンテスキュー)」のTechブログです。

GeminiのStructured outputでレスポンスの型を矯正するためのTips 7選

MNTSQ Tech Blog TOP > 記事一覧 > GeminiのStructured outputでレスポンスの型を矯正するためのTips 7選

こんにちは、MNTSQ(モンテスキュー)でアルゴリズムエンジニアをしている清水です。

MNTSQは契約書を解析・管理・検索するプロダクトを提供しています。これらのプロダクトには大規模言語モデル(以下LLM)が搭載された機能が実装されています。また、LLMを活用した新プロダクトも鋭意開発中です。

LLMをアプリケーションに組み込む際の大きな課題の一つとして、「LLMの出力形式(型)を如何に矯正するか?」が挙げられます。単純なチャットアプリケーションであればそこまで問題にはなりませんが、LLMによる生成結果を後続のプログラムで処理する必要がある場合、事前に定義された型に従って出力を生成する必要があります。

現在、複数のLLMサービスで出力形式を制御する機能が搭載されていますが、本記事ではGoogleが提供しているGeminiStructured outputを取り上げます。本記事では、開発の過程で得られた、GeminiのStructured outputにおける7つのTipsを紹介したいと思います。

サンプルコード

例として以下のようにPythonGoogle Gen AI SDKgoogle-genai)を使用することを想定しています。google-genaiではtypes.GenerateContentConfigresponse_schemaPydanticのモデルを渡すことで、Structured outputを使用することができます。

本記事ではStructured outputの機能にフォーカスするのでプロンプトは最低限の内容にしています。また、スキーマとして指定するPydanticモデルも、タイトルの抽出と契約書かどうかを判定するだけのシンプルなものにしています。また、Gemini APIではなくVertex AIAPIを介してGeminiを使用します。(ほとんどのケースでGemini APIに対しても同じTipsが適用できると思いますが、一部仕様が異なる可能性があります。)

from google.genai import Client, types
from pydantic import BaseModel

PROMPT_TEMPLATE = """\
JSONスキーマに従って、ドキュメントの内容を分析してください。

<json_schema>
{json_schema}
</json_schema>

<document>
{document_text}
</document>
"""

class ContractAnalysisResult(BaseModel):
    document_name: str
    is_contract: bool

def analyze_contract(document_text: str) -> ContractAnalysisResult:
    client = Client(vertexai=True, project="development", location="global")
    prompt = PROMPT_TEMPLATE.format(
        json_schema=ContractAnalysisResult.model_json_schema(),
        document_text=document_text,
    )
    contract_analysis_result = client.models.generate_content(
        model="gemini-2.5-flash",
        contents=prompt,
        # response_schemaにPydanticモデルを渡す
        config=types.GenerateContentConfig(
            response_schema=ContractAnalysisResult,
        ),
    )
    return contract_analysis_result

Tips 1: プロンプトにJSONスキーマを含めない

一番簡単に試すことができるテクニックは、「プロンプトにJSONスキーマを含めない」です。実は、response_schemaを設定した場合は、JSONスキーマをプロンプトに含めないことが公式のドキュメントで推奨されています。

警告: responseSchema を構成する場合は、テキスト プロンプトでスキーマを指定しないでください。これにより、予期しない結果や品質の低い結果が生じる可能性があります。

以下のサンプルコードでは、上記のサンプルコードからJSONスキーマを埋め込んでいた箇所を削除しています。

# response_schemaを指定する際には、JSONスキーマをプロンプトに含めない
PROMPT_TEMPLATE = """\
JSONスキーマに従って、ドキュメントの内容を分析してください。

<document>
{document_text}
</document>
"""

Structured outputについてすべての仕組みが詳細に明かされているわけではないので、なぜJSONスキーマをプロンプトに含めないことが推奨されるのか技術的な理由は分かりません。公式のドキュメントで”don't duplicate the schema in the text prompt.”と書いてあることから、重複した情報をLLMに与えることが悪影響を及ぼすのかもしれません。

また、OpenAIやAnthropicのドキュメントには同様の記述は見当たらず(見逃していたらすみません)、Gemini特有の性質である可能性もあります。

Tips 2: titledescriptionを設定する

JSONスキーマの各フィールドにおいて、自然言語による説明を付けたい時は以下のようにtitledescriptionフィールドを使いましょう。Structured outputではJSONスキーマによる構造化されたデータしか渡せないと勘違いされがちですが、LLMらしく自然言語による情報も与えることができます。

from pydantic import BaseModel, Field

class ContractAnalysisResult(BaseModel):
    document_name: str = Field(
        title="タイトル (Title)",
        description="ドキュメント冒頭に記載されているタイトル",
    )
    is_contract: bool = Field(
        title="契約書かどうか (Is Contract)",
        description="ドキュメントが契約書かどうかを示す。就業規則や賃金規定などは契約書ではない。",
    )

ここで定義されたtitledescriptionVertex AIのAPIリファレンスで記述されているresponseSchemaフィールドのtitleフィールドとdescriptionフィールドに渡されます。(詳しくは次の項目で言及します)

Tips 3: APIに渡されるパラメータを確認する

Pydanticモデルをresponse_schemaに渡すだけでStructured outputを使用できますが、最終的にどのような形式でAPIに渡されるのかを確認することは有効です。以下のようにして、response_schemaに渡したPydanticモデルが、どのようにAPIに渡すためのパラメータに変換されるかを確認することができます。

from google.genai import Client
from google.genai import _transformers as t
from pydantic import BaseModel, Field

class ContractAnalysisResult(BaseModel):
    document_name: str = Field(
        title="タイトル (Title)",
        description="ドキュメント冒頭に記載されているタイトル",
    )
    is_contract: bool = Field(
        title="契約書かどうか (Is Contract)",
        description="ドキュメントが契約書かどうか。就業規則や賃金規定などは契約書ではない。",
    )

if __name__ == "__main__":
    client = Client(vertexai=True, project="development", location="global")
    # _transformers.t_schemaにClientオブジェクトとPydanticモデルを渡す
    request_params = t.t_schema(client, ContractAnalysisResult)
    print(request_params.model_dump_json(indent=2, exclude_none=True))

出力結果

{
  "properties": {
    "document_name": {
      "description": "ドキュメント冒頭に記載されているタイトル",
      "title": "タイトル (Title)",
      "type": "STRING"
    },
    "is_contract": {
      "description": "ドキュメントが契約書かどうか。就業規則や賃金規定などは契約書ではない。",
      "title": "契約書かどうか (Is Contract)",
      "type": "BOOLEAN"
    }
  },
  "property_ordering": [
    "document_name",
    "is_contract"
  ],
  "required": [
    "document_name",
    "is_contract"
  ],
  "title": "ContractAnalysisResult",
  "type": "OBJECT"
}

例えば、私は開発の過程で以下のようなgoogle-genaiの仕様(というよりはバグ?)を見つけました1

下記のように、Pydanticモデルが入れ子構造になっているスキーマにおいて、以下のように親モデルのFieldにおいてtitledescriptionを設定します。

from google.genai import Client
from google.genai import _transformers as t
from pydantic import BaseModel, Field

class ContractTerm(BaseModel):
    effective_date: str
    expiration_date: str

class ContractAnalysisResult(BaseModel):
    contract_term: ContractTerm = Field(
        title="契約期間 (Contract Term)",
        description="契約有効日と失効日から構成される契約の期間。",
    )

if __name__ == "__main__":
    client = Client(vertexai=True, project="development", location="global")
    request_params = t.t_schema(client, ContractAnalysisResult)
    print(request_params.model_dump_json(indent=2, exclude_none=True))

このスキーマt_schemaの出力を確認してみると、以下のようにtitledescriptionが消えてしまっていることが確認できます。(title はデフォルト値のクラス名(ContractTerm)が代わりに格納されています。)

{
  "properties": {
    "contract_term": {
      "properties": {
        "effective_date": {
          "title": "Effective Date",
          "type": "STRING"
        },
        "expiration_date": {
          "title": "Expiration Date",
          "type": "STRING"
        }
      },
      "property_ordering": [
        "effective_date",
        "expiration_date"
      ],
      "required": [
        "effective_date",
        "expiration_date"
      ],
      "title": "ContractTerm",
      "type": "OBJECT"
    }
  },
  "required": [
    "contract_term"
  ],
  "title": "ContractAnalysisResult",
  "type": "OBJECT"
}

以下のように親モデルのFieldではなく子モデルのConfigDicttitleを、docstringでdescriptionを設定すると、問題なく変換されます。

from google.genai import Client
from google.genai import _transformers as t
from pydantic import BaseModel, ConfigDict

class ContractTerm(BaseModel):
    """契約有効日と失効日から構成される契約の期間。"""
    # docstringを設定するとdescriptionとして認識される

    model_config = ConfigDict(title="契約期間 (Contract Term)")
    effective_date: str
    expiration_date: str

class ContractAnalysisResult(BaseModel):
    contract_term: ContractTerm

if __name__ == "__main__":
    client = Client(vertexai=True, project="development", location="global")
    request_params = t.t_schema(client, ContractAnalysisResult)
    print(request_params.model_dump_json(indent=2, exclude_none=True))

出力結果

{
  "properties": {
    "contract_term": {
      "description": "契約有効日と失効日から構成される契約の期間。",
      "properties": {
        "effective_date": {
          "title": "Effective Date",
          "type": "STRING"
        },
        "expiration_date": {
          "title": "Expiration Date",
          "type": "STRING"
        }
      },
      "property_ordering": [
        "effective_date",
        "expiration_date"
      ],
      "required": [
        "effective_date",
        "expiration_date"
      ],
      "title": "契約期間 (Contract Term)",
      "type": "OBJECT"
    }
  },
  "required": [
    "contract_term"
  ],
  "title": "ContractAnalysisResult",
  "type": "OBJECT"
}

思ったように型の矯正が効かないときはこのようなエッジケースを踏んでいるのかもしれません。そのような時は、この方法を使ってAPIに渡されるパラメータを確認すると良いでしょう。

Tips 4: date型やdatetime型を使用する

スキーマを定義するPydanticモデルの各フィールドおいて、Pythondate型やdatetime型を使用することができます。以下のPydanticモデルをt_schemaに渡すと以下のようなパラメータに変換されていることが確認できます。

from datetime import date

from google.genai import Client
from google.genai import _transformers as t
from pydantic import BaseModel

class ContractTerm(BaseModel):
    effective_date: date  # str型ではなくdate型を指定
    expiration_date: date

if __name__ == "__main__":
    client = Client(vertexai=True, project="development", location="global")
    request_params = t.t_schema(client, ContractTerm)
    print(request_params.model_dump_json(indent=2, exclude_none=True))

出力結果("format": "date", となっている箇所に注目してください)

{
  "properties": {
    "effective_date": {
      "format": "date",
      "title": "Effective Date",
      "type": "STRING"
    },
    "expiration_date": {
      "format": "date",
      "title": "Expiration Date",
      "type": "STRING"
    }
  },
  "property_ordering": [
    "effective_date",
    "expiration_date"
  ],
  "required": [
    "effective_date",
    "expiration_date"
  ],
  "title": "ContractTerm",
  "type": "OBJECT"
}

このformatフィールドはtitledescriptionと同様にAPIresponseSchemaでサポートされているフィールドです。ただし、どのようなformatでも良いわけではなく現状はdatedate-timetimedurationのみがサポートされているようです。それぞれPythonのdatetimeライブラリのdateクラス、datetimeクラス、timeクラス、timedeltaクラスが対応しています2

from datetime import date, datetime, time, timedelta

from google.genai import Client
from google.genai import _transformers as t
from pydantic import BaseModel

class DateTimeClasses(BaseModel):
    date_field: date
    datetime_field: datetime
    time_field: time
    duration_field: timedelta

if __name__ == "__main__":
    client = Client(vertexai=True, project="development", location="global")
    request_params = t.t_schema(client, DateTimeClasses)
    print(request_params.model_dump_json(indent=2, exclude_none=True))

出力結果

{
  "properties": {
    "date_field": {
      "format": "date",
      "title": "Date Field",
      "type": "STRING"
    },
    "datetime_field": {
      "format": "date-time",
      "title": "Datetime Field",
      "type": "STRING"
    },
    "time_field": {
      "format": "time",
      "title": "Time Field",
      "type": "STRING"
    },
    "duration_field": {
      "format": "duration",
      "title": "Duration Field",
      "type": "STRING"
    }
  },
  ...
}

Tips 5: その他サポートされているAPIのフィールドを使用する

responseSchemaがサポートしているフィールドは、上記で紹介したtitledescriptionformatフィールド以外にもあります。詳しくは公式ドキュメントをご参照ください。これらのフィールドはPydanticで以下のように表現できます。

from datetime import date, datetime, time, timedelta
from enum import Enum

from google.genai import Client
from google.genai import _transformers as t
from pydantic import BaseModel, Field

class EnumClass(Enum):
    A = "a"
    B = "b"
    C = "c"

class Schema(BaseModel):
    number_field: int = Field(ge=1, le=10)
    string_field: str = Field(min_length=1, max_length=10)
    list_field: list[int] = Field(min_items=1, max_items=10)
    with_pattern_field: str = Field(pattern=r"^[a-z]+$")
    # examplesを渡すとエラーになるので注意
    with_example_field: str = Field(json_schema_extra={"example": "example string"})
    nullable_field: str | None = Field(default=None)
    any_of_field: str | int
    enum_field: EnumClass

if __name__ == "__main__":
    client = Client(vertexai=True, project="development", location="global")
    request_params = t.t_schema(client, Schema)
    print(request_params.model_dump_json(indent=2, exclude_none=True))

出力結果

{
  "properties": {
    "number_field": {
      "maximum": 10.0,
      "minimum": 1.0,
      "title": "Number Field",
      "type": "INTEGER"
    },
    "string_field": {
      "max_length": 10,
      "min_length": 1,
      "title": "String Field",
      "type": "STRING"
    },
    "list_field": {
      "items": {
        "type": "INTEGER"
      },
      "max_items": 10,
      "min_items": 1,
      "title": "List Field",
      "type": "ARRAY"
    },
    "with_pattern_field": {
      "pattern": "^[a-z]+$",
      "title": "With Pattern Field",
      "type": "STRING"
    },
    "with_example_field": {
      "example": "example string",
      "title": "With Example Field",
      "type": "STRING"
    },
    "nullable_field": {
      "nullable": true,
      "title": "Nullable Field",
      "type": "STRING"
    },
    "any_of_field": {
      "any_of": [
        {
          "type": "STRING"
        },
        {
          "type": "INTEGER"
        }
      ],
      "title": "Any Of Field"
    },
    "enum_field": {
      "enum": [
        "a",
        "b",
        "c"
      ],
      "title": "EnumClass",
      "type": "STRING"
    }
  },
  "property_ordering": [
    "number_field",
    "string_field",
    "list_field",
    "with_pattern_field",
    "with_example_field",
    "nullable_field",
    "any_of_field",
    "enum_field"
  ],
  "required": [
    "number_field",
    "string_field",
    "list_field",
    "with_pattern_field",
    "with_example_field",
    "any_of_field",
    "enum_field"
  ],
  "title": "Schema",
  "type": "OBJECT"
}

Tips 6: エラー回避のためのvalidatorを実装する

上記で紹介したdate型のフィールドやmax_engthなどはPydanticモデルの制約として働きます。例えば、date型のフィールドに無効な日付の文字列が代入されるとエラーになります。また、max_length=10と指定されているフィールドに11文字以上の文字列が渡されると同じくエラーになります。

この時、Geminiがこれらの制約に違反したJSONを生成する可能性があることに注意が必要です。一定の矯正力はありますが、100%制約を守ってくれるわけではありません3。制約に違反したテキストが生成されたときにエラーにならないように、生成されたテキストを加工するvalidatorを実装しておくと安全でしょう。

例えば私は、date型のフィールドに対して0000-01-01のような無効な日付をGeminiが生成するケースを観測したことがあります。この場合、以下のようなvalidatorを実装してエラーを回避すると良いでしょう。

import logging
from datetime import date
from typing import Any

from pydantic import BaseModel, Field, ModelWrapValidatorHandler, ValidationError, field_validator

class EffectiveDate(BaseModel):
    effective_date: date | None = Field(default=None)

    @field_validator("effective_date", mode="wrap")
    def date_parsing_validator(value: Any, handler: ModelWrapValidatorHandler[Any]) -> Any:
        """0000-01-01のような無効な日付をNoneに変換する"""
        try:
            return handler(value)
        except ValidationError as e:
            if "date_parsing" in (error["type"] for error in e.errors()):
                logging.warning(f"Invalid date: {value}")
                return None
            else:
                raise e

if __name__ == "__main__":
    # Geminiが0000-01-01のような無効な日付を生成したと想定
    effective_date = EffectiveDate.model_validate_json('{"effective_date": "0000-01-01"}')
    print(effective_date)
    # WARNING:root:Invalid date: 0000-01-01
    # effective_date=None

Tips 7: Chain of Thoughtを意識する

Chain of Thought (CoT)とは、結論だけでなく推論の過程も生成させることでLLMの性能を向上させる手法のことです。通常はプロンプトを工夫したり、専用にチューニングされたモデルを使用することでCoTを実現するのですが、response_schemaを工夫することで擬似的なCoTを実現することができます

例として、以下のようなPydanticモデルを定義します。

from datetime import date

from pydantic import BaseModel, Field

class ContractTerm(BaseModel):
    effective_date: date | None = Field(description="契約期間の有効日")
    period: int | None = Field(description="契約が持続する期間")    
    expiration_date: date | None = Field(description="契約期間の失効日")

欲しいのはeffective_dateexpiration_date だけですが、同時にperiod も抽出するようにしています。このようにすることで、例えば「本契約は2025年1月1日から3年間有効とする」のように契約失効日が直接的に書かれていない場合でも、事前に抽出したeffective_dateperiod からexpiration_date を計算してくれる効果が期待できます。

このように、フィールドを定義する順番を工夫したり、関連する情報を抽出するように促すことで、擬似的なCoTが期待できるでしょう。

注意事項

本記事で紹介した内容は、2025年6月時点のGemini/Vertex AIの仕様と、google-genaiのバージョン1.19.0の仕様に基づいています。今後のアップデートによってGeminiやSDKの仕様が変更される可能性があります。実際に利用される際は、必ず公式のドキュメントをご確認いただくようお願いします。

まとめ

本記事では、GeminiのStructured outputでレスポンスの型を矯正するためのTipsをいくつか紹介しました。開発で得られた知見を全て盛り込んだら想定よりも多い文字数になってしまいました。是非開発のヒントにしていただけたら幸いです。

冒頭でも触れましたが、MNTSQではLLMを活用したプロダクトを鋭意開発中です。もしMNTSQの仕事にご興味を持っていただけたら、ぜひお気軽にカジュアル面談でお話ししましょう!

careers.mntsq.co.jp

note.mntsq.co.jp

tech.mntsq.co.jp

この記事を書いた人

tealgreen0503

清水健

MNTSQ アルゴリズムエンジニア
LLMのご機嫌と格闘する日々です。


  1. google-genaiバージョン1.19.0時点での動作です。バグであれば今後解消されるかもしれません。
  2. 他にもPendulumも対応しています。
  3. どの程度の矯正力を持つかはフィールドによって異なるようです。例えば私の場合enumフィールドに違反したケースに遭遇したことはありません。反対にmin_length, max_lengthは矯正力が弱く、validatorの実装は必須だと思われます。