Создавайте агентные рабочие процессы с помощью Gemini Nano

Агентные рабочие процессы ML Kit на устройстве (com.google.mlkit:genai-agent, доступные в рамках программы предварительного тестирования AICore для разработчиков) позволяют создавать автономных многоэтапных разговорных агентов на базе Gemini Nano и системного сервиса Android AICore.

Класс AgentRunner, созданный на основе класса GenerativeModel, который обеспечивает прямой доступ к модели, автоматизирует полные циклы агентов: планирование многоэтапных действий, вызов типизированных инструментов (например, запрос к репозиторию приложений, вызов API бэкенда или чтение локальной базы данных), оценку результатов работы инструментов и управление контекстом разговора локально на устройстве с нулевой задержкой сети и полной конфиденциальностью пользователя. Класс GenerativeModel поддерживает управляемую клиентом историю многоэтапного диалога с помощью объекта List<Content> (с ролями USER и MODEL) и значения sessionId, а класс AgentRunner предоставляет управляемые многоэтапные сеансы (с помощью интерфейса AgentSession), которые автоматически записывают историю разговора в памяти на всех этапах, поэтому вам не нужно управлять расшифровками вручную. Статические объявления инструментов и системные инструкции также автоматически кешируются на устройстве, что сокращает время до первого токена без дополнительной настройки.

Основные разделы

  • Класс Agent объявляет инструкции, доступные инструменты и ограничения на выполнение с помощью метода Agent.builder(). По умолчанию автоматически устанавливается значение maxTurns = 10.
  • Класс AgentRunner. Создан на основе класса GenerativeModel. Он управляет автономными циклами рассуждений и сеансами с моделями на устройстве. По умолчанию безопасно для основного потока в Dispatchers.IO, а temperature по умолчанию имеет значение null (которое зависит от значения по умолчанию для модели; передайте temperature = 0.0f для жадного декодирования).
  • Интерфейс AgentSession. Дескриптор сеанса многоэтапного разговора, полученный с помощью метода runner.getOrCreateSession(sessionId), который автоматически записывает историю разговора в памяти на разных этапах с помощью методов runAsync(content) и close().
  • Интерфейс AgentEvent. Реактивный поток Flow Kotlin, который передает текстовые токены и обновления инструментов.
  • Класс AgentTool. Типизированные функции, вызываемые моделью и определенные с помощью Android AppFunctions (@AppFunctionDeclaration).
  • Класс GenAiException. Иерархия типизированных исключений (com.google.mlkit.genai.common.GenAiException), сигнализирующих о сбоях, таких как MAX_TURNS_EXCEEDED, NOT_AVAILABLE или BUSY.

Предварительные требования и зависимости

Прежде чем интегрировать Agent API, убедитесь, что ваша среда разработки и конфигурация Gradle соответствуют следующим требованиям.

Требования к платформе и оборудованию

Конфигурация Gradle

Добавьте зависимость genai-agent в файл build.gradle.kts модуля:

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 не требуется инструмент обработки аннотаций или плагин компилятора, для определения инструментов с помощью androidx.appfunctions необходим плагин appfunctions-compiler KSP.

Быстрый старт: инициализация агента

В следующем примере кода показано, как создать экземпляр агента и исполнителя на устройстве:

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)

Определите инструменты

Модель может вызывать типизированные инструменты для запроса данных приложения, связи с бэкенд-службами или запуска действий в приложении. Инструменты определяются декларативно с помощью Android AppFunctions (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",
        )
    }
}

Зарегистрируйте сгенерированный сервис в файле 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>

Ограничение доступа вызывающего абонента с помощью константы ACCESS_LEVEL_SELF

Функции AppFunctions не обязательно должны быть общедоступными для системы или внешних вызывающих объектов. В Android контроль доступа осуществляется через уровни доступа вызывающего абонента, а не через проверку его идентификационных данных.

