בעזרת תהליכי עבודה מבוססי-סוכנים (com.google.mlkit:genai-agent, זמינים דרך תוכנית גרסת טרום-ההשקה (Preview) למפתחים של AICore) במכשיר של ערכת למידת המכונה, אתם יכולים ליצור סוכנים אוטונומיים לשיחות בכמה שלבים, שמבוססים על Gemini Nano ועל שירות המערכת של Android AICore.
היא מבוססת על המחלקה GenerativeModel, שמספקת גישה ישירה למודל, והמחלקה AgentRunner הופכת לולאות סוכנים שלמות לאוטומטיות: תכנון פעולות בכמה שלבים, הפעלת כלים מסוגים שונים (כמו שליחת שאילתות למאגר אפליקציות, קריאה ל-API לקצה העורפי או קריאת מסד נתונים מקומי), הערכת תוצאות הכלים וניהול ההקשר של השיחה באופן מקומי במכשיר, בלי זמן אחזור ברשת ותוך שמירה מלאה על פרטיות המשתמשים. במחלקה GenerativeModel יש תמיכה בהיסטוריה של שיחות בכמה שלבים בניהול הלקוח באמצעות אובייקט List<Content> (עם התפקידים USER ו-MODEL) וערך sessionId, ובמחלקה AgentRunner יש סשנים מנוהלים של שיחות בכמה שלבים (באמצעות הממשק AgentSession) שמתעדים באופן אוטומטי את היסטוריית השיחות בזיכרון בין השלבים, כך שלא צריך לנהל את התמלילים באופן ידני. גם הצהרות סטטיות על כלים והוראות מערכת נשמרות במטמון באופן אוטומטי במכשיר, וכך זמן האחזור עד לקבלת הטוקן הראשון מתקצר בלי צורך בהגדרות נוספות.
רכיבי ליבה
- המחלקה
Agent: מגדירה הוראות, כלים זמינים ומגבלות ביצוע באמצעות ה-methodAgent.builder(). ברירת המחדל היאmaxTurns = 10באופן אוטומטי. - הכיתה
AgentRunner: מבוססת על הכיתהGenerativeModel, ומנהלת לולאות חשיבה רציונלית אוטונומיות וסשנים מנוהלים עם מודלים במכשיר. בטוח לשימוש ב-Main כברירת מחדל ב-Dispatchers.IO, עםtemperatureכברירת מחדל ל-null(שמפנה לברירת המחדל של המודל; צריך להעביר אתtemperature = 0.0fלפענוח חמדני). - הממשק
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.
מיומנויות ב-Android
הצגה ב-GitHubשימוש ב-ML Kit Agent API
android skills add mlkit-agent-apiדרישות מוקדמות ויחסי תלות
לפני שמשלבים את Agent API, צריך לוודא שסביבת הפיתוח והגדרות Gradle עומדות בדרישות הבאות.
דרישות הפלטפורמה והחומרה
- גרסת Android: Android 14 (רמת API 34) ומעלה, שרשומה בתוכנית AICore Developer Preview.
- מכשירים נתמכים: רשימה של מכשירים נתמכים וגרסאות המודל של Gemini Nano מופיעה במאמר תמיכה במכשירים ליצירת סוכנים בהתאמה אישית.
- גרסת SDK מינימלית:
minSdk 26ואילך. - Kotlin: גרסה 2.3 ומעלה.
הגדרת 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.
מדריך למתחילים: הפעלת סוכן
דוגמת הקוד הבאה מראה איך ליצור מופע של סוכן ו-runner במכשיר:
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)באופן ידני בתהליכי עבודה רגילים של שיחה או קריאת API לכלי. - פענוח חמדני דטרמיניסטי: המחלקה
AgentRunnerמגדירה את הפרמטרtemperatureשמוגדר כברירת מחדל לערךnull, ודוחה את ברירת המחדל של המודל הבסיסי. כדי לבחור כלים דטרמיניסטיים ולעמוד בדרישות של סכימת JSON במכשיר, מעבירים את הערךtemperature = 0.0f(temperature) ל-constructor של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 (לדוגמה, תיוג תמונות או סיכום טקסט זמני), קוראים ישירות ל-method 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 שומרת על היסטוריית השיחות באופן אוטומטי כשמסובבים את המסך, בלי צורך להעביר מחרוזות בכל פעם. - פינוי סשן: קוראים ל-method
session.delete()כדי לפנות סשן באופן סינכרוני כששרשור שיחה נסגר, או קוראים ל-methodsession.close()שלא מושהה ב-methodViewModel.onCleared()כדי לפנות זיכרון RAM. - פינוי בכמות גדולה: מפעילים את השיטה
runner.deleteSession(sessionId)כדי לפנות סשן ספציפי, או את השיטהrunner.clearAllSessions()כדי לפנות את כל השיחות שנשמרו במטמון. - מחזור החיים של Runner: המחלקה
AgentRunnerמטמיעה את הממשקAutoCloseable. כשמשתפים את ה-runner בין מסכים, צריך לסגור את ה-handles הנפרדים שלAgentSessionב-methodViewModel.onCleared()של כל מסך, ולקרוא ל-methodrunner.close()כשמכבים את ה-runner לגמרי. - סשנים בו-זמנית: מכונה אחת של
AgentRunnerיכולה לארח כמה סשנים נפרדים בו-זמנית. לכל ערך שלsessionIdיש היסטוריית שיחות נפרדת.
טיפול בשגיאות באמצעות המחלקה GenAiException
כשלים בזמן הריצה במהלך הפעלת הנציג, למשל כשהנציג מגיע למספר המקסימלי של פניות לכלי בלי ליצור תשובה או כשה-AI במכשיר לא זמין באופן זמני, נפלטים או נזרקים כחריגה מסוג 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): המערכת עסוקה או שהאפליקציה ששלחה את הקריאה חרגה מהמכסה שלה.