Generowanie ustrukturyzowanych danych wyjściowych

Jeśli chcesz przeanalizować odpowiedzi z Gemini Nano i przekonwertować je na określone formaty, np. JSON, w celu dalszego przetwarzania, użyj interfejsu Structured Output API z bezpośrednim dostępem do modelu.

Za pomocą interfejsu Structured Output API definiujesz docelową strukturę danych wyjściowych, używając klas i adnotacji Kotlin. Bezpośredni dostęp do modelu zwraca odpowiedź w postaci obiektu Kotlin.

Generowanie ustrukturyzowanych danych wyjściowych jest szczególnie przydatne w przypadku takich zadań jak:

  • Ekstrakcja encji: wyodrębnianie pól uporządkowanych (np. nazwy wydarzenia, daty, lokalizacji) z nieuporządkowanego tekstu.
  • Klasyfikacja: przypisywanie tekstu wejściowego do wstępnie zdefiniowanych kategorii.
  • Serializacja danych: konwertowanie nieuporządkowanych danych wejściowych użytkownika na 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. Interfejs API zwraca wartość true, jeśli interfejs Structured Output API jest dostępny na urządzeniu, a w przeciwnym razie – false.

suspend fun isStructuredOutputFeatureAvailable(): Boolean

Interfejs Structured Output API ma też te wymagania:

  • Android API na poziomie 26 lub wyższym (minSdk 26)
  • Wtyczka KSP w wersji 2.3.6 lub nowszej

Ograniczenia

Interfejs Structured Output API ma te ograniczenia:

  • Działa tylko w języku Kotlin.
  • ProGuard może zakłócać analizowanie klasy z adnotacjami. Dodaj klasę z adnotacjami do reguł zachowywania, aby wykluczyć ją z ProGuard, jeśli wystąpią błędy analizy, na przykład:
# Keep classes used by structured output for deserialization for release builds.
-keep class com.google.mlkit.genai.demo.kotlin.Plant { *; }

Konfigurowanie projektu

Aby rozpocząć korzystanie z interfejsu Structured Output API, wykonaj te czynności:

  1. Dodaj zależność com.google.mlkit:genai-prompt w pliku build.gradle.kts (lub build.gradle) na poziomie aplikacji, jeśli jeszcze nie została dodana.

  2. Dodaj wtyczkę KSP do pliku build.gradle.kts na poziomie projektu. Użyj wersji wtyczki KSP, która jest zgodna z Twoją wersją Kotlina. Zalecamy 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 uporządkowanego kompilatora do pliku build.gradle.kts na poziomie aplikacji:

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

Zdefiniuj strukturę danych wyjściowych

Zdefiniuj strukturę danych, które model ma zwracać, za pomocą klas danych w języku Kotlin. Do definiowania struktury danych wyjściowych służą 2 główne adnotacje:

  • Użyj adnotacji @Generable, aby zdefiniować klasę jako cel dla ustrukturyzowanych 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.

Poniższy 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 W przypadku tekstu. description, enumValues
Double/Float W przypadku liczb zmiennoprzecinkowych. 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 String wartości. description, enumValues, minItems, maxItems
@Generable zajęcia W przypadku zagnieżdżonych obiektów uporządkowanych. description

Generowanie ustrukturyzowanych 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
)

// 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

Podczas korzystania z interfejsu Structured Output API należy obsługiwać potencjalne wyjątki zgłaszane przez interfejs API i sprawdzać właściwość finishReason w odpowiedziach kandydatów, jeśli przeanalizowana odpowiedź ma wartość null.

Wartości finishReason

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

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

Wyjątki

Interfejs Structured Output API może zgłaszać błąd 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 nie ma żadnych zależności zapętlonych.
  • GenAiException.STRUCTURED_OUTPUT_INVALID_VALUE (-105): wartości wygenerowane przez model są nieprawidłowe lub nie przeszły weryfikacji ograniczeń. Jest to błąd środowiska wykonawczego. Jeśli ten błąd występuje często, rozważ te rozwiązania:
    • ulepszyć instrukcje w prompcie, aby model ściślej się ich trzymał;
    • Złagodzenie ograniczeń (takich jak limity minimalne, maksymalne lub rozmiaru listy) w adnotacjach @Guide, jeśli są zbyt restrykcyjne dla możliwości modelu.
    • wdrożenie w aplikacji strategii zastępczej, np. ponowienie żądania lub wyświetlenie stanu domyślnego;

Zliczanie tokenów

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

Ponieważ żądania ustrukturyzowanych danych wyjściowych muszą instruować model o strukturze schematu, zliczanie tokenów tylko na podstawie nieprzetworzonego tekstu prompta (za pomocą instancji GenerateContentRequest) nie jest dokładne. Aby uzyskać dokładną liczbę tokenów, musisz przekazać do metody countTokens() pełną instancję GenerateTypedContentRequest, która zawiera konfiguracje klasy docelowej i schematu:

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