추가 처리를 위해 Gemini Nano의 대답을 JSON과 같은 특정 형식으로 파싱해야 하는 경우 직접 모델 액세스와 함께 Structured Output API를 사용하세요.
Structured Output API를 사용하면 Kotlin 클래스와 주석을 사용하여 타겟 출력 구조를 정의할 수 있습니다. 그러면 직접 모델 액세스가 Kotlin 객체 형식으로 대답을 반환합니다.
구조화된 출력을 생성하는 것은 특히 다음과 같은 작업에 유용합니다.
- 항목 추출: 구조화되지 않은 텍스트에서 구조화된 필드 (예: 이벤트 이름, 날짜, 위치)를 추출합니다.
- 분류: 입력 텍스트를 사전 정의된 카테고리로 분류합니다.
- 데이터 직렬화: 비정형 사용자 입력을 데이터베이스 스토리지 또는 API 호출에 적합한 형식으로 변환합니다.
기본 요건
기기에서 Structured Output API를 사용할 수 있는지 확인하려면 isStructuredOutputFeatureAvailable() API를 사용합니다. API는 기기에서 구조화된 출력 API를 사용할 수 있는 경우 true를 반환하고, 그렇지 않으면 false를 반환합니다.
suspend fun isStructuredOutputFeatureAvailable(): Boolean
구조화된 출력 API에는 다음과 같은 요구사항도 있습니다.
- Android API 수준 26 이상 (
minSdk26) - 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 { *; }
프로젝트 구성
구조화된 출력 API를 시작하려면 다음 단계를 따르세요.
아직 추가하지 않았다면 앱 수준
build.gradle.kts(또는build.gradle) 파일에com.google.mlkit:genai-prompt종속 항목을 추가합니다.프로젝트 수준
build.gradle.kts파일에 KSP 플러그인을 추가합니다. Kotlin 버전과 호환되는 KSP 플러그인 버전을 사용합니다. KSP 버전 2.3.6 이상을 사용하는 것이 좋습니다.dependencies { ... classpath "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin:2.3.6" }앱 수준
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 |
참/거짓 값입니다. | 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
)
// 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}")
}
}
}
완료 사유 및 오류 처리
구조화된 출력 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