กำหนดเครื่องมือ

เครื่องมือช่วยให้ Agent ในอุปกรณ์สามารถเข้าถึงข้อมูลแอป สื่อสารกับบริการแบ็กเอนด์ และดำเนินการในนามของผู้ใช้ได้

วิธีการทำงานของเครื่องมือ

เมื่อผู้ใช้ส่งพรอมต์ คลาส AgentRunner จะเรียกใช้ลูป Agent ใน 4 ขั้นตอนดังนี้

  1. สังเกต: Gemini Nano จะวิเคราะห์พรอมต์ของผู้ใช้และบริบทการสนทนาปัจจุบันเทียบกับการประกาศเครื่องมือที่ลงทะเบียนไว้ (พื้นที่การดำเนินการของ Agent)
  2. เหตุผล: โมเดลจะพิจารณาว่าจำเป็นต้องใช้เครื่องมือในแอปหรือไม่ หากจำเป็นต้องใช้เครื่องมือ Gemini Nano จะส่งการเรียกใช้เครื่องมือที่มีโครงสร้างซึ่งประกอบด้วยชื่อฟังก์ชันและอาร์กิวเมนต์ที่พิมพ์
  3. ดำเนินการ: อินสแตนซ์ AgentRunner จะสกัดกั้นการเรียกใช้นี้โดยอัตโนมัติและเรียกใช้ฟังก์ชัน Kotlin ที่เกี่ยวข้องในแอปของคุณ
  4. ประเมินความคิดเห็น: ระบบจะป้อนเอาต์พุตของเครื่องมือกลับไปยัง Gemini Nano โมเดลจะประเมินความคิดเห็นและตอบผู้ใช้หรือเรียกใช้เครื่องมืออื่นจนกว่าจะเป็นไปตามเงื่อนไขการสิ้นสุด (บรรลุเป้าหมายหรือถึงขีดจำกัด maxTurns)

เครื่องมือที่ประกาศพร้อม AppFunctions ของ Android

วิธีที่แนะนำในการกำหนดเครื่องมือคือการประกาศโดยใช้ Android AppFunctions (androidx.appfunctions) AppFunctions มีโมเดลแพลตฟอร์มที่เป็นมาตรฐานสำหรับการประกาศความสามารถในแอปที่พิมพ์ด้วยการสร้างสคีมาอัตโนมัติ

1. เพิ่มทรัพยากร Dependency ของ Gradle

เพิ่มอาร์ติแฟกต์ AppFunctions ควบคู่ไปกับ genai-agent ในไฟล์ build.gradle.kts ของโมดูล

dependencies {
    implementation("com.google.mlkit:genai-agent:1.0.0-alpha1")
    implementation("androidx.appfunctions:appfunctions:1.0.0-alpha13")
    ksp("androidx.appfunctions:appfunctions-compiler:1.0.0-alpha13")
}

2. ใส่คำอธิบายประกอบเมธอดของเครื่องมือด้วยคำอธิบายประกอบ @AppFunctionDeclaration

ใส่คำอธิบายประกอบเมธอดเครื่องมือสาธารณะด้วยคำอธิบายประกอบ @AppFunctionDeclaration(isDescribedByKDoc = true) คำอธิบายพารามิเตอร์จะบันทึกไว้ในแท็ก @param ของ KDoc มาตรฐาน

AppFunctions กำหนดให้ค่าที่แสดงผลเป็นประเภทพื้นฐาน (เช่น String, Int หรือ Boolean) หรือคลาส Structured Data ที่มีคำอธิบายประกอบ @AppFunctionSerializable

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

/**
 * Structured details for an order.
 */
@AppFunctionSerializable
data class OrderDetails(
    val orderId: String,
    val status: String,
    val estimatedDelivery: String?,
    val itemCount: Int,
)

/**
 * Result returned when cancelling an order.
 */
@AppFunctionSerializable
data class CancelOrderResult(
    val orderId: String,
    val cancelled: Boolean,
    val message: String,
)

@AppFunctionServiceEntryPoint(
    serviceName = "OrderAppFunctionService",
    appFunctionXmlFileName = "order_app_functions",
)
abstract class OrderTools : AppFunctionService() {

    private val orderRepository = OrderRepository()

    /**
     * Fetches details and delivery status for a customer order by ID.
     *
     * @param orderId The alphanumeric order identifier (for example, ORD-8492)
     */
    @AppFunctionDeclaration(isDescribedByKDoc = true)
    suspend fun fetch_order_details(
        orderId: String
    ): OrderDetails = withContext(Dispatchers.IO) {
        val order = orderRepository.findOrder(orderId)
        if (order != null) {
            OrderDetails(
                orderId = order.id,
                status = order.status,
                estimatedDelivery = order.deliveryEstimate,
                itemCount = order.items.size,
            )
        } else {
            OrderDetails(
                orderId = orderId,
                status = "NOT_FOUND",
                estimatedDelivery = null,
                itemCount = 0,
            )
        }
    }

