Membangun alur kerja agentic dengan Gemini Nano

Dengan Agentic Workflows di perangkat ML Kit (com.google.mlkit:genai-agent, tersedia melalui program Pratinjau Developer AICore), Anda dapat membangun agen percakapan multi-turn otonom yang didukung oleh Gemini Nano dan layanan sistem Android AICore.

Dibangun di atas class GenerativeModel, yang menyediakan akses model langsung, class AgentRunner mengotomatiskan loop agentic lengkap: merencanakan tindakan multi-langkah, memanggil alat yang diketik (seperti membuat kueri repositori aplikasi, memanggil API backend, atau membaca database lokal), mengevaluasi hasil alat, dan mengelola konteks percakapan secara lokal di perangkat dengan latensi jaringan nol dan privasi pengguna penuh. Meskipun class GenerativeModel mendukung histori multi-turn yang dikelola klien menggunakan objek List<Content> (dengan peran USER dan MODEL) dan nilai sessionId, class AgentRunner menyediakan sesi multi-turn terkelola (menggunakan antarmuka AgentSession) yang merekam histori percakapan dalam memori secara otomatis di seluruh giliran sehingga Anda tidak perlu mengelola transkrip secara manual. Deklarasi alat statis dan petunjuk sistem juga otomatis di-cache di perangkat, sehingga mengurangi latensi waktu ke token pertama tanpa konfigurasi tambahan.

Komponen inti

  • Class Agent: Mendeklarasikan petunjuk, alat yang tersedia, dan guardrail eksekusi menggunakan metode Agent.builder(). Default-nya adalah maxTurns = 10 secara otomatis.
  • Class AgentRunner: Dibangun di atas class GenerativeModel, mengatur reasoning loop otonom dan sesi terkelola dengan model di perangkat. Main-safe secara default di Dispatchers.IO, dengan temperature ditetapkan secara default ke null (yang mengikuti default model; teruskan temperature = 0.0f untuk decoding greedy).
  • Antarmuka AgentSession: Handle sesi percakapan multi-turn tercakup yang diperoleh menggunakan metode runner.getOrCreateSession(sessionId) yang otomatis mencatat histori percakapan dalam memori di seluruh giliran, dengan metode runAsync(content) dan close().
  • Antarmuka AgentEvent: Aliran data Flow Kotlin reaktif yang memunculkan token teks dan update alat.
  • Class AgentTool: Fungsi yang diketik yang dapat dipanggil oleh model, yang ditentukan menggunakan AppFunctions Android (@AppFunctionDeclaration).
  • Class GenAiException: Hierarki pengecualian yang diketik (com.google.mlkit.genai.common.GenAiException) yang menandakan status kegagalan seperti MAX_TURNS_EXCEEDED, NOT_AVAILABLE, atau BUSY.

Prasyarat dan dependensi

Pastikan lingkungan pengembangan dan konfigurasi Gradle Anda memenuhi persyaratan berikut sebelum mengintegrasikan Agent API.

Persyaratan platform dan hardware

Konfigurasi Gradle

Tambahkan dependensi genai-agent ke file build.gradle.kts modul Anda:

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

Meskipun genai-agent sendiri tidak memerlukan pemroses anotasi atau plugin compiler, menentukan alat dengan androidx.appfunctions memerlukan plugin KSP appfunctions-compiler.

Panduan memulai: Melakukan inisialisasi agen

Contoh kode berikut menunjukkan cara membuat instance agen dan runner di perangkat:

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)

Menentukan alat

Model ini dapat memanggil alat yang diketik untuk mengkueri data aplikasi, berkomunikasi dengan layanan backend, atau memicu tindakan dalam aplikasi. Alat ditentukan secara deklaratif menggunakan AppFunctions Android (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",
        )
    }
}

Daftarkan layanan yang dihasilkan dalam file AndroidManifest.xml aplikasi Anda:

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

Membatasi akses pemanggil dengan konstanta ACCESS_LEVEL_SELF

AppFunctions tidak harus bersifat publik untuk sistem atau pemanggil eksternal. Di Android, kontrol akses ditangani melalui tingkat akses pemanggil, bukan memeriksa identitas pemanggil.

Secara default (menggunakan konstanta ACCESS_LEVEL_ANDROID_TRUSTED), AppFunction dapat dipanggil oleh asisten di perangkat yang disertifikasi platform yang memiliki izin sistem EXECUTE_APP_FUNCTIONS. Jika alat Anda benar-benar ditujukan untuk agen internal aplikasi (misalnya, mengelola keranjang pengguna atau data database pribadi), batasi akses pemanggil menggunakan konstanta 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."
    }
}

