Tools definieren

Mit Tools kann Ihr On-Device-Agent auf In-App-Daten zugreifen, mit Backend-Diensten kommunizieren und Aktionen im Namen des Nutzers ausführen.

So funktionieren Tools

Wenn ein Nutzer einen Prompt sendet, führt die Klasse AgentRunner eine Agentenschleife in vier Phasen aus:

  1. Beobachten: Gemini Nano analysiert den Prompt des Nutzers und den aktuellen Kontext der Unterhaltung anhand der registrierten Tooldeklarationen (dem Aktionsbereich des Agenten).
  2. Grund: Das Modell bestimmt, ob ein In-App-Tool erforderlich ist. Wenn ein Tool erforderlich ist, gibt Gemini Nano einen strukturierten Toolaufruf aus, der den Funktionsnamen und die typisierten Argumente enthält.
  3. Handeln: Die AgentRunner-Instanz fängt diesen Aufruf automatisch ab und führt die entsprechende Kotlin-Funktion in Ihrer App aus.
  4. Feedback auswerten: Die Ausgabe des Tools wird an Gemini Nano zurückgegeben. Das Modell wertet das Feedback aus und antwortet entweder dem Nutzer oder ruft ein anderes Tool auf, bis eine Beendigungsbedingung erfüllt ist (das Ziel wurde erreicht oder das maxTurns-Limit wurde erreicht).

Deklarative Tools mit Android AppFunctions

Die empfohlene Methode zum Definieren von Tools ist deklarativ mit Android AppFunctions (androidx.appfunctions). AppFunctions bieten ein standardisiertes Plattformmodell zum Deklarieren typisierter In-App-Funktionen mit automatischer Schemagenerierung.

1. Gradle-Abhängigkeiten hinzufügen

Fügen Sie die AppFunctions-Artefakte zusammen mit genai-agent in die build.gradle.kts-Datei Ihres Moduls ein:

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. Toolmethoden mit der Annotation @AppFunctionDeclaration versehen

Annotieren Sie öffentliche Tool-Methoden mit der Annotation @AppFunctionDeclaration(isDescribedByKDoc = true). Parameterbeschreibungen sind in Standard-KDoc-@param-Tags dokumentiert.

AppFunctions erfordern, dass Rückgabewerte entweder primitive Typen (wie String, Int oder Boolean) oder strukturierte Datenklassen sind, die mit der Annotation @AppFunctionSerializable versehen sind.

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."
            }
        )
    }
}

Registrieren Sie den generierten Dienst in der Datei AndroidManifest.xml Ihrer App:

<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. Aufruferzugriff mit der Konstante ACCESS_LEVEL_SELF einschränken

AppFunctions müssen nicht für das System oder externe Assistenten öffentlich sein. In Android wird die Zugriffssteuerung über Zugriffsebenen für Aufrufer und nicht über manuelle Prüfungen der Aufruferidentität verwaltet.

Standardmäßig (mit der Konstante ACCESS_LEVEL_ANDROID_TRUSTED) kann eine AppFunction von plattformzertifizierten On-Device-Assistenten mit der Systemberechtigung EXECUTE_APP_FUNCTIONS gefunden und ausgeführt werden. Wenn Sie jedoch Funktionen entwickeln, die ausschließlich für den internen Agenten Ihrer App bestimmt sind (z. B. das Prüfen von Einkaufswagen, das Zugreifen auf lokale Tokens oder das Ausführen von In-App-Aktionen), können Sie den Zugriff mit der Konstante ACCESS_LEVEL_SELF ausschließlich auf Ihre eigene Anwendung beschränken:

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."
        )
    }
}