    /**
     * Requests a cancellation or return for an active order.
     *
     * @param orderId The order identifier to cancel
     * @param reason The reason provided by the user for cancellation
     */
    @AppFunctionDeclaration(isDescribedByKDoc = true)
    suspend fun cancel_order(
        orderId: String,
        reason: String? = null
    ): CancelOrderResult = withContext(Dispatchers.IO) {
        val success = orderRepository.cancelOrder(orderId, reason)
        CancelOrderResult(
            orderId = orderId,
            cancelled = success,
            message = if (success) {
                "Order cancellation processed."
            } else {
                "Order cannot be cancelled at this stage."
            }
        )
    }
}

ลงทะเบียนบริการที่สร้างขึ้นในไฟล์ 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>

3. จำกัดสิทธิ์เข้าถึงของผู้โทรด้วยค่าคงที่ ACCESS_LEVEL_SELF

AppFunctions ไม่จำเป็นต้องเป็นแบบสาธารณะสำหรับระบบหรือผู้ช่วยภายนอก ใน Android การควบคุมการเข้าถึงจะได้รับการจัดการผ่านระดับการเข้าถึงของผู้โทร แทนที่จะเป็นการตรวจสอบข้อมูลประจำตัวของผู้โทรด้วยตนเอง

โดยค่าเริ่มต้น (ใช้ค่าคงที่ ACCESS_LEVEL_ANDROID_TRUSTED) ผู้ช่วยในอุปกรณ์ที่ได้รับการรับรองจากแพลตฟอร์มซึ่งมีสิทธิ์ของระบบ EXECUTE_APP_FUNCTIONS จะค้นพบและเรียกใช้ AppFunction ได้ อย่างไรก็ตาม เมื่อคุณสร้างความสามารถที่ออกแบบมาเพื่อใช้กับ 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() {

    private val orderRepository = OrderRepository()

    /**
     * Cancels an order. Restricted exclusively to this app's internal agent.
     */
    @OptIn(ExperimentalAppFunctionsApi::class)
    @AppFunctionDeclaration(isDescribedByKDoc = true)
    @AppFunctionAccessLevel(
        level = AppFunctionMetadata.ACCESS_LEVEL_SELF,
        isCompatEnforcementEnabled = true,
    )
    suspend fun cancel_order(orderId: String): CancelOrderResult {
        // Only your app's internal agent can invoke this function
        val success = orderRepository.cancelOrder(orderId)
        return CancelOrderResult(
            orderId = orderId,
            cancelled = success,
            message = if (success) "Cancelled." else "Failed."
        )
    }
}

แพลตฟอร์มรองรับระดับการเข้าถึงของผู้โทร 3 ระดับ ดังนี้

  • ACCESS_LEVEL_SELF (100): จำกัดเฉพาะแอปพลิเคชันโฮสต์เท่านั้น ผู้ช่วยภายนอก แพลตฟอร์มของระบบ และแอปอื่นๆ จะไม่สามารถค้นพบหรือเรียกใช้ฟังก์ชันผ่าน IPC ได้ ใช้ระดับนี้สำหรับการดำเนินการหรือเครื่องมือที่มีความละเอียดอ่อนซึ่งควรให้บริการเฉพาะตัวแทนภายในของแอปเท่านั้น
  • ACCESS_LEVEL_SYSTEM (200): จำกัดไว้สำหรับผู้โทรเข้าของระบบที่มีสิทธิ์ซึ่งมีสิทธิ์ android.Manifest.permission#EXECUTE_APP_FUNCTIONS_SYSTEM (เช่น Launcher ของระบบหรือแอปที่ใช้ล่าสุด) และแอปโฮสต์เอง
  • ACCESS_LEVEL_ANDROID_TRUSTED (300, ค่าเริ่มต้น): เปิดให้ตัวแทนในอุปกรณ์ที่ได้รับการรับรองจากแพลตฟอร์มซึ่งมีสิทธิ์ EXECUTE_APP_FUNCTIONS, แพลตฟอร์มของระบบ และแอปโฮสต์เข้าถึงได้

4. ลงทะเบียนเครื่องมือกับ Agent

คุณสามารถลงทะเบียน AppFunction กับ Agent โดยอัตโนมัติโดยใช้เมธอด enableAppFunctions() หรือใช้เมธอด AppFunctionAgentTools.discover() โดยใช้โปรแกรมก็ได้

การค้นหาอัตโนมัติด้วยเมธอด enableAppFunctions()

สำหรับแอปที่ตรงไปตรงมาซึ่ง AppFunctions จะทำงานโดยอัตโนมัติเมื่อโมเดลเรียกใช้ ให้ทำดังนี้

import com.google.mlkit.genai.agent.Agent
import com.google.mlkit.genai.agent.AgentRunner

val agent = Agent.builder()
    .setName("OrderAssistant")
    .setInstructions("Assist users with order tracking.")
    .enableAppFunctions(true) // Automatically discovers all AppFunctions in your app package
    .setMaxTurns(10)
    .build()

val runner = AgentRunner(agent)

หรือจะเรียกใช้เมธอด addAppFunctionIds("fetch_order_details", "cancel_order") เพื่อจำกัดขอบเขตการค้นหาให้เฉพาะเจาะจงกับรหัส AppFunction บางรายการแทนที่จะแสดงฟังก์ชันในแอปทั้งหมดก็ได้

