Agentische Workflows mit Gemini Nano erstellen

Mit den On-Device-Agentic Workflows von ML Kit (com.google.mlkit:genai-agent, verfügbar über das AICore Entwicklervorschau-Programm) können Sie autonome, mehrstufige Konversations-Agents erstellen, die auf Gemini Nano und dem Android AICore-Systemdienst basieren.

Die Klasse AgentRunner basiert auf der Klasse GenerativeModel, die direkten Modellzugriff bietet, und automatisiert vollständige Agentenschleifen: Planung mehrstufiger Aktionen, Aufruf typisierter Tools (z. B. Abfrage eines App-Repositorys, Aufruf einer Backend-API oder Lesen einer lokalen Datenbank), Auswertung von Toolergebnissen und Verwaltung des Konversationskontexts lokal auf dem Gerät ohne Netzwerklatenz und mit vollständigem Datenschutz für Nutzer. Die Klasse GenerativeModel unterstützt den vom Client verwalteten Multi-Turn-Verlauf mit einem List<Content>-Objekt (mit den Rollen USER und MODEL) und einem sessionId-Wert. Die Klasse AgentRunner bietet dagegen verwaltete Multi-Turn-Sitzungen (mit der Schnittstelle AgentSession), die den Gesprächsverlauf im Arbeitsspeicher automatisch über mehrere Turns hinweg aufzeichnen, sodass Sie Transkripte nicht manuell verwalten müssen. Statische Tooldeklarationen und Systemanweisungen werden ebenfalls automatisch auf dem Gerät im Cache gespeichert. Dadurch wird die Latenz bis zum ersten Token ohne zusätzliche Konfiguration reduziert.

Kernkomponenten

  • Die Klasse Agent: Deklariert Anweisungen, verfügbare Tools und Ausführungsleitlinien mit der Methode Agent.builder(). Die Standardeinstellung ist automatisch maxTurns = 10.
  • Die Klasse AgentRunner: Basiert auf der Klasse GenerativeModel und orchestriert autonome Reasoning Loops und verwaltete Sitzungen mit On-Device-Modellen. Standardmäßig sicher für den Hauptthread auf Dispatchers.IO, wobei temperature standardmäßig auf null gesetzt ist (was dem Standardwert des Modells entspricht; übergeben Sie temperature = 0.0f für Greedy Decoding).
  • Die AgentSession-Schnittstelle: Ein Handle für eine Multi-Turn-Unterhaltungssitzung, das mit der Methode runner.getOrCreateSession(sessionId) abgerufen wird und den Unterhaltungsverlauf im Arbeitsspeicher automatisch über die Methoden runAsync(content) und close() aufzeichnet.
  • Die AgentEvent-Schnittstelle: Der reaktive Kotlin-Flow-Stream, der Text-Tokens und Tool-Updates ausgibt.
  • Die Klasse AgentTool: Typisierte Funktionen, die vom Modell aufgerufen werden können und mit Android AppFunctions (@AppFunctionDeclaration) definiert werden.
  • Die Klasse GenAiException: Typisierte Ausnahmehierarchie (com.google.mlkit.genai.common.GenAiException), die Fehlerzustände wie MAX_TURNS_EXCEEDED, NOT_AVAILABLE oder BUSY signalisiert.

Voraussetzungen und Abhängigkeiten

Ihre Entwicklungsumgebung und Gradle-Konfiguration müssen die folgenden Anforderungen erfüllen, bevor Sie die Agent API einbinden können.

Plattform- und Hardwareanforderungen

Gradle-Konfiguration

Fügen Sie der Datei build.gradle.kts Ihres Moduls die Abhängigkeit genai-agent hinzu:

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 selbst erfordert zwar keinen Annotationsprozessor oder Compiler-Plug-in, für die Definition von Tools mit androidx.appfunctions ist jedoch das appfunctions-compiler-KSP-Plug-in erforderlich.

Kurzanleitung: Agent initialisieren

Im folgenden Codebeispiel wird gezeigt, wie ein On-Device-Agent und ein Runner instanziiert werden:

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)

Tools definieren

Das Modell kann typisierte Tools aufrufen, um App-Daten abzufragen, mit Backend-Diensten zu kommunizieren oder In-App-Aktionen auszulösen. Tools werden deklarativ mit Android AppFunctions (androidx.appfunctions) definiert:

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

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>

Aufruferzugriff mit der Konstante ACCESS_LEVEL_SELF einschränken

AppFunctions müssen nicht für das System oder externe Aufrufer öffentlich sein. In Android wird die Zugriffssteuerung über Zugriffsebenen des Aufrufers und nicht über die Prüfung der Identität des Aufrufers verwaltet.

Standardmäßig (mit der Konstante ACCESS_LEVEL_ANDROID_TRUSTED) können AppFunctions von plattformzertifizierten On-Device-Assistenten aufgerufen werden, die die Systemberechtigung EXECUTE_APP_FUNCTIONS haben. Wenn Ihr Tool ausschließlich für den internen Agenten Ihrer Anwendung vorgesehen ist (z. B. zum Verwalten von Einkaufswagen oder privaten Datenbankeinträgen), beschränken Sie den Zugriff des Aufrufers mit der Konstante 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."
    }
}

Wenn die Funktion mit der Konstante ACCESS_LEVEL_SELF eingeschränkt ist, blockiert die Plattform externe Oberflächen und Systemassistenten, damit sie die Funktion nicht über IPC erkennen oder ausführen können.

