도구 정의

도구를 사용하면 온디바이스 에이전트가 앱 데이터에 액세스하고, 백엔드 서비스와 통신하고, 사용자를 대신하여 작업을 실행할 수 있습니다.

도구의 작동 방식

사용자가 프롬프트를 제출하면 AgentRunner 클래스는 4단계에 걸쳐 에이전트 루프를 실행합니다.

  1. 관찰: Gemini Nano는 등록된 도구 선언 (에이전트의 작업 공간)을 기준으로 사용자의 프롬프트와 현재 대화 컨텍스트를 분석합니다.
  2. 이유: 모델이 인앱 도구가 필요한지 여부를 결정합니다. 도구가 필요한 경우 Gemini Nano는 함수 이름과 유형이 지정된 인수가 포함된 구조화된 도구 호출을 내보냅니다.
  3. Act: AgentRunner 인스턴스는 이 호출을 자동으로 가로채고 앱에서 상응하는 Kotlin 함수를 실행합니다.
  4. 의견 평가: 도구의 출력이 Gemini Nano에 다시 입력됩니다. 모델은 피드백을 평가하고 종료 조건이 충족될 때까지 (목표를 완료하거나 maxTurns 한도에 도달할 때까지) 사용자에게 답변하거나 다른 도구를 호출합니다.

Android AppFunctions를 사용하는 선언적 도구

도구를 정의하는 권장 방법은 Android AppFunctions (androidx.appfunctions)를 사용하여 선언적으로 정의하는 것입니다. AppFunctions는 자동 스키마 생성으로 유형이 지정된 인앱 기능을 선언하기 위한 표준화된 플랫폼 모델을 제공합니다.

1. Gradle 종속 항목 추가

모듈의 build.gradle.kts 파일에서 genai-agent와 함께 AppFunctions 아티팩트를 추가합니다.

dependencies {
    implementation("com.google.mlkit:genai-agent:1.0.0-alpha1")
    implementation("androidx.appfunctions:appfunctions:1.0.0-alpha13")
    ksp("androidx.appfunctions:appfunctions-compiler:1.0.0-alpha13")
}

2. @AppFunctionDeclaration 주석으로 도구 메서드에 주석 달기

@AppFunctionDeclaration(isDescribedByKDoc = true) 주석을 사용하여 공개 도구 메서드에 주석을 추가합니다. 매개변수 설명은 표준 KDoc @param 태그에 문서화되어 있습니다.

AppFunctions는 반환 값이 기본 유형 (예: String, Int, Boolean)이거나 @AppFunctionSerializable 주석으로 주석 처리된 구조화된 데이터 클래스여야 합니다.

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

/**
 * Structured details for an order.
 */
@AppFunctionSerializable
data class OrderDetails(
    val orderId: String,
    val status: String,
    val estimatedDelivery: String?,
    val itemCount: Int,
)

/**
 * Result returned when cancelling an order.
 */
@AppFunctionSerializable
data class CancelOrderResult(
    val orderId: String,
    val cancelled: Boolean,
    val message: String,
)

@AppFunctionServiceEntryPoint(
    serviceName = "OrderAppFunctionService",
    appFunctionXmlFileName = "order_app_functions",
)
abstract class OrderTools : AppFunctionService() {

    private val orderRepository = OrderRepository()

    /**
     * Fetches details and delivery status for a customer order by ID.
     *
     * @param orderId The alphanumeric order identifier (for example, ORD-8492)
     */
    @AppFunctionDeclaration(isDescribedByKDoc = true)
    suspend fun fetch_order_details(
        orderId: String
    ): OrderDetails = withContext(Dispatchers.IO) {
        val order = orderRepository.findOrder(orderId)
        if (order != null) {
            OrderDetails(
                orderId = order.id,
                status = order.status,
                estimatedDelivery = order.deliveryEstimate,
                itemCount = order.items.size,
            )
        } else {
            OrderDetails(
                orderId = orderId,
                status = "NOT_FOUND",
                estimatedDelivery = null,
                itemCount = 0,
            )
        }
    }