Jika dibatasi dengan konstanta ACCESS_LEVEL_SELF, platform akan memblokir platform eksternal dan asisten sistem agar tidak menemukan atau menjalankan fungsi melalui IPC.

Mengonfigurasi agen

Konfigurasikan instance Agent dengan petunjuk sistem dan aktifkan AppFunctions, lalu lakukan inisialisasi instance 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)

Secara opsional, panggil metode addAppFunctionIds("fetch_order_details", "cancel_order") untuk membatasi penemuan secara ketat ke ID AppFunction tertentu, bukan mengekspos semua fungsi dalam aplikasi.

Default agen

  • Batas giliran: Class Agent.Builder menetapkan maxTurns = 10 secara default sebagai kondisi penghentian untuk mencegah loop panggilan alat yang tak terbatas. Anda tidak perlu memanggil metode setMaxTurns(10) secara manual untuk alur kerja percakapan atau panggilan alat standar.
  • Decoding greedy deterministik: Class AgentRunner menetapkan parameter temperature default ke null, yang mengacu pada default model yang mendasarinya. Untuk pemilihan alat deterministik dan kepatuhan skema JSON di perangkat, teruskan temperature = 0.0f (temperature) ke konstruktor AgentRunner. Hindari menyetel suhu bergaya cloud (> 0,4f), yang meningkatkan risiko halusinasi dan penurunan kualitas skema pada model seluler.

Eksekusi multi-turn dan peristiwa streaming

Dapatkan instance AgentSession menggunakan metode runner.getOrCreateSession(sessionId) di dalam class ViewModel Android. Sesi ini menampilkan streaming Flow<AgentEvent> yang memunculkan token teks real-time dan update eksekusi alat:

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

Men-streaming token teks secara langsung

Untuk antarmuka percakapan yang detail eksekusi alatnya tidak perlu ditampilkan secara terpisah di UI, gunakan fungsi ekstensi textTokens() di antarmuka 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)
    }

Eksekusi satu giliran

Untuk tugas satu kali yang histori dialognya tidak boleh disimpan dalam RAM (misalnya, pemberian tag foto atau ringkasan teks sementara), panggil metode runner.runSingleTurnAsync(content) secara langsung:

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

Saat memanggil metode runner.runSingleTurnAsync(content) secara langsung, instance AgentRunner mengalokasikan ID sesi sementara secara internal dan otomatis menghapus status percakapan dari memori setelah selesai, sehingga mencegah kebocoran memori.

Siklus proses sesi dan manajemen memori

Histori dialog multi-turn disimpan dalam memori dan diindeks berdasarkan nilai sessionId-nya:

  • Perubahan konfigurasi: Mempertahankan instance AgentSession di dalam class ViewModel Android akan mempertahankan histori percakapan di seluruh rotasi layar secara otomatis tanpa perlu menghubungkan string di setiap giliran.
  • Pengeluaran sesi: Panggil metode session.delete() untuk mengeluarkan sesi secara sinkron saat rangkaian percakapan ditutup, atau panggil metode session.close() yang tidak ditangguhkan dalam metode ViewModel.onCleared() untuk mengosongkan RAM.
  • Pengusiran massal: Panggil metode runner.deleteSession(sessionId) untuk mengosongkan sesi tertentu, atau metode runner.clearAllSessions() untuk mengusir semua percakapan yang di-cache.
  • Siklus proses runner: Class AgentRunner mengimplementasikan antarmuka AutoCloseable. Saat membagikan runner di seluruh layar, tutup setiap handle AgentSession di metode ViewModel.onCleared() setiap layar, dan panggil metode runner.close() saat menonaktifkan runner sepenuhnya.
  • Sesi serentak: Satu instance AgentRunner dapat menghosting beberapa sesi yang berbeda secara bersamaan. Setiap nilai sessionId mempertahankan histori percakapan yang terisolasi.

Penanganan error dengan class GenAiException

Kegagalan runtime selama eksekusi agen, seperti saat agen mencapai giliran alat maksimum tanpa menghasilkan respons atau saat AI di perangkat tidak tersedia untuk sementara, akan ditampilkan atau ditampilkan sebagai pengecualian GenAiException (com.google.mlkit.genai.common.GenAiException). Tangani kegagalan secara reaktif menggunakan operator catch Flow Coroutine Kotlin:

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
    }

Kode error utama

  • GenAiException.ErrorCode.MAX_TURNS_EXCEEDED (-112): Agen melebihi batas giliran maksimum (batas maxTurns) tanpa menghasilkan respons akhir.
  • GenAiException.ErrorCode.NOT_AVAILABLE (8): Model Gemini Nano atau layanan sistem AICore tidak tersedia atau belum siap.
  • GenAiException.ErrorCode.BUSY (9): Sistem sedang sibuk atau aplikasi panggilan telah melebihi kuotanya.