ML Kit의 온디바이스 에이전트 워크플로 (com.google.mlkit:genai-agent, AICore 개발자 프리뷰 프로그램을 통해 사용 가능)를 사용하면 Gemini Nano 및 Android AICore 시스템 서비스를 기반으로 하는 자율적인 멀티턴 대화형 에이전트를 빌드할 수 있습니다.
직접 모델 액세스를 제공하는 GenerativeModel 클래스를 기반으로 빌드된 AgentRunner 클래스는 완전한 에이전트 루프를 자동화합니다. 즉, 다단계 작업을 계획하고, 유형이 지정된 도구 (예: 앱 저장소 쿼리, 백엔드 API 호출, 로컬 데이터베이스 읽기)를 호출하고, 도구 결과를 평가하고, 네트워크 지연 시간 없이 완전한 사용자 개인 정보 보호를 통해 기기에서 로컬로 대화 컨텍스트를 관리합니다. GenerativeModel 클래스는 List<Content> 객체(USER 및 MODEL 역할 포함)와 sessionId 값을 사용하여 클라이언트 관리 멀티턴 기록을 지원하지만, AgentRunner 클래스는 AgentSession 인터페이스를 사용하여 관리형 멀티턴 세션을 제공합니다. 이 세션은 여러 차례에 걸쳐 인메모리 대화 기록을 자동으로 기록하므로 스크립트를 수동으로 관리할 필요가 없습니다. 정적 도구 선언과 시스템 안내도 기기 내에 자동으로 캐시되므로 추가 구성 없이 첫 번째 토큰까지의 지연 시간이 단축됩니다.
핵심 구성요소
Agent클래스:Agent.builder()메서드를 사용하여 요청 사항, 사용 가능한 도구, 실행 가드레일을 선언합니다. 기본값은 자동으로maxTurns = 10입니다.AgentRunner클래스:GenerativeModel클래스를 기반으로 빌드되며, 기기 내 모델을 사용하여 자율 추론 루프와 관리형 세션을 조정합니다.Dispatchers.IO에서 기본적으로 기본 안전,temperature은 기본적으로null로 설정됨(모델의 기본값으로 지연됨, 탐욕적 디코딩의 경우temperature = 0.0f전달)AgentSession인터페이스:runAsync(content)및close()메서드를 사용하여 차례마다 인메모리 대화 기록을 자동으로 기록하는runner.getOrCreateSession(sessionId)메서드를 사용하여 가져온 범위가 지정된 멀티턴 대화 세션 핸들입니다.AgentEvent인터페이스: 텍스트 토큰과 도구 업데이트를 내보내는 반응형 KotlinFlow스트림입니다.AgentTool클래스: 모델에서 호출할 수 있는 유형이 지정된 함수로, Android AppFunctions (@AppFunctionDeclaration)를 사용하여 정의됩니다.GenAiException클래스:MAX_TURNS_EXCEEDED,NOT_AVAILABLE,BUSY와 같은 실패 상태를 알리는 유형이 지정된 예외 계층 구조(com.google.mlkit.genai.common.GenAiException)입니다.
Android 기술
GitHub에서 보기ML Kit Agent API 사용
android skills add mlkit-agent-api기본 요건 및 종속 항목
Agent API를 통합하기 전에 개발 환경과 Gradle 구성이 다음 요구사항을 충족하는지 확인하세요.
플랫폼 및 하드웨어 요구사항
- Android 버전: AICore 개발자 프리뷰 프로그램에 등록된 Android 14 (API 수준 34) 이상
- 지원되는 기기: 지원되는 기기 및 Gemini Nano 모델 버전 목록은 커스텀 생성 및 에이전트 기기 지원을 참고하세요.
- 최소 SDK:
minSdk 26이상 - Kotlin: Kotlin 2.3 이상
Gradle 구성
모듈의 build.gradle.kts 파일에 genai-agent 종속 항목을 추가합니다.
dependencies {
// Gemini Nano agentic workflows
implementation("com.google.mlkit:genai-agent:1.0.0-alpha1")
// Optional: Only required when defining tools using Android AppFunctions
implementation("androidx.appfunctions:appfunctions:1.0.0-alpha13")
ksp("androidx.appfunctions:appfunctions-compiler:1.0.0-alpha13")
}
genai-agent 자체에는 주석 프로세서나 컴파일러 플러그인이 필요하지 않지만 androidx.appfunctions로 도구를 정의하려면 appfunctions-compiler KSP 플러그인이 필요합니다.
빠른 시작: 에이전트 초기화
다음 코드 샘플은 기기 내 에이전트와 실행기를 인스턴스화하는 방법을 보여줍니다.
import com.google.mlkit.genai.agent.Agent
import com.google.mlkit.genai.agent.AgentRunner
// Configure the agent with system instructions
val agent = Agent.builder()
.setInstructions("You are a helpful on-device assistant.")
.build()
// Create the runner to execute prompts
val runner = AgentRunner(agent)
도구 정의
모델은 유형이 지정된 도구를 호출하여 앱 데이터를 쿼리하거나, 백엔드 서비스와 통신하거나, 인앱 액션을 트리거할 수 있습니다. 도구는 Android AppFunctions (androidx.appfunctions)를 사용하여 선언적으로 정의됩니다.
package com.example.ai.agent.tools
import androidx.appfunctions.AppFunctionDeclaration
import androidx.appfunctions.AppFunctionSerializable
import androidx.appfunctions.AppFunctionService
import androidx.appfunctions.AppFunctionServiceEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@AppFunctionSerializable
data class OrderDetails(
val orderId: String,
val status: String,
val eta: String,
)
@AppFunctionServiceEntryPoint(
serviceName = "OrderAppFunctionService",
appFunctionXmlFileName = "order_app_functions",
)
abstract class OrderTools : AppFunctionService() {
/**
* Fetches details and delivery status for an in-app order.
*
* @param orderId The alphanumeric order identifier
*/
@AppFunctionDeclaration(isDescribedByKDoc = true)
suspend fun fetch_order_details(
orderId: String
): OrderDetails = withContext(Dispatchers.IO) {
// Query local database or call your backend API
OrderDetails(
orderId = orderId,
status = "Out for delivery",
eta = "2:30 PM",
)
}
}
생성된 서비스를 앱의 AndroidManifest.xml 파일에 등록합니다.
<service
android:name=".OrderAppFunctionService"
android:permission="android.permission.BIND_APP_FUNCTION_SERVICE"
android:exported="true">
<property
android:name="android.app.appfunctions.schema"
android:value="app_functions_schema.xsd" />
<property
android:name="android.app.appfunctions.v2"
android:value="order_app_functions.xml" />
<intent-filter>
<action android:name="android.app.appfunctions.AppFunctionService" />
</intent-filter>
</service>
ACCESS_LEVEL_SELF 상수를 사용하여 호출자 액세스 제한
AppFunctions는 시스템이나 외부 호출자에게 공개될 필요가 없습니다. Android에서 액세스 제어는 호출자 ID를 검사하는 대신 호출자 액세스 수준을 통해 처리됩니다.
기본적으로(ACCESS_LEVEL_ANDROID_TRUSTED 상수 사용) AppFunctions는 EXECUTE_APP_FUNCTIONS 시스템 권한을 보유한 플랫폼 인증 기기 내 어시스턴트에서 호출할 수 있습니다. 도구가 앱의 내부 에이전트 전용인 경우 (예: 사용자 장바구니 또는 비공개 데이터베이스 레코드 관리) ACCESS_LEVEL_SELF 상수를 사용하여 호출자 액세스를 제한합니다.
import androidx.appfunctions.AppFunctionAccessLevel
import androidx.appfunctions.AppFunctionDeclaration
import androidx.appfunctions.AppFunctionService
import androidx.appfunctions.AppFunctionServiceEntryPoint
import androidx.appfunctions.ExperimentalAppFunctionsApi
import androidx.appfunctions.metadata.AppFunctionMetadata
@AppFunctionServiceEntryPoint(
serviceName = "OrderAppFunctionService",
appFunctionXmlFileName = "order_app_functions",
)
abstract class OrderTools : AppFunctionService() {
/**
* Cancels an order. Restricted strictly to this app's internal agent.
*/
@OptIn(ExperimentalAppFunctionsApi::class)
@AppFunctionDeclaration(isDescribedByKDoc = true)
@AppFunctionAccessLevel(
level = AppFunctionMetadata.ACCESS_LEVEL_SELF,
isCompatEnforcementEnabled = true,
)
fun cancel_order(
orderId: String
): String {
// Only your app's internal agent can invoke this function
return "Order $orderId cancelled successfully."
}
}
ACCESS_LEVEL_SELF 상수로 제한되면 플랫폼은 외부 표시 경로 및 시스템 어시스턴트가 IPC를 통해 함수를 검색하거나 실행하지 못하도록 차단합니다.
에이전트 구성
시스템 안내를 사용하여 Agent 인스턴스를 구성하고 AppFunctions를 사용 설정한 다음 AgentRunner 인스턴스를 초기화합니다.
import com.google.mlkit.genai.agent.Agent
import com.google.mlkit.genai.agent.AgentRunner
// Configure the agent with system instructions and enable AppFunctions
val agent = Agent.builder()
.setName("OrderAssistant")
.setInstructions(
"You are a helpful assistant for looking up store orders. " +
"Use available tools to fetch order information."
)
.enableAppFunctions(true) // Automatically discovers all registered AppFunctions in your app
.build()
// Initialize the runner
val runner = AgentRunner(agent)
원하는 경우 addAppFunctionIds("fetch_order_details", "cancel_order") 메서드를 호출하여 모든 인앱 기능을 노출하는 대신 특정 AppFunction ID로만 탐색 범위를 지정합니다.
에이전트 기본값
- 차례 제한:
Agent.Builder클래스는 무한 도구 호출 루프를 방지하기 위해maxTurns = 10를 종료 조건으로 자동 기본 설정합니다. 표준 대화 또는 도구 호출 워크플로의 경우setMaxTurns(10)메서드를 수동으로 호출할 필요가 없습니다. - 결정론적 탐욕 디코딩:
AgentRunner클래스는 기본temperature매개변수를null로 설정하여 기본 모델의 기본값을 따릅니다. 기기 내에서 결정론적 도구 선택 및 JSON 스키마 규정 준수를 위해temperature = 0.0f(temperature)를AgentRunner생성자에 전달합니다. 모바일 모델에서 할루시네이션 위험과 스키마 저하를 증가시키는 클라우드 스타일 온도 (> 0.4f)를 설정하지 마세요.
멀티턴 실행 및 스트리밍 이벤트
Android ViewModel 클래스 내에서 runner.getOrCreateSession(sessionId) 메서드를 사용하여 AgentSession 인스턴스를 가져옵니다. 세션은 실시간 텍스트 토큰과 도구 실행 업데이트를 내보내는 Flow<AgentEvent> 스트림을 반환합니다.
package com.example.ai.agent
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.google.mlkit.genai.agent.AgentEvent
import com.google.mlkit.genai.agent.AgentRunner
import com.google.mlkit.genai.agent.AgentSession
import com.google.mlkit.genai.common.GenAiException
import com.google.mlkit.genai.prompt.Content
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
data class AgentUiState(
val messages: List<String> = emptyList(),
val currentResponse: String = "",
val activeToolStatus: String? = null,
val isLoading: Boolean = false,
val errorMessage: String? = null
)
class ChatViewModel(
private val runner: AgentRunner,
private val sessionId: String = "chat_session"
) : ViewModel() {
// Scoped session preserves conversation history across configuration changes (screen rotations)
private val session: AgentSession = runner.getOrCreateSession(sessionId)
private val _uiState = MutableStateFlow(AgentUiState())
val uiState: StateFlow<AgentUiState> = _uiState.asStateFlow()
fun sendMessage(userText: String) {
_uiState.update {
it.copy(
messages = it.messages + userText,
currentResponse = "",
activeToolStatus = null,
isLoading = true,
errorMessage = null
)
}
viewModelScope.launch {
val content = Content.builder().text(userText).build()
session.runAsync(content)
.catch { e ->
val message = when {
e is GenAiException && e.errorCode == GenAiException.ErrorCode.MAX_TURNS_EXCEEDED ->
"Agent exceeded the maximum reasoning turns allowed."
e is GenAiException && e.errorCode == GenAiException.ErrorCode.NOT_AVAILABLE ->
"On-device AI is currently unavailable."
e is GenAiException && e.errorCode == GenAiException.ErrorCode.BUSY ->
"On-device AI is busy. Please try again."
else -> e.localizedMessage ?: "Agent error"
}
_uiState.update {
it.copy(
isLoading = false,
activeToolStatus = null,
errorMessage = message
)
}
}
.collect { event ->
when (event) {
is AgentEvent.TextMessage -> {
_uiState.update {
it.copy(
currentResponse = if (event.isPartial) {
it.currentResponse + event.text
} else {
event.text.ifEmpty { it.currentResponse }
},
activeToolStatus = null
)
}
}
is AgentEvent.FunctionCall -> {
_uiState.update {
it.copy(activeToolStatus = "Executing ${event.name}...")
}
}
is AgentEvent.FunctionResponse -> {
_uiState.update {
it.copy(activeToolStatus = "Received data from ${event.name}")
}
}
else -> {}
}
}
// Finalize message once stream completes
_uiState.update {
it.copy(
messages = it.messages + it.currentResponse,
currentResponse = "",
activeToolStatus = null,
isLoading = false
)
}
}
}
override fun onCleared() {
super.onCleared()
// Cleanly evicts conversation memory from RAM when ViewModel is destroyed
session.close()
}
}
텍스트 토큰 직접 스트리밍
도구 실행 세부정보를 UI에 별도로 표시할 필요가 없는 대화형 인터페이스의 경우 Flow<AgentEvent> 인터페이스에서 textTokens() 확장 함수를 사용합니다.
import com.google.mlkit.genai.agent.textTokens
import com.google.mlkit.genai.prompt.Content
val content = Content.builder().text("What is my order status?").build()
session.runAsync(content)
.textTokens()
.collect { token ->
print(token)
}
싱글턴 실행
대화 기록을 RAM에 보관해서는 안 되는 일회성 작업 (예: 사진 태그 지정 또는 임시 텍스트 요약)의 경우 runner.runSingleTurnAsync(content) 메서드를 직접 호출합니다.
// Ephemeral single-turn: cleans up session state from RAM automatically upon completion
val content = Content.builder()
.text("Summarize the order history for customer #1042: ...")
.build()
runner.runSingleTurnAsync(content)
.textTokens()
.collect { token ->
displaySummary(token)
}
runner.runSingleTurnAsync(content) 메서드를 직접 호출하면 AgentRunner 인스턴스가 내부적으로 임시 세션 ID를 할당하고 완료 시 메모리에서 대화 상태를 자동으로 삭제하여 메모리 누수를 방지합니다.
세션 수명 주기 및 메모리 관리
멀티턴 대화 기록은 메모리에 저장되고 sessionId 값으로 색인이 생성됩니다.
- 구성 변경: Android
ViewModel클래스 내에AgentSession인스턴스를 유지하면 매번 문자열을 연결하지 않고도 화면 회전 시 대화 기록이 자동으로 보존됩니다. - 세션 제거: 대화 스레드가 닫힐 때
session.delete()메서드를 호출하여 세션을 동기식으로 제거하거나ViewModel.onCleared()메서드에서 정지되지 않는session.close()메서드를 호출하여 RAM을 확보합니다. - 일괄 제거:
runner.deleteSession(sessionId)메서드를 호출하여 특정 세션을 해제하거나runner.clearAllSessions()메서드를 호출하여 캐시된 모든 대화를 제거합니다. - 실행기 수명 주기:
AgentRunner클래스는AutoCloseable인터페이스를 구현합니다. 화면 간에 실행기를 공유할 때는 각 화면의ViewModel.onCleared()메서드에서 개별AgentSession핸들을 닫고 실행기를 완전히 종료할 때runner.close()메서드를 호출합니다. - 동시 세션: 단일
AgentRunner인스턴스는 여러 개의 고유한 세션을 동시에 호스팅할 수 있습니다. 각sessionId값은 격리된 대화 기록을 유지합니다.
GenAiException 클래스를 사용한 오류 처리
에이전트가 응답을 생성하지 않고 최대 도구 차례에 도달하거나 기기 내 AI를 일시적으로 사용할 수 없는 경우와 같이 에이전트 실행 중 런타임 오류가 발생하면 GenAiException 예외(com.google.mlkit.genai.common.GenAiException)로 발생하거나 발생합니다. Kotlin 코루틴 흐름 catch 연산자를 사용하여 오류를 사후 대응적으로 처리합니다.
session.runAsync(content)
.catch { e ->
if (e is GenAiException) {
when (e.errorCode) {
GenAiException.ErrorCode.MAX_TURNS_EXCEEDED -> {
// Turn limit exceeded
}
GenAiException.ErrorCode.NOT_AVAILABLE -> {
// Gemini Nano or AICore unavailable
}
GenAiException.ErrorCode.BUSY -> {
// System busy or rate limit reached
}
else -> {
// Other GenAiException errors
}
}
}
}
.collect { event ->
// Process response events
}
주요 오류 코드
GenAiException.ErrorCode.MAX_TURNS_EXCEEDED(-112): 에이전트가 최종 대답을 생성하지 않고 최대 대화 제한(maxTurns제한)을 초과했습니다.GenAiException.ErrorCode.NOT_AVAILABLE(8): Gemini Nano 모델 또는 AICore 시스템 서비스를 사용할 수 없거나 준비되지 않았습니다.GenAiException.ErrorCode.BUSY(9): 시스템이 사용 중이거나 호출 앱이 할당량을 초과했습니다.