構造化出力を生成する

Prompt API からのレスポンスを JSON などの特定の形式に解析して、さらに処理する必要がある場合は、Structured Output API を使用します。

Structured Output API を使用すると、Kotlin クラスとアノテーションを使用してターゲット出力構造を定義できます。Prompt API は、Kotlin オブジェクトの形式でレスポンスを返します。

構造化された出力を生成することは、次のようなタスクに特に役立ちます。

  • エンティティ抽出: 非構造化テキストから構造化フィールド(イベント名、日付、場所など)を抽出します。
  • 分類: 入力テキストを事前定義されたカテゴリに分類します。
  • データのシリアル化: 非構造化ユーザー入力をデータベース ストレージや API 呼び出しに適した形式に変換すること。

前提条件

構造化出力 API がデバイスで利用可能であることを確認するには、isStructuredOutputFeatureAvailable() API を使用します。この API は、デバイスで構造化出力 API が利用可能な場合は true を返し、それ以外の場合は false を返します。

suspend fun isStructuredOutputFeatureAvailable(): Boolean

構造化出力 API には次の要件もあります。

  • Android API レベル 26 以上(minSdk 26)
  • KSP プラグイン バージョン 2.3.6 以降

制限事項

Structured Output API には次の制限があります。

  • Kotlin でのみ動作します。
  • ProGuard がアノテーション付きクラスの解析を妨げる可能性があります。解析エラーが発生した場合は、アノテーション付きクラスを keep ルールに追加して、ProGuard から除外します。次に例を示します。
# Keep classes used by structured output for deserialization for release builds.
-keep class com.google.mlkit.genai.demo.kotlin.Plant { *; }

プロジェクトの構成

構造化出力 API を使用する手順は次のとおりです。

  1. アプリレベルの build.gradle.kts(または build.gradle)ファイルに ML Kit Prompt API を依存関係として追加します(まだ追加していない場合)。

  2. KSP プラグインをプロジェクト レベルの build.gradle.kts ファイルに追加します。Kotlin のバージョンと互換性のある KSP プラグイン バージョンを使用します。KSP バージョン 2.3.6 以降をおすすめします。

    dependencies {
        ...
        classpath "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin:2.3.6"
    }
    
  3. 構造化コンパイラの依存関係をアプリレベルの build.gradle.kts ファイルに追加します。

    dependencies {
        ...
        ksp("com.google.mlkit:genai-schema-compiler:1.0.0-alpha1")
    }
    

出力構造を定義する

モデルが返すデータの構造を Kotlin データクラスを使用して定義します。出力構造を定義する主なアノテーションは次の 2 つです。

  • @Generable アノテーションを使用して、クラスを構造化出力のターゲットとして定義します。
  • クラス プロパティで @Guide アノテーションを使用して、モデルの出力をガイドする説明と制約を指定します。

次の例では、植物情報を抽出するための構造を定義しています。

import com.google.mlkit.genai.schema.annotations.Generable
import com.google.mlkit.genai.schema.annotations.Guide

@Generable
data class PlantList(
    @Guide(description = "The list of plants found", minItems = 1, maxItems = 5)
    val plants: List<Plant>
)

@Generable("Information about a plant species")
data class Plant(
    @Guide(description = "The common name of the plant")
    val commonName: String,
    
    @Guide(description = "The full latin scientific name of the plant")
    val scientificName: String,
    
    @Guide(
        description = "The maximum height of the plant in centimeters.",
        minimum = 1.0,
        maximum = 10000.0
    )
    val maxHeightCm: Int,
    
    @Guide(description = "Whether the plant is poisonous or not")
    val isPoisonous: Boolean?,
    
    @Guide(
        description = "The primary continent where this plant is native to",
        enumValues = ["Africa", "Antarctica", "Asia", "Australia", "Europe", "North America", "South America"]
    )
    val nativeContinent: String
)

サポートされている型と制約

@Generable アノテーション付きクラスでは、次の型がサポートされています。また、それぞれの @Guide 制約もサポートされています。

タイプ 説明 サポートされている @Guide 制約
String テキストの場合。 descriptionenumValues
Double / Float 浮動小数点数。 descriptionminimummaximum
Int / Long 整数の場合。 descriptionminimummaximum
Boolean true/false 値の場合。 description
List<T> サポートされている型またはネストされた @Generable クラスのリスト。 descriptionminItemsmaxItems
List<String> String 値のリスト。 descriptionenumValuesminItemsmaxItems
@Generable クラス ネストされた構造化オブジェクトの場合。 description

構造化コンテンツを生成する

構造化された出力をリクエストするには、generateTypedContentRequest ヘルパー関数を使用して標準プロンプトをラップし、ターゲット出力クラスを指定します。

