定義工具

工具可讓裝置端代理存取應用程式資料、與後端服務通訊,以及代表使用者執行動作。

工具的運作方式

使用者提交提示詞時,AgentRunner 類別會執行代理迴圈,分為四個階段:

  1. 觀察:Gemini Nano 會根據已註冊的工具宣告 (代理的動作空間),分析使用者的提示和目前的對話內容。
  2. 原因:模型會判斷是否需要應用程式內工具。如果需要工具,Gemini Nano 會發出結構化工具叫用,其中包含函式名稱和類型引數。
  3. 執行:AgentRunner 執行個體會自動攔截這個叫用,並在應用程式中執行對應的 Kotlin 函式。
  4. 評估意見回饋:工具的輸出內容會回傳給 Gemini Nano。模型會評估意見回饋,並回答使用者或呼叫其他工具,直到符合終止條件 (達成目標或達到 maxTurns 限制)。

使用 Android AppFunctions 的宣告式工具

建議使用 Android AppFunctions (androidx.appfunctions) 以宣告方式定義工具。AppFunctions 提供標準化平台模型,可宣告具類型的應用程式內功能,並自動產生結構定義。

1. 新增 Gradle 依附元件

在模組的 build.gradle.kts 檔案中,將 AppFunctions 構件與 genai-agent 一併加入:

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 中,存取控管是透過呼叫端存取層級管理,而非手動檢查呼叫端身分。

根據預設 (使用 ACCESS_LEVEL_ANDROID_TRUSTED 常數),具備 EXECUTE_APP_FUNCTIONS 系統權限的平台認證裝置端助理,可以探索及執行 AppFunction。不過,如果您建構的功能僅供應用程式內部代理程式使用 (例如檢查使用者購物車、存取本機權杖或執行應用程式內動作),可以使用 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() 方法自動向代理註冊 AppFunction,也可以使用 AppFunctionAgentTools.discover() 方法透過程式輔助方式註冊:

使用 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),因此您可以使用 Decorator 模式包裝探索到的 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 調度器上執行,以免封鎖協同程式呼叫端。

後續步驟