การค้นพบแบบเป็นโปรแกรมด้วยเมธอด AppFunctionAgentTools.discover()

เมื่อแอปของคุณต้องการตรวจสอบ AppFunction ที่ค้นพบ ให้กรองหรือตกแต่งก่อนลงทะเบียนกับ Agent ดังนี้

import com.google.mlkit.genai.agent.Agent
import com.google.mlkit.genai.agent.AgentRunner
import com.google.mlkit.genai.agent.AppFunctionAgentTools

val discoveredTools = AppFunctionAgentTools.discover(
    enabledAppFunctionIds = listOf("fetch_order_details", "cancel_order") // Optional ID filter
)

val agent = Agent.builder()
    .setName("OrderAssistant")
    .setInstructions("Assist users with order tracking.")
    .addTools(discoveredTools)
    .build()

val runner = AgentRunner(agent)

รูปแบบ Decorator และโมเดลแบบมีมนุษย์ร่วม

การดำเนินการที่มีความละเอียดอ่อน เช่น การยกเลิกคำสั่งซื้อ การชำระเงิน หรือการลบข้อมูลผู้ใช้ ควรมีมนุษย์ร่วม (HITL) โดยกำหนดให้ผู้ใช้ต้องยืนยัน เครื่องมือทั้งหมดที่เพิ่มลงในคลาส Agent มาจาก Android AppFunctions และคลาส AgentTool มีคอนสตรัคเตอร์ที่มอบสิทธิ์ AgentTool(delegate) เพื่อให้คุณสามารถรวมเครื่องมือ AppFunction ที่ค้นพบโดยใช้รูปแบบ Decorator ได้ ดังนี้

import com.google.mlkit.genai.agent.AgentTool

class HitlConfirmationTool(
    delegate: AgentTool,
    private val onConfirm: suspend (toolName: String, args: Map<String, Any>) -> Boolean
) : AgentTool(delegate) {

    override suspend fun execute(args: Map<String, Any>): Map<String, Any?> {
        val confirmed = onConfirm(name, args)
        if (!confirmed) {
            return mapOf(
                "status" to "CANCELLED_BY_USER",
                "message" to "Action was declined by the user."
            )
        }
        return checkNotNull(delegate).execute(args)
    }
}

แนวทางปฏิบัติแนะนำสำหรับการออกแบบเครื่องมือ

  • กำหนดขอบเขตพื้นที่การดำเนินการ: กำหนดพื้นที่การดำเนินการของ Agent (ชุดเครื่องมือและสิทธิ์ที่แสดงต่อ Agent) ให้มีขนาดใหญ่พอสำหรับงานโดยใช้เมธอด addAppFunctionIds() และค่าคงที่ ACCESS_LEVEL_SELF พื้นที่การดำเนินการที่ใหญ่เกินไปทำให้โมเดลในอุปกรณ์มีแนวโน้มที่จะเกิดข้อผิดพลาดมากขึ้นและใช้โทเค็นหน้าต่างบริบท
  • ระบุคำอธิบายอย่างชัดเจน: Gemini Nano อาศัยคำอธิบายเครื่องมือและพารามิเตอร์เพื่อตัดสินใจว่าจะเรียกใช้เครื่องมือเมื่อใด ระบุวัตถุประสงค์ของเครื่องมือให้ชัดเจนและพูดถึงรูปแบบอาร์กิวเมนต์ที่คาดไว้ (เช่น วันที่หรือรหัส)
  • แสดงแผนที่ข้อผิดพลาดที่มีโครงสร้าง: แสดงแผนที่ข้อผิดพลาดที่สื่อความหมาย เช่น mapOf("error" to "Order not found") เมื่อเกิดความล้มเหลวที่คาดไว้ ซึ่งจะช่วยให้ Gemini Nano อธิบายปัญหาให้ผู้ใช้เข้าใจได้อย่างราบรื่น หากเครื่องมือแสดงข้อยกเว้นที่ไม่คาดคิด รันไทม์จะตรวจจับข้อยกเว้นนั้นโดยอัตโนมัติและแสดงข้อความแสดงข้อผิดพลาดกลับไปยังโมเดลเพื่อแก้ไขด้วยตนเอง
  • เน้นเพย์โหลดที่แสดงผล: โมเดลในอุปกรณ์มีหน้าต่างบริบทที่เล็กกว่าโมเดลเซิร์ฟเวอร์ แสดงเฉพาะช่องที่เกี่ยวข้องกับการตอบคำถามของผู้ใช้แทนการแสดงแถวฐานข้อมูลทั้งหมดหรือการตอบกลับเครือข่ายดิบ
  • เรียกใช้ I/O ใน Dispatchers.IO Dispatcher: ตรวจสอบเสมอว่าคำขอเครือข่ายและการค้นหาฐานข้อมูล Room ภายในเครื่องมือทำงานใน Dispatchers.IO Dispatcher เพื่อป้องกันไม่ให้บล็อกผู้เรียก Coroutine

ขั้นตอนถัดไป