生成结构化输出

如果您需要将 Prompt API 的响应解析为特定格式(例如 JSON)以进行进一步处理,请使用 Structured Output API。

借助 Structured Output API,您可以使用 Kotlin 类和注解定义目标输出结构。然后,Prompt API 会以 Kotlin 对象的形式返回响应。

生成结构化输出对于以下任务特别有用:

  • 实体提取:从非结构化文本中提取结构化字段(例如事件 名称、日期、位置)。
  • 分类:将输入文本归类到预定义的类别中。
  • 数据序列化:将非结构化用户输入转换为适合数据库存储或 API 调用的格式。

前提条件

如需验证设备上是否提供 Structured Output API,请使用 isStructuredOutputFeatureAvailable() API。如果设备上提供 Structured Output API,该 API 会返回 true,否则返回 false

suspend fun isStructuredOutputFeatureAvailable(): Boolean

Structured Output API 还具有以下要求:

  • Android API 级别 26 或更高版本 (minSdk 26)
  • KSP 插件版本 2.3.6 或更高版本

限制

Structured Output API 具有以下限制:

  • 仅适用于 Kotlin。
  • ProGuard 可能会干扰对带注解的类的解析。如果您在解析时遇到错误,请将带 注解的类添加到保留规则中,以将其从 ProGuard 中排除,例如:
# Keep classes used by structured output for deserialization for release builds.
-keep class com.google.mlkit.genai.demo.kotlin.Plant { *; }

配置项目

如需开始使用 Structured Output API,请按以下步骤操作:

  1. 如果您尚未添加机器学习套件 Prompt API,请将其作为依赖项添加到您的 应用级 build.gradle.kts(或 build.gradle)文件中。

  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 数据类定义您希望模型返回的数据的结构。定义输出结构有两个主要注解:

  • 使用 @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:模型因达到 token 限制而停止。输出可能不完整。
  • TypedFinishReason.PARSE_CLASS_ERROR:模型已完成生成,但生成的 JSON 无法解析为目标 Kotlin 类。
  • TypedFinishReason.STRUCTURE_NOT_ANNOTATED:目标类或其嵌套类缺少必需的 @Generable 注解。
  • TypedFinishReason.STRUCTURE_VALUES_INVALID:生成的值违反了 @Guide 注解中定义的限制条件(例如,值超出范围、列表大小超出范围)。
  • TypedFinishReason.OTHER:生成因其他原因而停止。

例外情况

Structured Output API 可能会抛出 GenAiException,并显示以下错误代码:

  • GenAiException.STRUCTURED_OUTPUT_INVALID_CLASS (-104):带注解的类的结构无效或包含不受支持的类型。这通常是开发时的配置错误。检查 @Generable 数据类定义,以确保所有属性类型都受支持,并且不存在任何循环依赖关系。
  • GenAiException.STRUCTURED_OUTPUT_INVALID_VALUE (-105):模型生成的值无效或未能通过限制条件验证。这是一个运行时错误。如果您经常遇到此错误,请考虑以下解决方案:
    • 优化提示说明,以更严格地指导模型。
    • 放宽 @Guide 注解中的限制条件(例如最小值、最大值或列表大小限制),如果这些限制条件对于模型的功能过于严格。
    • 在应用中实现回退策略,例如重试请求或显示默认状态。

统计 token 数量

如需检查结构化提示是否在输入 token 限制范围内,请计算 token 数量,使用 countTokens() 方法。

由于结构化输出请求需要指示模型架构结构,因此仅对原始提示文本(使用 GenerateContentRequest 实例)统计 token 数量并不准确。如需获得准确的 token 数量,您必须将完整的 GenerateTypedContentRequest 实例(包括目标类和架构配置)传递给 countTokens() 方法:

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