По умолчанию (с использованием константы ACCESS_LEVEL_ANDROID_TRUSTED) AppFunctions могут вызываться сертифицированными платформой помощниками на устройстве, у которых есть системное разрешение 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() {

    /**
     * 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."
    }
}

Если ограничить ее с помощью константы ACCESS_LEVEL_SELF, платформа заблокирует внешним интерфейсам и системным помощникам возможность обнаруживать или выполнять функцию через IPC.

Настройте агент

Настройте экземпляр Agent с помощью системных инструкций и включите AppFunctions, а затем инициализируйте экземпляр 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)

При необходимости вызовите метод addAppFunctionIds("fetch_order_details", "cancel_order"), чтобы ограничить поиск определенными идентификаторами AppFunction, а не всеми функциями приложения.

Настройки агента по умолчанию

  • Ограничение количества ходов. В классе Agent.Builder по умолчанию используется maxTurns = 10 в качестве условия завершения, чтобы предотвратить бесконечные циклы вызова инструментов. Для стандартных рабочих процессов, связанных с разговорами или вызовом инструментов, вызывать метод setMaxTurns(10) вручную не нужно.
  • Детерминированное жадное декодирование. Класс AgentRunner задает для параметра temperature значение по умолчанию null, которое соответствует значению по умолчанию базовой модели. Чтобы обеспечить детерминированный выбор инструментов и соответствие схеме JSON на устройстве, передайте temperature = 0.0f (temperature) в конструктор AgentRunner. Не устанавливайте температуру, характерную для облачных моделей (> 0,4f), поскольку это повышает риск галлюцинаций и ухудшения схемы в мобильных моделях.

Многоэтапное выполнение и потоковая передача событий

Получите экземпляр AgentSession с помощью метода runner.getOrCreateSession(sessionId) в классе ViewModel Android. Сеанс возвращает поток Flow<AgentEvent>, который в реальном времени передает текстовые токены и обновления выполнения инструментов.

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() в интерфейсе 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)
    }

Выполнение за один шаг

Для разовых задач, когда историю диалога не нужно сохранять в ОЗУ (например, для добавления тегов к фотографиям или краткого пересказа текста), вызывайте метод 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)
    }

При прямом вызове метода runner.runSingleTurnAsync(content) экземпляр AgentRunner выделяет временный идентификатор сеанса и автоматически удаляет состояние разговора из памяти по завершении, предотвращая утечки памяти.

Жизненный цикл сеанса и управление памятью

История диалога хранится в памяти и индексируется по значению sessionId:

  • Изменения конфигурации. Сохранение экземпляра AgentSession в классе ViewModel Android позволяет автоматически сохранять историю разговора при повороте экрана без необходимости передавать строки при каждом повороте.
  • Вытеснение сеанса. Вызовите метод session.delete(), чтобы синхронно вытеснить сеанс, когда цепочка чата закрыта, или вызовите метод session.close() без приостановки в методе ViewModel.onCleared(), чтобы освободить ОЗУ.
  • Массовое удаление. Вызовите метод runner.deleteSession(sessionId), чтобы освободить определенный сеанс, или метод runner.clearAllSessions(), чтобы удалить все кешированные чаты.
  • Жизненный цикл исполнителя. Класс AgentRunner реализует интерфейс AutoCloseable. Если вы используете один и тот же объект runner на нескольких экранах, закрывайте отдельные дескрипторы AgentSession в методе ViewModel.onCleared() каждого экрана и вызывайте метод runner.close() при полном завершении работы объекта runner.
  • Параллельные сеансы. Один экземпляр AgentRunner может одновременно поддерживать несколько отдельных сеансов. Для каждого значения sessionId сохраняется отдельная история чата.

Обработка ошибок с помощью класса GenAiException

Сбои во время выполнения агента, например когда он достигает максимального количества обращений к инструментам, не генерируя ответ, или когда ИИ на устройстве временно недоступен, генерируются или выдаются как исключение GenAiException (com.google.mlkit.genai.common.GenAiException). Обрабатывайте сбои реактивно с помощью оператора 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
    }

Основные коды ошибок

  • GenAiException.ErrorCode.MAX_TURNS_EXCEEDED (-112): агент превысил максимальное количество реплик (maxTurns), не сгенерировав окончательный ответ.
  • GenAiException.ErrorCode.NOT_AVAILABLE (8): модель Gemini Nano или системный сервис AICore недоступны или не готовы.
  • GenAiException.ErrorCode.BUSY (9): система занята или приложение для звонков превысило квоту.