יצירת פלט מובנה

אם אתם צריכים לנתח את התגובות מ-Prompt API לפורמטים מסוימים, כמו JSON, כדי לבצע עיבוד נוסף, אתם יכולים להשתמש ב-Structured Output API.

באמצעות Structured Output API, מגדירים את מבנה פלט היעד באמצעות הערות ומחלקות Kotlin. לאחר מכן, Prompt API מחזיר תגובה בצורה של אובייקט Kotlin.

יצירת פלט מובנה שימושית במיוחד למשימות כמו:

  • חילוץ ישויות: חילוץ שדות מובְנים (למשל, שם האירוע, תאריך, מיקום) מטקסט לא מובְנה.
  • סיווג: סיווג טקסט קלט לקטגוריות מוגדרות מראש.
  • סריאליזציה של נתונים: המרה של קלט לא מובנה של משתמשים לפורמט שמתאים לאחסון במסד נתונים או לקריאות ל-API.

דרישות מוקדמות

כדי לוודא ש-Structured Output API זמין במכשיר, משתמשים ב-isStructuredOutputFeatureAvailable() API. ה-API מחזיר true אם Structured Output API זמין במכשיר, ו-false אם הוא לא זמין.

suspend fun isStructuredOutputFeatureAvailable(): Boolean

בנוסף, יש גם את הדרישות הבאות ל-Structured Output API:

  • רמת API‏ Android 26 ומעלה (minSdk 26)
  • פלאגין KSP בגרסה 2.3.6 ואילך

מגבלות

ל-Structured Output API יש את המגבלות הבאות:

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

הגדרת הפרויקט

כדי להתחיל להשתמש ב-Structured Output API, פועלים לפי השלבים הבאים:

  1. מוסיפים את ML Kit Prompt API כתלות בקובץ build.gradle.kts (או build.gradle) ברמת האפליקציה, אם עדיין לא עשיתם זאת.

  2. מוסיפים את הפלאגין KSP לקובץ build.gradle.kts ברמת הפרויקט. משתמשים בגרסת פלאגין KSP שתואמת לגרסת Kotlin. מומלץ להשתמש בגרסה 2.3.6 של KSP ואילך.

    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. יש שני סוגים עיקריים של הערות להגדרת מבנה הפלט:

  • משתמשים בהערה @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 לטקסט. description, enumValues
Double / Float למספרים בשיטת נקודה צפה. description,‏ minimum,‏ maximum
Int / Long למספרים שלמים. description,‏ minimum,‏ maximum
Boolean לערכים true/false. description
List<T> רשימות של סוגים נתמכים או של מחלקות @Generable מקוננות. description,‏ minItems,‏ maxItems
List<String> רשימות של ערכי String. description, enumValues, minItems, maxItems
@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 יוצר ולבדוק את המאפיין finishReason במועמדים לתגובה אם התגובה שנותחה היא null.

ערכים של 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