    /**
     * Requests a cancellation or return for an active order.
     *
     * @param orderId The order identifier to cancel
     * @param reason The reason provided by the user for cancellation
     */
    @AppFunctionDeclaration(isDescribedByKDoc = true)
    suspend fun cancel_order(
        orderId: String,
        reason: String? = null
    ): CancelOrderResult = withContext(Dispatchers.IO) {
        val success = orderRepository.cancelOrder(orderId, reason)
        CancelOrderResult(
            orderId = orderId,
            cancelled = success,
            message = if (success) {
                "Order cancellation processed."
            } else {
                "Order cannot be cancelled at this stage."
            }
        )
    }
}

생성된 서비스를 앱의 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>

3. ACCESS_LEVEL_SELF 상수를 사용하여 호출자 액세스 제한

AppFunctions는 시스템이나 외부 어시스턴트에 공개될 필요가 없습니다. Android에서 액세스 제어는 수동 호출자 ID 확인이 아닌 호출자 액세스 수준을 통해 관리됩니다.

기본적으로(ACCESS_LEVEL_ANDROID_TRUSTED 상수 사용) AppFunction은 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() {

    private val orderRepository = OrderRepository()

    /**
     * Cancels an order. Restricted exclusively to this app's internal agent.
     */
    @OptIn(ExperimentalAppFunctionsApi::class)
    @AppFunctionDeclaration(isDescribedByKDoc = true)
    @AppFunctionAccessLevel(
        level = AppFunctionMetadata.ACCESS_LEVEL_SELF,
        isCompatEnforcementEnabled = true,
    )
    suspend fun cancel_order(orderId: String): CancelOrderResult {
        // Only your app's internal agent can invoke this function
        val success = orderRepository.cancelOrder(orderId)
        return CancelOrderResult(
            orderId = orderId,
            cancelled = success,
            message = if (success) "Cancelled." else "Failed."
        )
    }
}

플랫폼은 세 가지 호출자 액세스 수준을 지원합니다.

  • ACCESS_LEVEL_SELF (100): 호스트 애플리케이션 자체로 엄격하게 제한됩니다. 외부 어시스턴트, 시스템 표시 경로, 기타 앱은 IPC를 통해 함수를 검색하거나 호출할 수 없습니다. 앱의 내부 에이전트에만 제공되어야 하는 민감한 작업이나 도구에 이 수준을 사용합니다.
  • ACCESS_LEVEL_SYSTEM (200): android.Manifest.permission#EXECUTE_APP_FUNCTIONS_SYSTEM 권한을 보유한 권한이 있는 시스템 호출자 (예: 시스템 런처 또는 최근 항목) 및 호스트 앱 자체로 제한됩니다.
  • ACCESS_LEVEL_ANDROID_TRUSTED (300, 기본값): EXECUTE_APP_FUNCTIONS 권한을 보유한 플랫폼 인증 온디바이스 에이전트, 시스템 표시 경로, 호스트 앱에 공개됩니다.

4. 에이전트에 도구 등록

enableAppFunctions() 메서드를 사용하여 자동으로 또는 AppFunctionAgentTools.discover() 메서드를 사용하여 프로그래매틱 방식으로 에이전트에 AppFunctions를 등록할 수 있습니다.

enableAppFunctions() 메서드를 사용한 자동 탐색

모델이 호출할 때 AppFunctions가 자동으로 실행되는 간단한 앱의 경우:

import com.google.mlkit.genai.agent.Agent
import com.google.mlkit.genai.agent.AgentRunner

val agent = Agent.builder()
    .setName("OrderAssistant")
    .setInstructions("Assist users with order tracking.")
    .enableAppFunctions(true) // Automatically discovers all AppFunctions in your app package
    .setMaxTurns(10)
    .build()

val runner = AgentRunner(agent)

