Generowanie ustrukturyzowanych danych wyjściowych

Jeśli chcesz przeanalizować odpowiedzi z interfejsu Prompt API w określonych formatach, np. JSON, aby je dalej przetwarzać, użyj interfejsu Structured Output API.

Za pomocą interfejsu Structured Output API możesz zdefiniować docelową strukturę danych wyjściowych za pomocą klas i adnotacji Kotlin. Interfejs Prompt API zwraca wtedy odpowiedź w postaci obiektu Kotlin.

Generowanie uporządkowanych danych wyjściowych jest szczególnie przydatne w przypadku takich zadań:

  • Wyodrębnianie encji: wyodrębnianie uporządkowanych pól (np. nazwy wydarzenia , daty, lokalizacji) z nieustrukturyzowanego tekstu.
  • Klasyfikacja: kategoryzowanie tekstu wejściowego według wstępnie zdefiniowanych kategorii.
  • Serializacja danych: przekształcanie nieustrukturyzowanych danych wejściowych użytkownika w format odpowiedni do przechowywania w bazie danych lub wywołań interfejsu API.

Wymagania wstępne

Aby sprawdzić, czy interfejs Structured Output API jest dostępny na urządzeniu, użyj interfejsu isStructuredOutputFeatureAvailable() API. Jeśli interfejs Structured Output API jest dostępny na urządzeniu, interfejs API zwraca wartość true, a w przeciwnym razie – false.

suspend fun isStructuredOutputFeatureAvailable(): Boolean

Interfejs Structured Output API ma też te wymagania:

  • Poziom 26 lub wyższy interfejsu Android API (minSdk 26)
  • Wtyczka KSP w wersji 2.3.6 lub nowszej

Ograniczenia

Interfejs Structured Output API ma te ograniczenia:

# Keep classes used by structured output for deserialization for release builds.
-keep class com.google.mlkit.genai.demo.kotlin.Plant { *; }

Konfigurowanie projektu

Aby zacząć korzystać z interfejsu Structured Output API:

  1. Dodaj interfejs ML Kit Prompt API jako zależność w pliku na poziomie aplikacji build.gradle.kts (lub build.gradle), jeśli jeszcze tego nie zrobisz.

  2. Dodaj wtyczkę KSP do pliku build.gradle.kts na poziomie projektu. Użyj wtyczki KSP w wersji zgodnej z Twoją wersją języka Kotlin. Zalecamy używanie wtyczki KSP w wersji 2.3.6 lub nowszej.

    dependencies {
        ...
        classpath "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin:2.3.6"
    }
    
  3. Dodaj zależności kompilatora strukturalnego do pliku build.gradle.kts na poziomie aplikacji:

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

Definiowanie struktury danych wyjściowych

Zdefiniuj strukturę danych, które mają być zwracane przez model, za pomocą klas danych Kotlin. Do definiowania struktury danych wyjściowych służą 2 główne adnotacje:

  • Użyj adnotacji @Generable, aby zdefiniować klasę jako cel uporządkowanych danych wyjściowych.
  • Użyj adnotacji @Guide we właściwościach klasy, aby podać opisy i ograniczenia, które będą kierować danymi wyjściowymi modelu.

Ten przykład definiuje strukturę do wyodrębniania informacji o roślinach:

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
)

Obsługiwane typy i ograniczenia

W klasie z adnotacją @Generable obsługiwane są te typy wraz z odpowiednimi ograniczeniami @Guide:

Typ Opis Obsługiwane ograniczenia @Guide
String Tekst. description, enumValues
Double / Float Liczby zmiennoprzecinkowe. description, minimum, maximum
Int / Long Liczby całkowite. description, minimum, maximum
Boolean Wartości prawda/fałsz. description
List<T> Listy obsługiwanych typów lub zagnieżdżonych klas @Generable. description, minItems, maxItems
List<String> Listy wartości String. description, enumValues, minItems, maxItems
Klasa @Generable Zagnieżdżone obiekty strukturalne. description

Generowanie uporządkowanych treści

Aby poprosić o uporządkowane dane wyjściowe, użyj funkcji pomocniczej generateTypedContentRequest, aby opakować standardowy prompt i określić docelową klasę danych wyjściowych.

// 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}")
        }
    }
}

Obsługa przyczyn zakończenia i błędów

Gdy używasz interfejsu Structured Output API, musisz obsługiwać potencjalne wyjątki zgłaszane przez interfejs API i sprawdzać właściwość finishReason w kandydatach odpowiedzi, jeśli przeanalizowana odpowiedź ma wartość null.

Wartości finishReason

Właściwość finishReason może przyjmować jedną z tych wartości:

  • TypedFinishReason.STOP: model zakończył generowanie i dane wyjściowe są zgodne ze schematem.
  • TypedFinishReason.MAX_TOKENS: model został zatrzymany, ponieważ osiągnął limit tokenów. Dane wyjściowe mogą być niepełne.
  • TypedFinishReason.PARSE_CLASS_ERROR: model zakończył generowanie, ale nie udało się przeanalizować wynikowego kodu JSON w docelowej klasie Kotlin.
  • TypedFinishReason.STRUCTURE_NOT_ANNOTATED: w klasie docelowej lub jej zagnieżdżonych klasach brakuje wymaganej adnotacji @Generable.
  • TypedFinishReason.STRUCTURE_VALUES_INVALID: wygenerowane wartości naruszyły ograniczenia zdefiniowane w adnotacjach @Guide (np. wartość poza zakresem, rozmiar listy poza zakresem).
  • TypedFinishReason.OTHER: generowanie zostało zatrzymane z innych powodów.

Wyjątki

Interfejs Structured Output API może zgłaszać wyjątek GenAiException z tymi kodami błędów:

  • GenAiException.STRUCTURED_OUTPUT_INVALID_CLASS (-104): struktura klasy z adnotacjami jest nieprawidłowa lub zawiera nieobsługiwane typy. Zwykle jest to błąd konfiguracji w czasie programowania. Sprawdź definicję klasy danych @Generable, aby upewnić się, że wszystkie typy właściwości są obsługiwane i że nie ma zależności cyklicznych.
  • GenAiException.STRUCTURED_OUTPUT_INVALID_VALUE (-105): wartości wygenerowane przez model są nieprawidłowe lub nie spełniają ograniczeń. Jest to błąd czasu działania. Jeśli ten błąd występuje często, rozważ te rozwiązania:
    • Uściślenie instrukcji prompta, aby ściślej kierować modelem.
    • Złagodzenie ograniczeń (np. limitów minimalnych, maksymalnych lub rozmiaru listy) w adnotacjach @Guide, jeśli są one zbyt restrykcyjne dla możliwości modelu.
    • Wdrożenie w aplikacji strategii rezerwowej, np. ponowienie próby wysłania żądania lub wyświetlenie stanu domyślnego.

Zliczanie tokenów

Aby sprawdzić, czy uporządkowany prompt mieści się w limicie tokenów wejściowych, oblicz liczbę tokenów za pomocą countTokens() metody.

Ponieważ żądania uporządkowanych danych wyjściowych muszą zawierać instrukcje dla modelu dotyczące struktury schematu, zliczanie tokenów tylko w tekście prompta (za pomocą instancji GenerateContentRequest) nie jest dokładne. Aby uzyskać dokładną liczbę tokenów, musisz przekazać do metody countTokens() całą instancję GenerateTypedContentRequest, która zawiera docelową klasę i konfiguracje schematu:

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