Die Plattform unterstützt drei Zugriffsebenen für Anrufer:

  • ACCESS_LEVEL_SELF (100): Streng auf die Hostanwendung selbst beschränkt. Externe Assistenten, Systemoberflächen und andere Apps können die Funktion nicht über IPC finden oder aufrufen. Verwenden Sie diese Ebene für vertrauliche Vorgänge oder Tools, die nur dem internen Agent Ihrer Anwendung dienen sollen.
  • ACCESS_LEVEL_SYSTEM (200): Beschränkt auf privilegierte Systemaufrufer mit der Berechtigung android.Manifest.permission#EXECUTE_APP_FUNCTIONS_SYSTEM (z. B. der System-Launcher oder die Ansicht „Letzte Apps“) und die Host-App selbst.
  • ACCESS_LEVEL_ANDROID_TRUSTED (300, Standard): Offen für plattformzertifizierte On-Device-Agents mit der Berechtigung EXECUTE_APP_FUNCTIONS, Systemoberflächen und die Host-App.

4. Tools beim KI-Agenten registrieren

Sie können AppFunctions entweder automatisch mit der Methode enableAppFunctions() oder programmatisch mit der Methode AppFunctionAgentTools.discover() beim Agenten registrieren:

Automatische Erkennung mit der Methode enableAppFunctions()

Für einfache Apps, bei denen AppFunctions automatisch ausgeführt werden, wenn das Modell sie aufruft:

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)

Optional können Sie die Methode addAppFunctionIds("fetch_order_details", "cancel_order") aufrufen, um die Erkennung strikt auf bestimmte AppFunction-IDs zu beschränken, anstatt alle In-App-Funktionen offenzulegen.

Programmatische Erkennung mit der AppFunctionAgentTools.discover()-Methode

Wenn Ihre App erkannte AppFunctions prüfen, filtern oder dekorieren muss, bevor sie beim Agenten registriert werden:

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)

Decorator-Muster und Human in the Loop

Bei sensiblen Vorgängen wie Stornierungen, Zahlungen oder dem Löschen von Nutzerdaten sollte ein Human in the Loop (HITL) einbezogen werden, indem eine Bestätigung durch den Nutzer erforderlich ist. Alle Tools, die der Klasse Agent hinzugefügt werden, stammen aus Android AppFunctions. Die Klasse AgentTool bietet einen delegierenden Konstruktor AgentTool(delegate), mit dem Sie gefundene AppFunction-Tools mit dem Decorator-Muster umschließen können:

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)
    }
}

Best Practices für das Tool-Design

  • Aktionsbereich eingrenzen: Halten Sie den Aktionsbereich des KI-Agenten (die Tools und Berechtigungen, die dem KI-Agenten zur Verfügung stehen) so klein wie möglich, damit er seine Aufgabe erfüllen kann. Verwenden Sie dazu die Methode addAppFunctionIds() und die Konstante ACCESS_LEVEL_SELF. Ein zu großer Aktionsbereich macht ein On-Device-Modell fehleranfälliger und verbraucht Kontextfenster-Tokens.
  • Seien Sie in Beschreibungen explizit: Gemini Nano verwendet Tool- und Parameterbeschreibungen, um zu entscheiden, wann ein Tool aufgerufen werden soll. Geben Sie den Zweck des Tools klar an und erwähnen Sie die erwarteten Argumentformate (z. B. Datumsangaben oder IDs).
  • Strukturierte Fehlerzuordnungen zurückgeben: Geben Sie beschreibende Fehlerzuordnungen wie mapOf("error" to "Order not found") zurück, wenn ein erwarteter Fehler auftritt. So kann Gemini Nano das Problem dem Nutzer verständlich erklären. Wenn ein Tool eine unerwartete Ausnahme auslöst, wird sie von der Laufzeit automatisch abgefangen und die Fehlermeldung wird zur Selbstkorrektur an das Modell zurückgegeben.
  • Rückgabe-Nutzlasten fokussiert halten: On-Device-Modelle haben kleinere Kontextfenster als Servermodelle. Geben Sie nur die Felder zurück, die für die Beantwortung der Nutzeranfrage relevant sind, anstatt ganze Datenbankzeilen oder rohe Netzwerkantworten auszugeben.
  • E/A-Vorgänge im Dispatchers.IO-Dispatcher ausführen: Netzwerkanfragen und Room-Datenbankabfragen in Tools müssen immer im Dispatchers.IO-Dispatcher ausgeführt werden, damit der Coroutine-Aufrufer nicht blockiert wird.

Nächste Schritte