เวิร์กโฟลว์แบบ Agentic ในอุปกรณ์ของ ML Kit (com.google.mlkit:genai-agent, พร้อมใช้งานผ่านโปรแกรม AICore Developer Preview) ช่วยให้คุณสร้าง Agent การสนทนาแบบหลายรอบที่ทำงานอัตโนมัติซึ่งขับเคลื่อนโดย Gemini Nano และบริการระบบ Android AICore ได้
คลาส AgentRunner สร้างขึ้นจากคลาส GenerativeModel ซึ่งให้การเข้าถึงโมเดลโดยตรง โดยจะทำให้ลูป Agent ทำงานโดยอัตโนมัติอย่างสมบูรณ์ ได้แก่ การวางแผนการดำเนินการแบบหลายขั้นตอน การเรียกใช้เครื่องมือที่พิมพ์ (เช่น การค้นหาที่เก็บแอป การเรียกใช้ API แบ็กเอนด์ หรือการอ่านฐานข้อมูลในเครื่อง) การประเมินผลลัพธ์ของเครื่องมือ และการจัดการบริบทการสนทนาในเครื่องบนอุปกรณ์โดยไม่มีเวลาในการตอบสนองของเครือข่ายและความเป็นส่วนตัวของผู้ใช้ที่สมบูรณ์ แม้ว่าคลาส GenerativeModel จะรองรับประวัติการสนทนาแบบหลายเทิร์นที่จัดการโดยไคลเอ็นต์โดยใช้ออบเจ็กต์ List<Content> (ที่มีบทบาท USER และ MODEL) และค่า sessionId แต่คลาส AgentRunner จะมีเซสชันแบบหลายเทิร์นที่มีการจัดการ (โดยใช้อินเทอร์เฟซ AgentSession) ซึ่งจะบันทึกประวัติการสนทนาในหน่วยความจำโดยอัตโนมัติในเทิร์นต่างๆ เพื่อให้คุณไม่ต้องจัดการข้อความถอดเสียงด้วยตนเอง การประกาศเครื่องมือแบบคงที่และคำสั่งของระบบจะได้รับการแคชโดยอัตโนมัติในอุปกรณ์ด้วย ซึ่งจะช่วยลดเวลาในการตอบสนองของโทเค็นแรกโดยไม่ต้องกำหนดค่าเพิ่มเติม
องค์ประกอบหลัก
- คลาส
Agent: ประกาศวิธีการ เครื่องมือที่ใช้ได้ และแนวทางปฏิบัติในการดำเนินการโดยใช้เมธอดAgent.builder()ค่าเริ่มต้นคือmaxTurns = 10โดยอัตโนมัติ - คลาส
AgentRunner: สร้างขึ้นบนคลาสGenerativeModelโดยจัดระเบียบลูปการให้เหตุผลแบบอัตโนมัติและเซสชันที่มีการจัดการด้วยโมเดลในอุปกรณ์ ปลอดภัยในเธรดหลักโดยค่าเริ่มต้นในDispatchers.IOโดยtemperatureมีค่าเริ่มต้นเป็นnull(ซึ่งจะเลื่อนไปใช้ค่าเริ่มต้นของโมเดล โดยส่งtemperature = 0.0fสำหรับการถอดรหัสแบบ Greedy) - อินเทอร์เฟซ
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
ทักษะ Android
ดูใน GitHubใช้ ML Kit Agent API
android skills add mlkit-agent-apiข้อกำหนดเบื้องต้นและทรัพยากร Dependency
ตรวจสอบว่าสภาพแวดล้อมในการพัฒนาซอฟต์แวร์และการกำหนดค่า Gradle เป็นไปตามข้อกำหนดต่อไปนี้ก่อนผสานรวม Agent API
ข้อกำหนดด้านแพลตฟอร์มและฮาร์ดแวร์
- เวอร์ชัน Android: Android 14 (API ระดับ 34) ขึ้นไปที่ลงทะเบียนในโปรแกรมเวอร์ชันตัวอย่างสำหรับนักพัฒนาซอฟต์แวร์ของ AICore
- อุปกรณ์ที่รองรับ: ดูรายการอุปกรณ์ที่รองรับและเวอร์ชันโมเดล Gemini Nano ได้ที่การสร้างที่กำหนดเองและการรองรับอุปกรณ์ Agent
- SDK ขั้นต่ำ:
minSdk 26ขึ้นไป - Kotlin: Kotlin 2.3 ขึ้นไป
การกำหนดค่า Gradle
เพิ่มทรัพยากร Dependency 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
คู่มือเริ่มใช้งานฉบับย่อ: เริ่มต้น Agent
ตัวอย่างโค้ดต่อไปนี้แสดงวิธีสร้างอินสแตนซ์ Agent และ 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) ผู้ช่วยในอุปกรณ์ที่ได้รับการรับรองจากแพลตฟอร์มซึ่งมีสิทธิ์ของระบบ EXECUTE_APP_FUNCTIONS จะเรียกใช้ AppFunctions ได้ หากเครื่องมือของคุณมีไว้สำหรับ Agent ภายในของแอปโดยเฉพาะ (เช่น การจัดการรถเข็นของผู้ใช้หรือระเบียนฐานข้อมูลส่วนตัว) ให้จำกัดการเข้าถึงของผู้โทรโดยใช้ค่าคงที่ 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
กำหนดค่าอินสแตนซ์ 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
- ขีดจำกัดเทิร์น: คลาส
Agent.Builderจะใช้ค่าเริ่มต้นเป็นmaxTurns = 10โดยอัตโนมัติเป็นเงื่อนไขการสิ้นสุดเพื่อป้องกันไม่ให้เกิดลูปการเรียกใช้เครื่องมือแบบไม่สิ้นสุด คุณไม่จำเป็นต้องเรียกใช้เมธอดsetMaxTurns(10)ด้วยตนเองสำหรับเวิร์กโฟลว์การสนทนาหรือการเรียกใช้เครื่องมือมาตรฐาน - การถอดรหัสแบบ Greedy เชิงกำหนด: คลาส
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()
}
}
สตรีมโทเค็นข้อความโดยตรง
สำหรับอินเทอร์เฟซแบบสนทนาที่ไม่จำเป็นต้องแสดงรายละเอียดการดำเนินการของเครื่องมือแยกต่างหากใน UI ให้ใช้ฟังก์ชันส่วนขยาย 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()เพื่อลบการสนทนาที่แคชไว้ทั้งหมด - วงจรของ Runner: คลาส
AgentRunnerจะใช้อินเทอร์เฟซAutoCloseableเมื่อแชร์ Runner ในหน้าจอต่างๆ ให้ปิดแฮนเดิลAgentSessionแต่ละรายการในเมธอดViewModel.onCleared()ของแต่ละหน้าจอ และเรียกใช้เมธอดrunner.close()เมื่อปิด Runner โดยสมบูรณ์ - เซสชันที่เกิดขึ้นพร้อมกัน: อินสแตนซ์
AgentRunnerรายการเดียวสามารถโฮสต์เซสชันที่แตกต่างกันหลายรายการพร้อมกันได้ ค่าsessionIdแต่ละค่าจะเก็บประวัติการสนทนาแยกกัน
การจัดการข้อผิดพลาดด้วยคลาส GenAiException
ความล้มเหลวของรันไทม์ระหว่างการดำเนินการของ Agent เช่น เมื่อ Agent ถึงจำนวนเทิร์นสูงสุดของเครื่องมือโดยไม่สร้างคำตอบ หรือเมื่อ AI ในอุปกรณ์ไม่พร้อมใช้งานชั่วคราว จะแสดงหรือส่งเป็นข้อยกเว้น 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): Agent มีจำนวนเทิร์นเกินขีดจำกัดสูงสุด (ขีดจำกัดmaxTurns) โดยไม่ได้สร้างคำตอบสุดท้ายGenAiException.ErrorCode.NOT_AVAILABLE(8): โมเดล Gemini Nano หรือบริการระบบ AICore ไม่พร้อมใช้งานหรือไม่พร้อมทำงานGenAiException.ErrorCode.BUSY(9): ระบบไม่ว่างหรือแอปการโทรใช้โควต้าเกินขีดจำกัด