// 1. Initialize your GenerativeModel as usual
val generativeModel = Generation.getClient()

// 2. Prepare the prompt text
val promptText = "List some common plants found in California."
val baseRequest = GenerateContentRequest.Builder(TextPart(promptText)).build()

// 3. Create the typed request, specifying the target class (e.g., PlantList)
val typedRequest = generateTypedContentRequest(
    generateContentRequest = baseRequest,
    outputClass = PlantList::class,
    // Instructs ML Kit to include the generated schema structure in the prompt
    // sent to AICore. This should always be set to `true` unless the model
    // already knows what output format to use.
    includeSchemaInPrompt = true
)

// 4. Run the inference
try {
    val typedResponse = generativeModel.generateContent(typedRequest)
    
    // 5. Access the parsed object
    // The response candidates contain the parsed object of type T (PlantList in this case)
    val plantList: PlantList? = typedResponse.candidates.firstOrNull()?.response
    
    if (plantList != null) {
        // Process the structured data
        for (plant in plantList.plants) {
            Log.d("StructuredOutput", "Found plant: ${plant.commonName} (${plant.scientificName})")
        }
    } else {
        Log.e("StructuredOutput", "Failed to parse response into the desired structure.")
        
        // Inspect finish reason for details
        val finishReason = typedResponse.candidates.firstOrNull()?.finishReason
        Log.d("StructuredOutput", "Finish reason: $finishReason")
    }
} catch (e: GenAiException) {
    // Handle API errors
    when (e.errorCode) {
        GenAiException.STRUCTURED_OUTPUT_INVALID_CLASS -> {
            Log.e("StructuredOutput", "The class structure is not supported.")
        }
        GenAiException.STRUCTURED_OUTPUT_INVALID_VALUE -> {
            Log.e("StructuredOutput", "The model generated values that violate the schema constraints.")
        }
        else -> {
            Log.e("StructuredOutput", "API error: ${e.message}")
        }
    }
}

完了理由とエラーを処理する

Structured Output API を使用する場合は、API によってスローされる可能性のある例外を処理し、解析されたレスポンスが null の場合は、レスポンス候補の finishReason プロパティを検査する必要があります。

finishReason の値

finishReason プロパティには、次のいずれかの値を指定できます。

  • TypedFinishReason.STOP: モデルが正常に生成を完了し、出力がスキーマと一致しています。
  • TypedFinishReason.MAX_TOKENS: トークンの上限に達したため、モデルが停止しました。出力が不完全である可能性があります。
  • TypedFinishReason.PARSE_CLASS_ERROR: モデルは生成を完了しましたが、生成された JSON をターゲットの Kotlin クラスに解析できませんでした。
  • TypedFinishReason.STRUCTURE_NOT_ANNOTATED: ターゲット クラスまたはそのネストされたクラスに必要な @Generable アノテーションがありません。
  • TypedFinishReason.STRUCTURE_VALUES_INVALID: 生成された値が @Guide アノテーションで定義された制約に違反しました(値が範囲外、リストサイズが範囲外など)。
  • TypedFinishReason.OTHER: その他の理由で生成が停止されました。

例外

構造化出力 API は、次のエラーコードで GenAiException をスローする可能性があります。

  • GenAiException.STRUCTURED_OUTPUT_INVALID_CLASS(-104): アノテーション付きクラスの構造が無効であるか、サポートされていない型が含まれています。通常、これは開発時の構成エラーです。@Generable データクラスの定義を確認して、すべてのプロパティ タイプがサポートされており、循環依存関係がないことを確認します。
  • GenAiException.STRUCTURED_OUTPUT_INVALID_VALUE(-105): モデルによって生成された値が無効であるか、制約の検証に失敗しました。これはランタイム エラーです。このエラーが頻繁に発生する場合は、次の解決策を検討してください。
    • モデルをより厳密にガイドするようにプロンプトの指示を調整する。
    • モデルの機能に対して制約が厳しすぎる場合は、@Guide アノテーションの制約(最小値、最大値、リストサイズの上限など)を緩和します。
    • リクエストの再試行やデフォルト状態の表示など、アプリでフォールバック戦略を実装する。

トークンのカウント

構造化プロンプトが入力トークンの上限内にあるかどうかを確認するには、countTokens() メソッドを使用してトークン数を計算します。

構造化出力リクエストでは、スキーマ構造をモデルに指示する必要があるため、生のプロンプト テキストのみでトークンをカウントする(GenerateContentRequest インスタンスを使用する)のは正確ではありません。正確なトークン数を取得するには、ターゲット クラスとスキーマ構成を含む完全な GenerateTypedContentRequest インスタンスを countTokens() メソッドに渡す必要があります。

suspend fun <T : Any> countTokens(request: GenerateTypedContentRequest<T>): CountTokensResponse