ساختن گردش‌های کار عاملی با Gemini Nano

با گردش‌های کاری عاملی درون‌دستگاهی ML Kit (com.google.mlkit:genai-agent، که ازطریق برنامه پیش‌نمای توسعه‌دهندگان نرم‌افزار AICore دردسترس است)، می‌توانید عامل‌های مکالمه‌ای چندنوبتی خودکار با پشتیبانی Gemini Nano و سرویس سیستم Android AICore بسازید.

کلاس AgentRunner که بر پایه کلاس GenerativeModel ساخته شده است و دسترسی مستقیم به مدل را فراهم می‌کند، حلقه‌های عاملی کامل را خودکارسازی می‌کند: برنامه‌ریزی کنش‌های چندمرحله‌ای، فراخوانی ابزارهای تایپ‌شده (مانند پرس‌وجو از مخزن برنامه، فراخوانی میانای برنامه‌سازی کاربردی پس‌بست، یا خواندن پایگاه داده محلی)، ارزیابی نتایج ابزار، و مدیریت بافت مکالمه به‌صورت محلی در دستگاه با تأخیر شبکه صفر و حریم خصوصی کامل کاربر. درحالی‌که کلاس GenerativeModel از سابقه چندنوبتی مدیریت‌شده توسط کارخواه بااستفاده از شیء List<Content> (با نقش‌های USER و MODEL) و مقدار sessionId پشتیبانی می‌کند، کلاس AgentRunner جلسه‌های چندنوبتی مدیریت‌شده (بااستفاده از میانای AgentSession) ارائه می‌دهد که سابقه مکالمه درون‌حافظه‌ای را به‌طور خودکار درطول نوبت‌ها ضبط می‌کند تا مجبور نباشید ترانویسی‌ها را به‌صورت دستی مدیریت کنید. دستورالعمل‌های سیستم و اعلامیه‌های ابزار ایستا نیز به‌طور خودکار در دستگاه ذخیره می‌شوند، که این امر زمان تأخیر تا اولین کد را بدون نیاز به پیکربندی اضافی کاهش می‌دهد.

اجزای اصلی

  • کلاس Agent: دستورالعمل‌ها، ابزارهای دردسترس، و حفاظ‌های اجرا را بااستفاده از روش Agent.builder() اعلام می‌کند. به‌طور خودکار به maxTurns = 10 پیش‌فرض می‌شود.
  • کلاس AgentRunner: بر روی کلاس GenerativeModel ساخته شده است و حلقه‌های استدلال خودکار و جلسات مدیریت‌شده را با مدل‌های درون‌دستگاهی هماهنگ می‌کند. Main-safe by default on Dispatchers.IO, with temperature defaulting to null (which defers to the model's default; pass temperature = 0.0f for greedy decoding).
  • میانای AgentSession: دسته جلسه مکالمه چندنوبتی با دامنه محدود که بااستفاده از روش runner.getOrCreateSession(sessionId) به‌دست آمده است و سابقه مکالمه در حافظه را به‌طور خودکار در نوبت‌ها با روش‌های runAsync(content) و close() ضبط می‌کند.
  • میانای AgentEvent: جریان واکنشی Kotlin Flow که نشان‌های نوشتاری و به‌روزرسانی‌های ابزار را منتشر می‌کند.
  • کلاس 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 به افزونه KSP appfunctions-compiler نیاز دارد.

شروع سریع: مقداردهی اولیه عامل

نمونه کد زیر نحوه نمونه‌سازی عامل و اجراکننده درون‌دستگاهی را نشان می‌دهد:

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)، دستیارهای درون‌دستگاهی دارای گواهی پلاتفرم که اجازه سیستم EXECUTE_APP_FUNCTIONS را دارند می‌توانند AppFunctions را فراخوانی کنند. اگر ابزار شما صرفاً برای عامل داخلی برنامه‌تان درنظر گرفته شده است (برای مثال، مدیریت سبدهای خرید کاربر یا رکوردهای پایگاه داده خصوصی)، دسترسی فراخواننده را بااستفاده از ثابت 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) که خطر کژپنداری و تخریب طرح‌واره را در مدل‌های تلفن همراه افزایش می‌دهد خودداری کنید.

اجرای چندنوبتی و رویدادهای جاری‌سازی

بااستفاده از روش runner.getOrCreateSession(sessionId) در کلاس Android ViewModel، نمونه AgentSession را دریافت کنید. جلسه یک جاری‌سازی 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)
    }

اجرای تک‌نوبتی

برای کارهای یک‌باره که سابقه مکالمه نباید در RAM حفظ شود (برای مثال، برچسب‌گذاری عکس یا خلاصه‌سازی نوشتار زودگذر)، مستقیماً متد 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 در کلاس Android ViewModel به‌طور خودکار سابقه مکالمه را در چرخش‌های صفحه حفظ می‌کند، بدون اینکه نیاز به لوله‌کشی رشته‌ها در هر چرخش باشد.
  • اخراج جلسه: متد session.delete() را فراخوانی کنید تا وقتی رشته مکالمه‌ای رد می‌شود، جلسه را به‌طور هم‌زمان اخراج کنید، یا متد غیرتعلیقی session.close() را در متد ViewModel.onCleared() فراخوانی کنید تا RAM آزاد شود.
  • اخراج انبوه: برای آزاد کردن یک جلسه خاص، روش runner.deleteSession(sessionId) را فراخوانی کنید، یا برای اخراج کردن همه مکالمه‌های ذخیره‌شده در حافظه نهان، روش runner.clearAllSessions() را فراخوانی کنید.
  • چرخه حیات Runner: کلاس AgentRunner میانای AutoCloseable را پیاده‌سازی می‌کند. هنگام هم‌رسانی اجراکننده در صفحه‌ها، دسته‌های AgentSession جداگانه را در روش ViewModel.onCleared() هر صفحه ببندید، و هنگام خاموش کردن کامل اجراکننده، روش runner.close() را فراخوانی کنید.
  • جلسه‌های هم‌زمان: یک نمونه AgentRunner می‌تواند به‌طور هم‌زمان میزبان چندین جلسه متمایز باشد. هر مقدار sessionId سابقه مکالمه مجزایی را حفظ می‌کند.

خطا در انجام درخواست با کلاس GenAiException

خطاهای زمان اجرا درطول اجرای عامل، مثلاً وقتی عامل به حداکثر نوبت‌های ابزار بدون تولید پاسخ می‌رسد یا وقتی هوش مصنوعی درون‌دستگاهی موقتاً دردسترس نیست، به‌عنوان استثنای GenAiException (com.google.mlkit.genai.common.GenAiException) صادر یا ایجاد می‌شوند. خطاها را به‌صورت واکنشی بااستفاده از اپراتور Kotlin Coroutines Flow 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
    }

کدهای خطای کلیدی

  • ‫GenAiException.ErrorCode.MAX_TURNS_EXCEEDED (-112): عامل از حداکثر حد نوبت (حد maxTurns) فراتر رفت بدون اینکه پاسخ نهایی تولید کند.
  • GenAiException.ErrorCode.NOT_AVAILABLE (8): مدل Gemini Nano یا سرویس سیستم AICore دردسترس یا آماده نیست.
  • ‫GenAiException.ErrorCode.BUSY (9): سیستم مشغول است یا برنامه برقراری تماس از سهمیه‌اش فراتر رفته است.