Tworzenie przepływów pracy opartych na agentach za pomocą Gemini Nano

Dzięki Agentic Workflows ML Kit na urządzeniu (com.google.mlkit:genai-agent, dostępnym w ramach programu wersji przedpremierowej dla programistów AICore) możesz tworzyć autonomiczne, wieloetapowe agenty konwersacyjne oparte na Gemini Nano i usłudze systemowej Android AICore.

Klasa AgentRunner, która jest oparta na klasie GenerativeModel zapewniającej bezpośredni dostęp do modelu, automatyzuje całe pętle agenta: planowanie wieloetapowych działań, wywoływanie wpisanych narzędzi (takich jak wysyłanie zapytania do repozytorium aplikacji, wywoływanie interfejsu API backendu lub odczytywanie lokalnej bazy danych), ocenianie wyników narzędzi i zarządzanie kontekstem rozmowy lokalnie na urządzeniu bez opóźnień sieci i z zachowaniem pełnej prywatności użytkownika. Klasa GenerativeModel obsługuje zarządzaną przez klienta historię wieloetapową za pomocą obiektu List<Content> (z rolami USER i MODEL) i wartości sessionId, natomiast klasa AgentRunner udostępnia zarządzane sesje wieloetapowe (za pomocą interfejsu AgentSession), które automatycznie rejestrują historię rozmów w pamięci, dzięki czemu nie musisz ręcznie zarządzać transkrypcjami. Statyczne deklaracje narzędzi i instrukcje systemowe są również automatycznie zapisywane w pamięci podręcznej na urządzeniu, co skraca czas oczekiwania na pierwszy token bez konieczności dodatkowej konfiguracji.

Podstawowe komponenty

  • Klasa Agent: deklaruje instrukcje, dostępne narzędzia i ograniczenia wykonywania za pomocą metody Agent.builder(). Domyślnie automatycznie ustawiana jest wartość maxTurns = 10.
  • Klasa AgentRunner: oparta na klasie GenerativeModel, która koordynuje autonomiczne pętle rozumowania i zarządzane sesje z modelami na urządzeniu. Domyślnie bezpieczne dla wątku głównego w Dispatchers.IO, przy czym temperature domyślnie ma wartość null (która odnosi się do domyślnej wartości modelu; przekaż temperature = 0.0f, aby użyć dekodowania zachłannego).
  • Interfejs AgentSession: uchwyt sesji rozmowy wieloetapowej o określonym zakresie uzyskany za pomocą metody runner.getOrCreateSession(sessionId), która automatycznie rejestruje historię rozmowy w pamięci w ramach poszczególnych etapów, za pomocą metod runAsync(content) i close().
  • Interfejs AgentEvent: reaktywny strumień Flow w języku Kotlin, który emituje tokeny tekstowe i aktualizacje narzędzi.
  • Klasa AgentTool: funkcje typowane, które mogą być wywoływane przez model, zdefiniowane za pomocą funkcji aplikacji na Androida (@AppFunctionDeclaration).
  • Klasa GenAiException: typowana hierarchia wyjątków (com.google.mlkit.genai.common.GenAiException) sygnalizująca stany niepowodzenia, takie jak MAX_TURNS_EXCEEDED, NOT_AVAILABLE lub BUSY.

Wymagania wstępne i zależności

Zanim zintegrujesz interfejs Agent API, upewnij się, że środowisko programistyczne i konfiguracja Gradle spełniają te wymagania.

Wymagania dotyczące platformy i sprzętu

Konfiguracja Gradle

Dodaj zależność genai-agent do pliku build.gradle.kts modułu:

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

Chociaż genai-agent nie wymaga procesora adnotacji ani wtyczki kompilatora, definiowanie narzędzi za pomocą androidx.appfunctions wymaga wtyczki appfunctions-compiler KSP.

Krótkie wprowadzenie: inicjowanie agenta

Poniższy przykładowy kod pokazuje, jak utworzyć instancję agenta i modułu uruchamiającego na urządzeniu:

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)

Definiowanie narzędzi

Model może wywoływać narzędzia z określonym typem, aby wysyłać zapytania o dane aplikacji, komunikować się z usługami backendu lub wywoływać działania w aplikacji. Narzędzia są definiowane deklaratywnie za pomocą funkcji aplikacji na Androida (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",
        )
    }
}

Zarejestruj wygenerowaną usługę w pliku AndroidManifest.xml aplikacji:

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

Ograniczanie dostępu elementu wywołującego za pomocą stałej ACCESS_LEVEL_SELF

Funkcje AppFunctions nie muszą być publiczne dla systemu ani rozmówców zewnętrznych. W Androidzie kontrola dostępu jest obsługiwana przez poziomy dostępu elementu wywołującego, a nie przez sprawdzanie tożsamości elementu wywołującego.

Domyślnie (przy użyciu stałej ACCESS_LEVEL_ANDROID_TRUSTED) funkcje AppFunctions mogą być wywoływane przez certyfikowanych przez platformę asystentów na urządzeniu, którzy mają uprawnienia systemowe EXECUTE_APP_FUNCTIONS. Jeśli narzędzie jest przeznaczone wyłącznie dla agenta wewnętrznego aplikacji (np. do zarządzania koszykami użytkowników lub prywatnymi rekordami bazy danych), ogranicz dostęp wywołującego za pomocą stałej 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."
    }
}

