إنشاء مهام سير عمل مستندة إلى الذكاء الاصطناعي الوكيل باستخدام Gemini Nano

من خلال مهام سير العمل المستندة إلى الذكاء الاصطناعي الوكيل على الجهاز فقط في حزمة تعلّم الآلة (com.google.mlkit:genai-agent، المتاحة من خلال برنامج معاينة المطوّر في AICore)، يمكنك إنشاء وكلاء محادثات مستقلين ومتعدّدي الجولات مستندين إلى Gemini Nano وخدمة نظام Android AICore.

تستند فئة AgentRunner إلى فئة GenerativeModel التي توفّر إمكانية الوصول المباشر إلى النموذج، وتعمل على أتمتة الحلقات الوكيلة الكاملة: تخطيط الإجراءات المتعدّدة الخطوات، واستدعاء الأدوات المكتوبة (مثل الاستعلام عن مستودع التطبيقات أو استدعاء واجهة برمجة تطبيقات خلفية أو قراءة قاعدة بيانات محلية)، وتقييم نتائج الأدوات، وإدارة سياق المحادثة محليًا على الجهاز بدون أي وقت استجابة للشبكة وبخصوصية كاملة للمستخدم. While the GenerativeModel class supports client-managed multi-turn history using a List<Content> object (with USER and MODEL roles) and a sessionId value, the AgentRunner class provides managed multi-turn sessions (using the AgentSession interface) that record in-memory conversation history automatically across turns so that you don't have to manage transcripts manually. يتم أيضًا تخزين تعريفات الأدوات الثابتة وتعليمات النظام تلقائيًا في الذاكرة المؤقتة على الجهاز فقط، ما يقلل من وقت الاستجابة لأول رمز مميز بدون الحاجة إلى ضبط إعدادات إضافية.

المكوّنات الأساسية

  • الفئة 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: هي مسار Flow تفاعلي في Kotlin يُصدر رموزًا مميزة للنصوص وتعديلات على الأدوات.
  • الفئة AgentTool: دوال مكتوبة يمكن للنموذج استدعاؤها، ويتم تحديدها باستخدام AppFunctions في Android‏ (@AppFunctionDeclaration).
  • الفئة GenAiException: تسلسل هرمي للاستثناءات المكتوبة (com.google.mlkit.genai.common.GenAiException) يشير إلى حالات الفشل مثل MAX_TURNS_EXCEEDED أو NOT_AVAILABLE أو BUSY.

المتطلبات الأساسية والتبعيات

تأكَّد من أنّ بيئة التطوير وإعدادات Gradle تستوفي المتطلبات التالية قبل دمج Agent API.

متطلبات النظام الأساسي والأجهزة

إعدادات 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 (درجة الحرارة) إلى دالة الإنشاء 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)
    }

التنفيذ من خلال طلب واحد

بالنسبة إلى المهام التي يتم تنفيذها لمرة واحدة ولا يجب الاحتفاظ بسجلّ الحوار في ذاكرة 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 داخل فئة ViewModel في Android إلى الحفاظ على سجلّ المحادثات تلقائيًا عند تدوير الشاشة بدون الحاجة إلى ربط السلاسل في كل مرة.
  • إخلاء الجلسة: يمكنك استدعاء الطريقة session.delete() لإخلاء جلسة بشكل متزامن عند إغلاق سلسلة محادثات، أو استدعاء الطريقة session.close() غير المعلّقة في الطريقة ViewModel.onCleared() لإخلاء ذاكرة RAM.
  • الإخلاء المجمّع: يمكنك استدعاء الطريقة runner.deleteSession(sessionId) لإخلاء جلسة معيّنة، أو الطريقة runner.clearAllSessions() لإخلاء جميع المحادثات المخزّنة مؤقتًا.
  • مراحل نشاط المشغِّل: تنفّذ الفئة 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): النظام مشغول أو تجاوز تطبيق الاتصال الحصة المخصّصة له.