KI-Agenten konfigurieren

Konfigurieren Sie eine Agent-Instanz mit Systemanweisungen, aktivieren Sie AppFunctions und initialisieren Sie dann die AgentRunner-Instanz:

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)

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.

Standardeinstellungen für KI-Agenten

  • Rundenlimits: Die Klasse Agent.Builder verwendet standardmäßig maxTurns = 10 als Abbruchbedingung, um Endlosschleifen bei Toolaufrufen zu verhindern. Für Standardworkflows für Unterhaltungen oder Toolaufrufe müssen Sie die Methode setMaxTurns(10) nicht manuell aufrufen.
  • Deterministische Greedy-Decodierung: Die Klasse AgentRunner setzt den Standardparameter temperature auf null und verwendet den Standardwert des zugrunde liegenden Modells. Für eine deterministische Toolauswahl und die Einhaltung des JSON-Schemas auf dem Gerät übergeben Sie temperature = 0.0f (temperature) an den AgentRunner-Konstruktor. Vermeiden Sie das Festlegen von Temperaturen im Cloud-Stil (> 0,4f), da dies das Risiko von Halluzinationen und die Verschlechterung des Schemas bei mobilen Modellen erhöht.

Ausführung mit mehreren Durchgängen und Streaming-Ereignisse

Rufen Sie eine AgentSession-Instanz mit der Methode runner.getOrCreateSession(sessionId) in einer Android-ViewModel-Klasse ab. Die Sitzung gibt einen Flow<AgentEvent>-Stream zurück, der Echtzeit-Text-Tokens und Aktualisierungen zur Toolausführung ausgibt:

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

Texttokens direkt streamen

Verwenden Sie für konversationelle Oberflächen, bei denen die Details zur Ausführung des Tools nicht separat in der Benutzeroberfläche angezeigt werden müssen, die Erweiterungsfunktion textTokens() auf der Schnittstelle Flow<AgentEvent>:

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

Ausführung in einem Schritt

Für einmalige Aufgaben, bei denen der Dialogverlauf nicht im RAM gespeichert werden soll (z. B. Fototagging oder kurzlebige Textzusammenfassung), rufen Sie die Methode runner.runSingleTurnAsync(content) direkt auf:

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

Wenn Sie die Methode runner.runSingleTurnAsync(content) direkt aufrufen, weist die AgentRunner-Instanz intern eine temporäre Sitzungs-ID zu und löscht den Unterhaltungsstatus nach Abschluss automatisch aus dem Arbeitsspeicher, um Speicherlecks zu vermeiden.

Sitzungslebenszyklus und Speicherverwaltung

Der Verlauf von Unterhaltungen über mehrere Themen wird im Speicher gespeichert und nach dem Wert sessionId indexiert:

  • Konfigurationsänderungen: Wenn die AgentSession-Instanz in einer Android-ViewModel-Klasse beibehalten wird, bleibt der Unterhaltungsverlauf bei Bildschirmdrehungen automatisch erhalten, ohne dass bei jeder Drehung Strings verarbeitet werden müssen.
  • Sitzung entfernen: Rufen Sie die Methode session.delete() auf, um eine Sitzung synchron zu entfernen, wenn ein Unterhaltungsthread geschlossen wird. Alternativ können Sie die nicht-suspendierende Methode session.close() in der Methode ViewModel.onCleared() aufrufen, um RAM freizugeben.
  • Bulk-Entfernung: Rufen Sie die Methode runner.deleteSession(sessionId) auf, um eine bestimmte Sitzung freizugeben, oder die Methode runner.clearAllSessions(), um alle im Cache gespeicherten Unterhaltungen zu entfernen.
  • Runner-Lebenszyklus: Die Klasse AgentRunner implementiert die Schnittstelle AutoCloseable. Wenn Sie einen Runner auf mehreren Bildschirmen freigeben, schließen Sie die einzelnen AgentSession-Handles in der ViewModel.onCleared()-Methode jedes Bildschirms und rufen Sie die runner.close()-Methode auf, wenn Sie den Runner vollständig herunterfahren.
  • Gleichzeitige Sitzungen: Eine einzelne AgentRunner-Instanz kann mehrere unterschiedliche Sitzungen gleichzeitig hosten. Für jeden sessionId-Wert wird ein isolierter Unterhaltungsverlauf beibehalten.

Fehlerbehandlung mit der Klasse GenAiException

Laufzeitfehler während der Ausführung des KI-Agenten, z. B. wenn der KI-Agent die maximale Anzahl von Tool-Runden erreicht, ohne eine Antwort zu generieren, oder wenn die On-Device-KI vorübergehend nicht verfügbar ist, werden als GenAiException-Ausnahme (com.google.mlkit.genai.common.GenAiException) ausgegeben oder ausgelöst. Behandeln Sie Fehler reaktiv mit dem Kotlin Coroutines Flow-Operator 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
    }

Wichtige Fehlercodes

  • GenAiException.ErrorCode.MAX_TURNS_EXCEEDED (-112): Der KI-Agent hat das maximale Limit für die Anzahl der Turns (das maxTurns-Limit) überschritten, ohne eine endgültige Antwort zu generieren.
  • GenAiException.ErrorCode.NOT_AVAILABLE (8): Das Gemini Nano-Modell oder der AICore-Systemdienst ist nicht verfügbar oder bereit.
  • GenAiException.ErrorCode.BUSY (9): Das System ist ausgelastet oder die aufrufende Anwendung hat ihr Kontingent überschritten.