W przypadku ograniczenia za pomocą stałej ACCESS_LEVEL_SELF platforma blokuje platformom zewnętrznym i asystentom systemowym możliwość wykrywania lub wykonywania funkcji przez IPC.

Konfigurowanie agenta

Skonfiguruj instancję Agent z instrukcjami systemowymi i włącz AppFunctions, a następnie zainicjuj instancję 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)

Opcjonalnie możesz wywołać metodę addAppFunctionIds("fetch_order_details", "cancel_order"), aby ograniczyć wykrywanie wyłącznie do określonych identyfikatorów AppFunction, zamiast udostępniać wszystkie funkcje w aplikacji.

Ustawienia domyślne agenta

  • Limity tur: klasa Agent.Builder domyślnie ustawia maxTurns = 10 jako warunek zakończenia, aby zapobiec nieskończonym pętlom wywoływania narzędzi. W przypadku standardowych przepływów pracy związanych z rozmową lub wywoływaniem narzędzi nie musisz ręcznie wywoływać metody setMaxTurns(10).
  • Deterministyczne zachłanne dekodowanie: klasa AgentRunner ustawia domyślny parametr temperature na null, odwołując się do domyślnego modelu bazowego. Aby zapewnić deterministyczny wybór narzędzi i zgodność ze schematem JSON na urządzeniu, przekaż temperature = 0.0f (temperature) do konstruktora AgentRunner. Unikaj ustawiania temperatur w stylu chmury (> 0,4f), które zwiększają ryzyko halucynacji i pogorszenia schematu w modelach mobilnych.

Wieloetapowe wykonywanie i zdarzenia strumieniowe

Uzyskaj instancję AgentSession za pomocą metody runner.getOrCreateSession(sessionId) w klasie ViewModel Androida. Sesja zwraca strumień Flow<AgentEvent>, który emituje w czasie rzeczywistym tokeny tekstowe i aktualizacje dotyczące wykonywania narzędzi:

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

Przesyłanie strumieniowe tokenów tekstowych bezpośrednio

W przypadku interfejsów konwersacyjnych, w których szczegóły wykonywania narzędzia nie muszą być wyświetlane osobno w interfejsie, użyj funkcji rozszerzenia textTokens() w interfejsie 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)
    }

Wykonanie jednoetapowe

W przypadku jednorazowych zadań, w których historia dialogu nie powinna być przechowywana w pamięci RAM (np. tagowanie zdjęć lub tymczasowe podsumowanie tekstu), wywołaj bezpośrednio metodę 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)
    }

Gdy wywołujesz metodę runner.runSingleTurnAsync(content) bezpośrednio, instancja AgentRunner wewnętrznie przydziela tymczasowy identyfikator sesji i automatycznie usuwa stan rozmowy z pamięci po zakończeniu, co zapobiega wyciekom pamięci.

Cykl życia sesji i zarządzanie pamięcią

Historia wieloetapowego dialogu jest przechowywana w pamięci i indeksowana według wartości sessionId:

  • Zmiany konfiguracji: zachowanie instancji AgentSession w klasie ViewModel Androida pozwala automatycznie zachować historię rozmów podczas obracania ekranu bez konieczności przekazywania ciągów znaków przy każdym obrocie.
  • Usuwanie sesji: wywołaj metodę session.delete(), aby synchronicznie usunąć sesję, gdy wątek rozmowy zostanie odrzucony, lub wywołaj metodę session.close(), która nie zawiesza działania, w metodzie ViewModel.onCleared(), aby zwolnić pamięć RAM.
  • Zbiorcze usuwanie: wywołaj metodę runner.deleteSession(sessionId), aby zwolnić konkretną sesję, lub metodę runner.clearAllSessions(), aby usunąć wszystkie rozmowy z pamięci podręcznej.
  • Cykl życia modułu uruchamiającego: klasa AgentRunner implementuje interfejs AutoCloseable. Jeśli udostępniasz moduł uruchamiający na różnych ekranach, zamknij poszczególne uchwyty AgentSession w metodzie ViewModel.onCleared() każdego ekranu i wywołaj metodę runner.close(), gdy całkowicie wyłączasz moduł uruchamiający.
  • Sesje równoczesne: pojedyncza instancja AgentRunner może jednocześnie obsługiwać wiele różnych sesji. Każda wartość sessionId zachowuje odrębną historię rozmów.

Obsługa błędów za pomocą klasy GenAiException

Błędy czasu działania podczas wykonywania agenta, np. gdy agent osiągnie maksymalną liczbę tur narzędzia bez wygenerowania odpowiedzi lub gdy AI na urządzeniu jest tymczasowo niedostępna, są emitowane lub zgłaszane jako wyjątek GenAiException (com.google.mlkit.genai.common.GenAiException). Reaguj na błędy, używając operatora catch Kotlin Coroutines Flow:

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
    }

Kluczowe kody błędów

  • GenAiException.ErrorCode.MAX_TURNS_EXCEEDED (-112): agent przekroczył maksymalny limit tur (limit maxTurns) bez wygenerowania ostatecznej odpowiedzi.
  • GenAiException.ErrorCode.NOT_AVAILABLE (8): model Gemini Nano lub usługa systemowa AICore jest niedostępna lub niegotowa.
  • GenAiException.ErrorCode.BUSY (9): system jest zajęty lub aplikacja do dzwonienia przekroczyła limit.