원하는 경우 addAppFunctionIds("fetch_order_details", "cancel_order") 메서드를 호출하여 모든 인앱 기능을 노출하는 대신 특정 AppFunction ID로만 탐색 범위를 지정합니다.

AppFunctionAgentTools.discover() 메서드를 사용한 프로그래매틱 검색

앱에서 검색된 AppFunction을 검사하거나 필터링하거나 에이전트에 등록하기 전에 장식해야 하는 경우:

import com.google.mlkit.genai.agent.Agent
import com.google.mlkit.genai.agent.AgentRunner
import com.google.mlkit.genai.agent.AppFunctionAgentTools

val discoveredTools = AppFunctionAgentTools.discover(
    enabledAppFunctionIds = listOf("fetch_order_details", "cancel_order") // Optional ID filter
)

val agent = Agent.builder()
    .setName("OrderAssistant")
    .setInstructions("Assist users with order tracking.")
    .addTools(discoveredTools)
    .build()

val runner = AgentRunner(agent)

데코레이터 패턴 및 휴먼인더루프

주문 취소, 결제, 사용자 데이터 삭제와 같은 민감한 작업은 사용자 확인을 요구하여 휴먼인더루프 (HITL)를 유지해야 합니다. Agent 클래스에 추가된 모든 도구는 Android AppFunctions에서 비롯되며, AgentTool 클래스는 위임 생성자 AgentTool(delegate)를 제공하므로 데코레이터 패턴을 사용하여 검색된 AppFunction 도구를 래핑할 수 있습니다.

import com.google.mlkit.genai.agent.AgentTool

class HitlConfirmationTool(
    delegate: AgentTool,
    private val onConfirm: suspend (toolName: String, args: Map<String, Any>) -> Boolean
) : AgentTool(delegate) {

    override suspend fun execute(args: Map<String, Any>): Map<String, Any?> {
        val confirmed = onConfirm(name, args)
        if (!confirmed) {
            return mapOf(
                "status" to "CANCELLED_BY_USER",
                "message" to "Action was declined by the user."
            )
        }
        return checkNotNull(delegate).execute(args)
    }
}

도구 설계를 위한 권장사항

  • 작업 공간 범위 지정: addAppFunctionIds() 메서드와 ACCESS_LEVEL_SELF 상수를 사용하여 에이전트의 작업 공간 (에이전트에 노출되는 도구 및 권한 집합)을 작업에 충분한 크기로 유지합니다. 작업 공간이 너무 크면 온디바이스 모델에서 오류가 발생할 가능성이 높아지고 컨텍스트 윈도우 토큰이 소모됩니다.
  • 설명을 명확하게 작성: Gemini Nano는 도구 및 매개변수 설명을 사용하여 도구를 호출할 시기를 결정합니다. 도구의 목적을 명확히 하고 예상되는 인수 형식 (예: 날짜 또는 ID)을 언급합니다.
  • 구조화된 오류 맵 반환: 예상되는 오류가 발생하면 mapOf("error" to "Order not found")와 같은 설명 오류 맵을 반환합니다. 이렇게 하면 Gemini Nano가 사용자에게 문제를 원활하게 설명할 수 있습니다. 도구에서 예기치 않은 예외가 발생하면 런타임이 자동으로 이를 포착하고 자체 수정을 위해 오류 메시지를 모델에 다시 반영합니다.
  • 반환 페이로드에 집중: 온디바이스 모델은 서버 모델보다 컨텍스트 기간이 작습니다. 전체 데이터베이스 행이나 원시 네트워크 응답을 덤프하는 대신 사용자의 쿼리에 답변하는 데 관련된 필드만 반환합니다.
  • Dispatchers.IO 디스패처에서 I/O 실행: 도구 내의 네트워크 요청과 Room 데이터베이스 쿼리가 항상 Dispatchers.IO 디스패처에서 실행되도록 하여 코루틴 호출자가 차단되지 않도록 합니다.

다음 단계