สร้างเวิร์กโฟลว์แบบ Agentic ด้วย Gemini Nano

เวิร์กโฟลว์แบบ 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

ข้อกำหนดเบื้องต้นและทรัพยากร Dependency

ตรวจสอบว่าสภาพแวดล้อมในการพัฒนาซอฟต์แวร์และการกำหนดค่า Gradle เป็นไปตามข้อกำหนดต่อไปนี้ก่อนผสานรวม Agent API

ข้อกำหนดด้านแพลตฟอร์มและฮาร์ดแวร์

การกำหนดค่า 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): ระบบไม่ว่างหรือแอปการโทรใช้โควต้าเกินขีดจำกัด