แอปอัจฉริยะ

ผสานรวมแอปของคุณกับระบบอัจฉริยะของ Android

ลำโพง

เกี่ยวกับ

เกี่ยวกับการเสวนา

Android กำลังพัฒนาเป็นระบบอัจฉริยะที่ Agent อย่าง Gemini จะประสานงานต่างๆ ในแอป หากต้องการปลดล็อกประสบการณ์ของผู้ใช้ใหม่และเข้าร่วมเวิร์กโฟลว์ที่เป็น Agent เหล่านี้ แอปของคุณต้องแสดงความสามารถหลักของแอป

เซสชันนี้เป็นคู่มือที่นำไปใช้ได้จริงสำหรับการผสานรวมแอปกับเลเยอร์อัจฉริยะของ Android เราจะสำรวจเส้นทางการผสานรวมที่สำคัญ ซึ่งรวมถึง MCP ของ Android ในอุปกรณ์, MCP ฝั่งเซิร์ฟเวอร์ และการทำงานอัตโนมัติของงาน พร้อมทั้งให้กรอบการตัดสินใจในการเลือกเส้นทางที่เหมาะสมสำหรับกรณีการใช้งานของคุณ นอกจากนี้ เราจะสำรวจประสบการณ์การใช้งานเชิงรุกด้วยบริบทส่วนบุคคลด้วย

คุณจะได้เรียนรู้วิธีต่อไปนี้

  • เลือกเส้นทางการผสานรวมที่เหมาะสมที่สุดสำหรับความต้องการของแอป
  • ใช้ฟีเจอร์อัจฉริยะทั้งแบบเชิงรับและเชิงรุก
  • สร้างสมดุลระหว่างประสิทธิภาพและความปลอดภัยในการผสานรวม Agent

สรุป

  • Android MCP เป็นเฟรมเวิร์กการเรียกใช้เครื่องมือในอุปกรณ์แบบรวมที่ขับเคลื่อนโดยไลบรารี Jetpack AppFunctions โดยจะแมป MCP Primitives กับ Kotlin Constructs และทำงานบน Android Binder IPC
  • เครื่องมือในอุปกรณ์จะทำงานได้ดีเมื่อเข้าถึงข้อมูลที่เข้ารหัสหรือเซ็นเซอร์ของอุปกรณ์ หรือจะใช้การทำงานอัตโนมัติของงานที่ขับเคลื่อนโดยแพลตฟอร์ม ซึ่งช่วยให้เอเจนต์ที่โหลดไว้ล่วงหน้าจำลองการโต้ตอบแบบสัมผัสได้โดยไม่ต้องเปลี่ยนโค้ด
  • แอปของคุณจะทำหน้าที่เป็นเซิร์ฟเวอร์ MCP ของ Android ขณะที่ Agent ค้นพบและเรียกใช้เครื่องมือ ประกาศเครื่องมือด้วย AppFunctionDeclaration, ระบุคำอธิบาย KDoc และหลีกเลี่ยงการใช้ค่าเริ่มต้นของ Kotlin
  • แพลตฟอร์มแอปที่เชื่อมต่อซึ่งปัจจุบันอยู่ในเวอร์ชันตัวอย่างช่วยให้คุณทดสอบ AppFunction กับ Gemini ได้ ตั้งค่าแอปที่เชื่อมต่อใน Google Connected Console แล้วอัปโหลดแพ็กเกจใน Play Console

แหล่งข้อมูล

Gemini Notebook

หัวข้อ

  • android-mcp
  • appfunctions
  • gemini
  • connected-apps
  • model-context-protocol
  • on-device-ai

ข้อความถอดเสียง

ข้อมูลเบื้องต้นเกี่ยวกับยุคแห่งระบบอัจฉริยะ

  • โอเค

  • สวัสดีทุกคน

  • ผม Dan Galpin จากทีมนักพัฒนาสัมพันธ์

  • และฉัน Sarah Shamsie ผู้จัดการผลิตภัณฑ์ที่ทำงานเกี่ยวกับเฟรมเวิร์กอัจฉริยะสำหรับนักพัฒนาแอป Android

  • เรายินดีอย่างยิ่งที่ได้ต้อนรับคุณเข้าสู่เซสชันในวันนี้

  • เรากำลังเห็นการเปลี่ยนแปลงกระบวนทัศน์ในวิธีที่ผู้ใช้โต้ตอบกับอุปกรณ์ของตน

  • ในยุคเว็บ ผู้ใช้มักจะเริ่มต้นเส้นทางด้วยการไปยังเว็บเบราว์เซอร์ แล้วใช้ฟังก์ชันการค้นหา

  • จากนั้นในยุคอุปกรณ์เคลื่อนที่ เราเห็นพฤติกรรมของผู้ใช้เปลี่ยนไปใช้แอปที่มาพร้อมเครื่องซึ่งมีความเฉพาะเจาะจงมากขึ้นเพื่อดำเนินการแบบครั้งเดียว

  • ดังตัวอย่างต่อไปนี้

  • มีข้อความจากเพื่อนชวนไปกินข้าวเย็น

  • ในการตอบกลับคำเชิญนี้ ฉันต้องตรวจสอบปฏิทินเพื่อดูว่าฉันว่างไหม เพิ่มกิจกรรมลงในปฏิทิน จองรถหากต้องการเดินทางไปที่นั่น แล้วส่งข้อความกลับไปหาเพื่อนว่าฉันไปได้

  • ก่อนหน้านี้ การดำเนินการนี้ต้องใช้สิ่งที่เรียกว่าการสลับแอป ซึ่งโดยพื้นฐานแล้วฉันจะต้องสลับไปมาระหว่างแอปต่างๆ ในอุปกรณ์เพื่อดำเนินการให้เสร็จสมบูรณ์และบรรลุเป้าหมาย

  • แต่ตอนนี้เราอยู่ในยุคแห่งความอัจฉริยะ

  • ในยุคแห่งความอัจฉริยะ ผู้ใช้เริ่มเส้นทางด้วยการแสดงเจตนาและเป้าหมายระดับสูง แทนที่จะเป็นการกระทำที่เฉพาะเจาะจงที่ต้องการให้เสร็จสิ้น

  • มีการเปลี่ยนแปลงขั้นพื้นฐานที่ผู้ใช้มองว่าอุปกรณ์เป็นสิ่งที่คาดการณ์ความต้องการของตนในเชิงรุกและช่วยให้บรรลุเป้าหมายได้จริง แทนที่จะเป็นเพียงสิ่งที่ช่วยให้ดำเนินการต่างๆ ได้

  • และเราสังเกตเห็นว่าผู้ใช้คาดหวังให้เฟรมเวิร์กอัจฉริยะนี้ทำงานได้ไม่เพียงแค่ในอุปกรณ์เคลื่อนที่เท่านั้น แต่ยังรวมถึงในระบบนิเวศของอุปกรณ์รูปแบบต่างๆ เช่น นาฬิกา รถยนต์ ทีวี แล็ปท็อป ตลอดจน XR และแว่นตาด้วย

  • ในยุคแห่งความอัจฉริยะ พรอมต์ข้ามแอปเพียงรายการเดียวสามารถช่วยอำนวยความสะดวกในตัวอย่างคำเชิญรับประทานอาหารค่ำจากก่อนหน้านี้ได้

  • เพียงแค่ใช้พรอมต์เดียว เอเจนต์ในอุปกรณ์ก็ช่วยเปิดใช้การดำเนินการทั้งหมดที่ฉันต้องการได้ ตั้งแต่การตรวจสอบปฏิทิน เพิ่มกิจกรรม จองรถ และส่งข้อความ ทั้งหมดนี้ทำได้ในครั้งเดียว

  • วันนี้เราจะแชร์ข้อมูลเพิ่มเติมเกี่ยวกับแผนของ Android ในด้านนี้ และวิธีที่นักพัฒนาแอปอย่างคุณสามารถสร้างประสบการณ์การใช้งานระดับเฟิร์สคลาสได้

  • Dan

  • ขอบคุณนะ Sarah

  • Android จึงเพิ่มความอัจฉริยะลงในแกนหลักอย่างต่อเนื่อง ซึ่งจะช่วยให้แอปของคุณมีวิธีใหม่ๆ ในการเตรียมพร้อมสำหรับยุคแห่งความอัจฉริยะนี้

  • เสาหลักด้านความอัจฉริยะสำหรับนักพัฒนาแอปเสาแรกคือความอัจฉริยะในแอป ซึ่งก็คือการทำให้แอปฉลาดขึ้นอย่างเห็นได้ชัด โดยการนำความอัจฉริยะมาสู่แอป และคุณสามารถใช้โมเดลในอุปกรณ์อย่าง Gemini Nano 4 ผ่าน ML Kit เพื่อสรุปชุดข้อความ เขียนคำตอบใหม่ และทำความเข้าใจรูปภาพ หรือใช้ภาษาธรรมชาติเพื่อเรียกใช้ฟังก์ชันของแอป

  • นอกจากนี้ คุณยังใช้โมเดล Gemini ในระบบคลาวด์ได้โดยตรงในแอปผ่าน Firebase AI Logic

  • แต่วันนี้เราจะมุ่งเน้นไปที่เสาหลักที่ 2 ซึ่งก็คือการตรวจสอบว่าความสามารถของแอปพร้อมใช้งานภายในระบบอัจฉริยะ

  • แล้วระบบอัจฉริยะคืออะไร

  • ดังนั้น นอกเหนือจากการพูดคุยและโฮสต์เกี่ยวกับประสบการณ์การใช้งานที่อิงตามพรอมต์ที่ Sarah พูดถึงก่อนหน้านี้แล้ว ระบบนี้ยังช่วยให้สามารถผสานรวมอย่างลึกซึ้งเพื่อปรับปรุงประสบการณ์การใช้งานในระบบนิเวศของแอปด้วย

  • ดังนั้น อุปกรณ์ที่ออกแบบมาสำหรับ Gemini Intelligence จึงใช้ประโยชน์จากจุดผสานรวมเหล่านี้เพื่อให้บริการต่างๆ เช่น คำแนะนำ การแจ้งเตือนแบบสรุป การป้อนข้อมูลด้วยเสียงที่ได้รับการปรับปรุง และการกรอกแบบฟอร์มอัตโนมัติ

  • ตอนนี้คำแนะนำเป็นส่วนหนึ่งของฟีเจอร์ที่เรียกว่าการให้ความช่วยเหลือเชิงรุก

  • และจนถึงตอนนี้ เราได้พูดถึงความช่วยเหลือที่ได้รับแจ้งแล้ว

  • เราทุกคนคุ้นเคยกับเรื่องนี้ดี โดยที่ผู้ใช้จะบอกให้ Agent ทำงานอย่างน้อย 1 อย่างในนามของตน

  • แต่การให้ความช่วยเหลือเชิงรุกนั้นเป็นเรื่องของการคาดการณ์การดำเนินการถัดไปที่ผู้ใช้อาจทำ

  • ดังนั้น หากย้อนกลับไปที่ตัวอย่างมื้อค่ำของเรา เอเจนต์อาจแจ้งเตือนผู้ใช้ในเชิงรุกด้วยคำแนะนำเพื่อตอบกลับข้อความที่สามารถเพิ่มคำเชิญลงในปฏิทินหรือจองบริการร่วมเดินทางได้

  • แต่ระบบจะแสดงข้อความแจ้งผู้ใช้อย่างไร

  • การให้ความช่วยเหลือเชิงรุกสามารถแสดงการดำเนินการในตำแหน่งต่างๆ เช่น หน้าจอล็อก การแจ้งเตือน แป้นพิมพ์เสมือน และแม้แต่ภายในแอปที่เลือก

  • แล้วคุณจะทำอย่างไรให้แอปปรากฏในชิปเชิงรุกเหล่านี้เพื่อเพิ่มการเข้าชม

  • คุณได้ทำสิ่งต่างๆ เช่น การสร้างโดยอิงตาม SDK เวอร์ชันล่าสุดและการสร้างด้วยเครื่องมือที่ทันสมัยที่สุดของเราแล้ว แต่สิ่งที่ดีที่สุดที่คุณทำได้ในตอนนี้คือตรวจสอบว่าแอปของคุณพร้อมสำหรับความช่วยเหลือที่ได้รับแจ้งด้วย ซึ่ง Sarah จะพูดถึงในลำดับถัดไป

ความช่วยเหลือแบบมีข้อความแจ้งและ Android MCP

  • ขอบคุณ Dan

  • โอเค ความช่วยเหลือแบบมีข้อความแจ้งจะเกี่ยวข้องกับการที่ผู้ใช้ไปที่ Agent โดยตรงและขอให้เริ่มต้นและจัดการงานต่างๆ แม้กระทั่งในแอปต่างๆ

  • ซึ่งจะช่วยเปลี่ยนเวิร์กโฟลว์ที่เคยต้องทำเองและใช้เวลานานให้เป็นระบบอัตโนมัติ

  • สำหรับระบบอัจฉริยะบน Android เรามุ่งเน้นที่ตัวแทนระบบเป็นหลัก เช่น Gemini ที่สามารถเข้าถึงแอปต่างๆ ในอุปกรณ์ของผู้ใช้ได้

  • แอปของคุณสามารถปรากฏในประสบการณ์การใช้งานนี้และโต้ตอบกับตัวแทนระบบได้หลายวิธี

  • เรามีโซลูชันการทำงานอัตโนมัติที่ขับเคลื่อนด้วยแพลตฟอร์ม และการผสานรวม API โดยตรงเข้ากับ System Agent

  • เราขอแนะนำให้สร้างด้วยการผสานรวม API โดยตรงทุกครั้งที่ทำได้

  • ซึ่งนักพัฒนาแอปเป็นผู้ควบคุม คุณจึงตัดสินใจและควบคุมลักษณะที่แอปและแบรนด์จะปรากฏได้

  • นอกจากนี้ยังทำงานได้เร็วขึ้นและเชื่อถือได้มากขึ้น ซึ่งจะมอบประสบการณ์การใช้งานที่พรีเมียมยิ่งขึ้นให้แก่ผู้ใช้

  • คุณสามารถพิจารณาเส้นทาง API 2 เส้นทางเมื่อต้องการผสานรวมกับ Agent

  • Model Context Protocol หรือเรียกสั้นๆ ว่า MCP เป็นแนวทางปฏิบัติแนะนำในอุตสาหกรรมสำหรับการเรียกใช้ฟังก์ชันฝั่งเซิร์ฟเวอร์

  • แต่สำหรับการเรียกใช้เครื่องมือในอุปกรณ์ เราขอประกาศเปิดตัว Android MCP อย่างเป็นทางการ ซึ่งเป็นเฟรมเวิร์กการเรียกใช้ฟังก์ชันแบบรวมใน Android

  • Android MCP ขับเคลื่อนโดยไลบรารี Jetpack AppFunctions ซึ่งสอดคล้องกับ MCP อย่างใกล้ชิดทั้งในด้านเทคนิคและแนวคิด ขณะเดียวกันก็รองรับการใช้งาน Kotlin ที่คุ้นเคยและเป็นสำนวนที่นักพัฒนาแอป Android หลายคนคุ้นเคย

  • ซึ่งทำให้การใช้ API ค่อนข้างเข้าใจง่ายและใช้งานง่าย และคุณสามารถใช้ประโยชน์จากสิ่งที่ได้เรียนรู้จากการผสานรวม MCP ฝั่งเซิร์ฟเวอร์ที่สร้างไว้แล้ว

  • องค์ประกอบพื้นฐานของ MCP หลายรายการจะแมปแบบหนึ่งต่อหนึ่งกับโครงสร้าง Jetpack AppFunctions

  • ลองดูตัวอย่างต่อไปนี้เพื่อทำความเข้าใจเรื่องนี้

  • ปลายทางของเครื่องมือ MCP จะแมปกับฟังก์ชัน Kotlin ที่มีคำอธิบายประกอบ AppFunctionDeclaration ได้อย่างลงตัว

  • โมเดล pydantic ของคุณจะแมปกับคลาสข้อมูล Kotlin ที่มีคำอธิบายประกอบเป็น AppFunctionSerializable

  • แต่ Android MCP จะทำงานบน Android Binder IPC ที่เพิ่มประสิทธิภาพแล้ว แทนที่จะเป็น HTTP ที่สตรีมได้และ/หรือการรับส่งข้อมูล I/O มาตรฐาน

  • วันนี้เรายินดีอย่างยิ่งที่จะแจ้งให้ทราบว่าไลบรารี Jetpack อย่าง AppFunctions อยู่ในเวอร์ชันเบต้าแล้ว

กรณีการใช้งานหลักสำหรับ Android MCP

  • คุณอาจสงสัยว่าเมื่อใดควรใช้เส้นทาง API เดียว และเมื่อใดควรใช้ Android MCP โดยเฉพาะ

  • หลักการโดยรวมคือ Android MCP จะมีประโยชน์อย่างยิ่งเมื่อคุณต้องการใช้ประโยชน์จากบริบทในอุปกรณ์หรือบริบทเฉพาะรายการเพื่อตอบสนองเส้นทางของผู้ใช้

  • มาเจาะลึกตัวอย่างที่ Android MCP โดดเด่นกัน

  • โดยเฉพาะอย่างยิ่ง คุณควรพิจารณาใช้ Android MCP เมื่อต้องการเข้าถึงข้อมูลที่เข้ารหัสในเครื่อง

  • ซึ่งสำคัญอย่างยิ่งสำหรับแอปรับส่งข้อความและแอปการสื่อสารที่ต้องเก็บประวัติข้อความไว้ในอุปกรณ์เนื่องจากการเข้ารหัสจากต้นทางถึงปลายทาง

  • นอกจากนี้ Android MCP ยังมีประโยชน์สำหรับการดำเนินการในอุปกรณ์และแบบออฟไลน์เท่านั้น ซึ่งตัวแทนรองรับการดำเนินการนี้

  • Samsung Health ซึ่งเป็นพาร์ทเนอร์ Wear OS ของเราได้เปิดตัวฟังก์ชันเพื่อเริ่มวิ่งและเริ่มออกกำลังกาย ซึ่งจัดการทั้งหมดในอุปกรณ์

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

  • นอกจากนี้ Android MCP ยังเป็นเส้นทาง API ที่แนะนำหากคุณต้องการเข้าถึงเซ็นเซอร์ของอุปกรณ์หรือโต้ตอบกับฮาร์ดแวร์ของอุปกรณ์โดยตรง

  • ตัวอย่างเช่น แอปยานยนต์จำนวนมากต้องการอนุญาตให้ผู้ใช้ปลดล็อกประตูรถได้ทันทีผ่านการเชื่อมต่อ UWB และมักจะเป็นแบบแฮนด์ฟรี

  • Android MCP ช่วยคุณได้

  • และสุดท้าย โซลูชันนี้เหมาะสำหรับคุณหากไม่มี Cloud Endpoints ที่พร้อมใช้งานสำหรับแอปหรือ API

  • แอปของบุคคลที่หนึ่งของผู้ผลิตอุปกรณ์หลายรายเป็นแบบฝั่งไคลเอ็นต์เท่านั้น จึงใช้ Android MCP ด้วยเหตุผลนี้

  • ตัวอย่างเช่น คุณสามารถใช้ Gemini เพื่อค้นหาและดึงรูปภาพจากแอปแกลเลอรีที่อยู่ฝั่งไคลเอ็นต์เท่านั้น เช่น Xiaomi Gallery

  • ตรงนี้ ฉันสามารถเปิด Gemini แล้วถามว่า "ค้นหารูปภาพหุ่นยนต์ Android ที่ฉันถ่ายเมื่อสัปดาห์ที่แล้วใน Xiaomi Gallery"

  • Xiaomi ได้เปิดเผยฟังก์ชันของแอปเพื่อค้นหารูปภาพ ซึ่ง Gemini จะเรียกใช้ จากนั้นระบบจะแสดงรูปภาพที่เกี่ยวข้องใน UI ของแอป Gemini โดยตรง เพื่อให้ผู้ใช้ไม่ต้องออกจากแอป

  • และเราใช้สถาปัตยกรรมเดียวกันนี้สำหรับการดำเนินการในอุปกรณ์ในระบบปฏิบัติการ Android

  • Android MCP ขับเคลื่อนฟีเจอร์ความช่วยเหลือเกี่ยวกับอุปกรณ์ที่เพิ่งเปิดตัว ซึ่งเป็นการผสานรวมระบบปฏิบัติการ Android เข้ากับแอป Gemini โดยตรง

  • ความสามารถนี้เปิดตัวในอุปกรณ์ Pixel และ Lenovo แล้ว และกำลังเพิ่มโหนดไปยังระบบนิเวศ OEM ของ Android ในวงกว้าง

  • ผู้ใช้สามารถใช้ความช่วยเหลือเกี่ยวกับอุปกรณ์ได้โดยไปที่ Gemini และถามวิธีเพิ่มประสิทธิภาพการตั้งค่า การตั้งค่าอุปกรณ์ และการตั้งค่าระบบเพื่อปรับปรุงประสบการณ์การใช้งานอุปกรณ์

  • ตัวอย่างเช่น แทนที่จะต้องค้นหาผ่านเมนูต่างๆ ผู้ใช้ที่ตาบอดสีอาจขอความช่วยเหลือจาก Gemini ว่า "ฉันตาบอดสี

  • ฉันจะทำให้โทรศัพท์เครื่องนี้ทำงานได้ดีขึ้นสำหรับฉันได้อย่างไร"

  • Gemini จะเรียกใช้ฟังก์ชันของแอปในเครื่อง และพิจารณาคำขออย่างชาญฉลาด พร้อมทั้งแนะนำวิธีที่ผู้ใช้สามารถเปลี่ยนการตั้งค่าระบบได้จริง และวิธีเฉพาะเจาะจงที่ผู้ใช้สามารถเปลี่ยนการตั้งค่าอุปกรณ์เพื่อปรับปรุงประสบการณ์การใช้งาน

  • จากนั้น การควบคุมในแอปจะแสดงขึ้นโดยตรงใน UI ของแอป Gemini เพื่อให้ผู้ใช้ควบคุมได้ทันที

  • เช่น ในการตอบสนองคำขอเพิ่มประสิทธิภาพสำหรับผู้ใช้ตาบอดสีนี้ Gemini ได้แสดงปุ่มเปิด/ปิดการแก้สีนี้ขึ้นมา เพื่อให้ผู้ใช้ทำการเปลี่ยนแปลงได้ทันที

  • ประสบการณ์การใช้งาน MCP ของ Android พร้อมใช้งานแล้วในอุปกรณ์บางรุ่น และเรากำลังทำงานร่วมกับพาร์ทเนอร์หลายรายเพื่อนำ Use Case เพิ่มเติมมาสู่ระบบนิเวศของ Android

  • แม้ว่าการประกาศเครื่องมือ MCP ของ Android อย่างชัดเจนจะเป็นมาตรฐานสูงสุดของเรา แต่เราทราบดีว่าจะมีบางกรณีที่ผู้ใช้จะขอให้ Agent ดำเนินการ Use Case ที่ซับซ้อนหรือพบได้ไม่บ่อยนัก และนี่คือจุดที่ฟีเจอร์การทำงานอัตโนมัติของงานที่ขับเคลื่อนโดยแพลตฟอร์มของเราเข้ามามีบทบาท

  • ภายใต้โมเดลนี้ ตัวแทนที่โหลดไว้ล่วงหน้าของ OEM จะใช้ API การควบคุมคอมพิวเตอร์ของ Android เพื่อจำลองการโต้ตอบแบบสัมผัสบนแพลตฟอร์มของแอปได้

  • ซึ่งไม่จำเป็นต้องมีการเปลี่ยนแปลงโค้ดจากนักพัฒนาแอป และตัวแทนจะต้องแตะผ่าน UI เหมือนกับที่ผู้ใช้ทำ โดยการวิเคราะห์หน้าจอแอป

  • เพื่อให้ผู้ใช้ควบคุมและปลอดภัย สิ่งสำคัญคือเอเจนต์ต้องหยุดชั่วคราวเมื่อดำเนินการที่อาจมีความละเอียดอ่อน เช่น ขั้นตอนการชำระเงิน และขอให้ผู้ใช้ยืนยันเพื่อดำเนินการต่อ

  • มาดูตัวอย่างการใช้งานจริงกัน

  • ในตัวอย่างนี้ ผู้ใช้ถ่ายรูปทัวร์ชิมกาแฟและช็อกโกแลต และต้องการถาม Expedia หรือต้องการขอความช่วยเหลือจาก Gemini ให้ตรวจสอบ Expedia และดูว่าสามารถค้นหาทัวร์ที่คล้ายกันได้หรือไม่ แต่ปรับจำนวนผู้เข้าร่วมเป็น 6 คน

  • Gemini จะไปยังส่วนต่างๆ ของ UI ของ Expedia ในเบื้องหลังในลักษณะเดียวกับที่ผู้ใช้ทำ โดยไม่จำเป็นต้องผสานรวม API โดยตรง

  • จากนั้นจะแสดงตัวเลือกที่เกี่ยวข้องที่พบและส่งต่อให้ผู้ใช้อย่างราบรื่นเมื่อถึงเวลาสำหรับการยืนยันและการจองขั้นสุดท้าย

  • คุณควรทราบว่าทั้ง 3 แนวทางนี้ไม่ได้แยกออกจากกันโดยสิ้นเชิง และจริงๆ แล้วจะทำงานร่วมกันได้ดีกว่า

  • ผู้ใช้และนักพัฒนาแอปควรคาดหวังให้ Agent สามารถประสานงานเครื่องมือต่างๆ ในการติดตั้งใช้งานเซิร์ฟเวอร์ รวมถึงแอป Android ในเครื่องเพื่อมอบประสบการณ์ที่ครอบคลุมและเป็นองค์รวมมากที่สุดแก่ผู้ใช้

ข้อมูลเจาะลึกทางเทคนิคเกี่ยวกับ Android MCP

  • โอเค

  • ขอบคุณนะ Sarah

  • เราจะมาเจาะลึกเกี่ยวกับ MCP ของ Android กัน

  • และหากเป็นไปได้ ฉันอยากให้คุณทราบว่าฉันกำลังดึงข้อมูลโค้ด MCP จาก Jetpacker ซึ่งเป็นแอปตัวอย่างการเดินทางของเรา ซึ่งสร้างขึ้นโดยใช้ Antigravity

  • คุณได้เห็นฟีเจอร์นี้ในคีย์โน้ตแล้ว

  • ซึ่งมาพร้อมฟีเจอร์อัจฉริยะสุดเจ๋งทุกประเภท รวมถึงการผสานรวมในอุปกรณ์กับ ML Kit Gen AI และ ML Kit Speech Recognition API

  • ตอนนี้ Android MCP และ MCP ในระบบคลาวด์มีโครงสร้างที่คล้ายกัน

  • ทั้ง 2 ระบบมีไคลเอ็นต์และเซิร์ฟเวอร์

  • ในโลกของ MCP เซิร์ฟเวอร์จะจัดเตรียมเครื่องมือต่างๆ ส่วนไคลเอ็นต์จะค้นหาและเรียกใช้เครื่องมือเหล่านี้ได้

  • ตอนนี้เมื่อใช้ Android MCP แอปของคุณจะเป็นเซิร์ฟเวอร์ และ Agent จะเป็นไคลเอ็นต์

  • แพลตฟอร์ม Android ทำหน้าที่เป็นรีจิสทรีเครื่องมือ

  • ตอนนี้แอปจะเริ่มด้วยการลงทะเบียนบริการเพื่อจัดหาเซิร์ฟเวอร์ และเราจะเริ่มด้วยการเพิ่มทรัพยากร Dependency ของไลบรารี AppFunctions Jetpack ในตัวอย่างที่นี่

    implementation(libs.androidx.appfunctions)
    ksp(libs.androidx.appfunctions.compiler)
  • จากนั้นเราจะใช้คำอธิบายประกอบ KSP เพื่อช่วยสร้าง AppFunctionService

  • บริการนี้หมายความว่าฟังก์ชันของแอปจะทำงานตามวงจรของ Android ได้อย่างถูกต้อง

  • เราสามารถใช้การแทรกทรัพยากร Dependency กับ Hilt เพื่อแทรกที่เก็บข้อมูลที่ AppFunction ของเราใช้ได้

    @RequiresApi(36)
    @AndroidEntryPoint
    @AppFunctionServiceEntryPoint(
        serviceName = "JetPackerAppFunctionService",
        appFunctionXmlFileName = "jetpacker_app_function_service"
    )
    abstract class BaseJetPackerAppFunctionService : AppFunctionService() {
        @Inject internal lateinit var tripDao: TripDao
        @Inject internal lateinit var expenseDao: ExpenseDao
        ...
  • ขออภัย

  • กลับไปที่แผนภาพของเรา

  • หลังจากตั้งค่าบริการสำหรับเซิร์ฟเวอร์ Android MCP แล้ว แอปจะลงทะเบียนเครื่องมือในรูปแบบ AppFunctions พร้อมกับโมเดลข้อมูลที่เกี่ยวข้องซึ่งเครื่องมือเหล่านั้นใช้

  • ตอนนี้คุณสามารถลงทะเบียนโมเดลข้อมูลโดยใช้คำอธิบายประกอบ AppFunctionSerializable และ KDoc นี้ก็เจ๋งมาก

    /** Represents an event in a trip itinerary. */
    @AppFunctionSerializable(isDescribedByKDoc = true)
    data class TimelineEventSerializable(
      /** The event's unique identifier. */
      val id: String,
      /** The trip ID this event belongs to. */
      val tripId: String,
      /** The type of event (e.g., TRANSPORTATION, ACCOMMODATION, FOOD_AND_DRINK). */
      val type: String,
      /** The timestamp of the event. */
      val timestamp: Long,
      /** The title of the event. */
      val title: String,
      /** The location of the event. */
      val location: String,
      /** A description of the event. */
      val description: String?,
      /** The title of the trip this event belongs to. */
      val tripTitle: String? = null,
      /** Optional title of a matched itinerary event when added as a reminder. */
      val matchedEventTitle: String? = null,
      /** Execution status of the tool call (e.g., SUCCESS). */
      val status: String? = "SUCCESS",
      /** Human-readable completion message instructing the agent not to repeat the call. */
      val message: String? = null,
    )
  • แต่จะกลายเป็นข้อมูลเมตาที่ Agent ใช้เพื่อทำความเข้าใจวิธีใช้โมเดล

  • คุณสามารถใช้คำอธิบายประกอบ AppFunctionDeclaration เพื่อลงทะเบียนฟังก์ชันเป็นเครื่องมือได้

    @AppFunctionDeclaration(isDescribedByKDoc = true)
    suspend fun getItinerary(tripId: String? = null): List<TimelineEventSerializable> {
      return withContext(Dispatchers.IO) {
        val trip = resolveTrip(tripId = tripId)
        val targetTripId = trip?.id ?: ActiveTripSession.activeTripId ?: tripId?.takeIf { it.isNotBlank() } ?: "2026-1"
        val dbEvents = eventDao.getEventsForTrip(targetTripId).first()
        val events =
          if (dbEvents.isNotEmpty()) { dbEvents } else {
            val fallback = DummyData.events.filter { it.tripId == targetTripId }
            if (fallback.isNotEmpty()) { eventDao.insertEvents(fallback) }
            fallback
          }
        events.map { it.toSerializable(tripTitle = trip?.title) }
      }
    }
  • และเมื่อตั้งค่า isDescribedByKDoc เป็น "จริง" KDoc จะให้คำอธิบายแก่ Agent เพื่อให้เข้าใจว่าฟังก์ชันของคุณทำอะไร และยังช่วยให้มนุษย์เข้าใจด้วยเช่นกัน

    /**
     * Retrieves the itinerary events (with their eventId, title, type, and location) for a specific
     * trip. Note: getItinerary returns event summaries only; call getEventDetails(eventId) with a
     * specific event's ID to retrieve ticket prices, hotel nightly rates, baggage allowance, or
     * visitor tips.
     *
     * @param tripId Optional ID of the trip (e.g., '2026-1'). Defaults to the active trip if omitted.
     * @return A list of events in the itinerary.
     */
    @AppFunctionDeclaration(isDescribedByKDoc = true)
    suspend fun getItinerary(tripId: String? = null): List<TimelineEventSerializable> {
      return withContext(Dispatchers.IO) {
        val trip = resolveTrip(tripId = tripId)
        val targetTripId = trip?.id ?: ActiveTripSession.activeTripId ?: tripId?.takeIf { it.isNotBlank() } ?: "2026-1"
        val dbEvents = eventDao.getEventsForTrip(targetTripId).first()
    ...
  • และต่อไปนี้คือแนวทางปฏิบัติแนะนำบางส่วนสำหรับการออกแบบพารามิเตอร์

    /**
     * @param tripId Optional ID of the trip (e.g., '2026-1').
     *   Defaults to the active trip if omitted.
     * @return A list of events in the itinerary.
     */
    @AppFunctionDeclaration(isDescribedByKDoc = true)
    suspend fun getItinerary(tripId: String? = null): List<TimelineEventSerializable> {
      return withContext(Dispatchers.IO) {
        // resolveTrip defaults to the active trip if tripId == null
        val trip = resolveTrip(tripId = tripId)
  • ก่อนอื่น คุณควรเน้นที่ชุดย่อยของพารามิเตอร์ที่น้อยที่สุดซึ่งมีชื่อที่ชัดเจนและแตกต่างกัน

  • ทำให้พารามิเตอร์ที่ไม่จำเป็นเป็นค่า Null ได้ และแก้ไขค่าเริ่มต้นในเนื้อหาฟังก์ชัน

  • นี่ไม่ใช่แค่แนวทางปฏิบัติแนะนำ

  • ซึ่งจะใช้ไม่ได้ผล เนื่องจากระบบจะไม่สนใจค่าเริ่มต้นของ Kotlin เพราะไลบรารี AppFunctions จะส่งค่า Null สำหรับพารามิเตอร์ที่อนุญาตค่า Null ที่ละไว้

  • เราจะไม่แสดงตัวอย่างของวิธีแรกนี้ แต่ขอแนะนำว่าอย่าใช้พารามิเตอร์ที่ครอบคลุมทั้งหมด

  • สิ่งต่างๆ เช่น บริบทแบบเต็มหรือประวัติการสนทนาล้วนไม่ดีทั้งนั้น

  • ซึ่งอาจทำให้ Agent ส่งข้อมูลจำนวนมากและนำไปสู่การขโมยข้อมูล และเป็นแนวทางปฏิบัติที่ไม่ดี

  • เนื่องจาก AppFunction จะทำงานในเธรด UI โดยค่าเริ่มต้น คุณจึงต้องตรวจสอบว่าการดำเนินการที่ใช้เวลานานเกิดขึ้นในเบื้องหลังโดยเลือกผู้มอบหมายงาน Coroutine ที่เหมาะสม

    @AppFunctionDeclaration(isDescribedByKDoc = true)
    suspend fun getItinerary(tripId: String? = null): List<TimelineEventSerializable> {
      return withContext(Dispatchers.IO) {
        val trip = resolveTrip(tripId = tripId)
        val targetTripId = trip?.id ?: ActiveTripSession.activeTripId ?:
        tripId?.takeIf { it.isNotBlank() } ?: "2026-1"
        val dbEvents = eventDao.getEventsForTrip(targetTripId).first()
        val events =
          if (dbEvents.isNotEmpty()) { dbEvents } else {
            val fallback = DummyData.events.filter { it.tripId == targetTripId }
            ...
  • คุณควรแสดงข้อยกเว้น AppFunction เมื่อจำเป็นพร้อมข้อมูลโดยละเอียด เพื่อให้ Agent เข้าใจวิธีแก้ไขข้อผิดพลาด

    @AppFunctionDeclaration(isDescribedByKDoc = true)
    suspend fun send(
        message: Message
    ): Result {
      ...
      throw AppFunctionException(
        errorCode = AppFunctionException.ErrorCode.ERROR_INVALID_ARGUMENT,
        errorMessage = "To address has invalid syntax!"
      )
    }
  • เอาล่ะ ตอนนี้เรามี AppFunction แล้ว มาดูแผนภาพสถาปัตยกรรมกันอีกครั้ง

  • แอปไคลเอ็นต์ของ Agent จะลงทะเบียนกับแพลตฟอร์มเพื่อเรียกใช้ AppFunctions

  • ตัวอย่างเช่น Gemini

  • และในการดำเนินการดังกล่าว ตัวแทนจะต้องมีสิทธิ์พิเศษ จากนั้นตัวแทนที่ลงทะเบียนจะสามารถค้นหาข้อมูลเมตาของ AppFunction จากแพลตฟอร์ม Android และตัดสินใจว่าจะเรียกใช้ AppFunction ที่ลงทะเบียนไว้ 1 รายการหรือมากกว่านั้นเพื่อดำเนินการตามคำขอของผู้ใช้หรือไม่

  • เมื่อพูดถึง AppFunction อย่างน้อย 1 รายการ Agent จะตัดสินใจอย่างไร

  • สิ่งสุดท้ายที่คุณต้องทำคืออธิบายความสามารถของแอปในไฟล์ข้อมูลเมตาของแอป

    <AppFunctionAppMetadata
    xmlns:appfn="http://schemas.android.com/apk/androidx.appfunctions"
    
      appfn:description="This app manages trips and travel expenses.
    
      Operational Patterns:
        - Use 'getAllTrips' to find the correct 'tripId' before
          adding expenses or managing trip details.
    
      Constraints:
        - Expenses must be associated with a valid trip ID."
    
      appfn:displayDescription="@string/user_visible_description" />
  • และ Agent จะเลือกเครื่องมือโดยอ่านคำอธิบายนี้ก่อนภายใน AppFunctionAppMetadata แล้วจึงอ่านคำอธิบายพร้อมกับ AppFunction แต่ละรายการอีกครั้ง

  • ดังนั้น คุณควรเน้นที่ส่วนต้นของคำอธิบายด้วยคำอธิบายสั้นๆ เกี่ยวกับโดเมนหลักของแอปหรือเครื่องมือ

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

  • และคุณสามารถเพิ่มส่วนเกี่ยวกับข้อจำกัดที่กำหนดขีดจำกัดเชิงตรรกะของประเภทข้อมูลอินพุตหรือเสริมรูปแบบการทำงานได้

  • ที่กล่าวมาคือข้อมูลเบื้องต้นเกี่ยวกับ Android MCP แต่ยังมีสิ่งอื่นๆ ที่คุณทำได้อีก

  • สิ่งหนึ่งที่คุณทำได้ซึ่งค่อนข้างเจ๋งคือคุณสามารถเปิดและปิดใช้ AppFunctions แบบไดนามิกได้

    viewModelScope.launch {
        val newState =
            if (enabled) {
                AppFunctionManager.APP_FUNCTION_STATE_ENABLED
            } else {
                AppFunctionManager.APP_FUNCTION_STATE_DISABLED
            }
        appFunctionManager?.setAppFunctionEnabled(id, newState)
    }
  • ซึ่งเป็นสิ่งสำคัญ เช่น หากคุณต้องการให้ AppFunction พร้อมใช้งานเฉพาะเมื่อผู้ใช้เข้าสู่ระบบเท่านั้น

  • อีกสิ่งหนึ่งที่คุณทำได้คือสร้าง AppFunctions ที่กำหนดขอบเขตไว้สำหรับหน้าจอหรือกิจกรรมที่เฉพาะเจาะจงในแอป

    @AppFunctionSignature(
        scope = AppFunctionMetadata.SCOPE_ACTIVITY,
        appFunctionXmlFileName = "cart_functions"
    )
    fun interface AddCurrentItemToCart {
        /** Adds the item currently shown on screen to the cart. Returns the new cart size. */
        suspend fun addToCart(quantity: Int): Int
    }
  • ดังนั้น แทนที่จะประกาศ AppFunction คุณจะประกาศ AppFunctionSignature แทน

  • จากนั้นคุณสามารถใช้แฮนเดิลสำหรับ AppFunction ใน UI ได้

    val addToCartAppFunctionAdapter =
        appFunctionManager.getHandleAppFunctionRequestAdapter<AddCurrentItemToCart>()
    
    coroutineScope.launch {
        appFunctionManager.handleAppFunction(
            addToCartAppFunctionAdapter.adapt { quantity ->
                val newCartSize = cart.addItem(quantity)
                newCartSize
            }
        )
    }
  • และส่วนที่น่าสนใจเกี่ยวกับเรื่องนี้คือวงจรของตัวแฮนเดิลจะเชื่อมโยงกับวงจรของขอบเขต Coroutine ที่คุณใช้โดยอัตโนมัติ

  • ตอนนี้คุณจะต้องแคชอะแดปเตอร์คำขอ AppFunctions นี้ ขออภัย อะแดปเตอร์คำขอ handle AppFunctions เนื่องจากไลบรารีใช้การสะท้อนภายในเพื่อสร้างอินสแตนซ์

  • นอกจากนี้ยังมีฟังก์ชันการทำงานเพื่อสร้าง AppFunctions ที่กำหนดขอบเขตซึ่งการลงทะเบียนไม่ได้เชื่อมโยงกับวงจรของ Android โดยตรง

  • สิ่งสุดท้ายคือเรามีสิ่งที่เรียกว่าระดับการเข้าถึง

    @AppFunctionDeclaration(isDescribedByKDoc = true)
    @AppFunctionAccessLevel(
        level = AppFunctionMetadata.ACCESS_LEVEL_SELF,
        isCompatEnforcementEnabled = true,
    )
    suspend fun getItinerary(tripId: String? = null): List<TimelineEventSerializable> {
      return withContext(Dispatchers.IO) {
      ...
  • และคุณสามารถรับและใช้ระดับการเข้าถึงของตนเองหรือระบบ หรือที่เชื่อถือได้ ซึ่งจะระบุว่าฟังก์ชันใดมีไว้สำหรับใช้กับตัวแทนในแอปของคุณเองเท่านั้น เช่น ฟังก์ชันที่สร้างด้วย ML Kit GenAI ฟังก์ชันใดที่ใช้ได้เฉพาะกับระบบ และฟังก์ชันใดที่ใช้ได้กับตัวแทนระบบที่เชื่อถือได้

  • และเพื่อช่วยคุณสร้าง Agent ในแอปของคุณเอง เราได้ประกาศไปแล้วว่า ML Kit รองรับ AppFunctions โดยค่าเริ่มต้น ซึ่งเป็นส่วนหนึ่งของเฟรมเวิร์กการเรียกใช้เครื่องมือแบบรวม

  • คุณกำหนดเครื่องมือเหล่านี้เพียงครั้งเดียวและจะได้รับการครอบคลุมทั้ง Agent ระดับระบบและโมเดลในแอปในเครื่อง ซึ่งจะช่วยให้คุณหลีกเลี่ยงการประกาศเครื่องมือที่กระจัดกระจายได้

  • และหากต้องการดูข้อมูลเพิ่มเติม คุณสามารถดูการพูดคุยนี้ได้ที่หัวข้อ "ปลดล็อกความสามารถด้าน Agentic AI ในแอปด้วย ML Kit GenAI" หรือจะไปดูข้อมูลล่าสุดใน Jetpack ก็ได้

  • เจ๋งมากเลย

  • เอาล่ะ กลับมาที่ Sarah

  • โอเค

  • Dan เพิ่งแสดงให้คุณเห็นวิธีการทำงานเบื้องหลังของ Android MCP

  • มาลองนำไปใช้จริงกัน

  • โปรดทราบว่าแอปของคุณคือเซิร์ฟเวอร์และ Agent คือไคลเอ็นต์ ดังนั้นเป้าหมายจึงควรเป็นการตรวจสอบว่าแอปของคุณเป็นเซิร์ฟเวอร์ที่ดีสำหรับ Agent

  • เราจะติดตาม Jetpacker ซึ่งเป็นแอปตัวอย่างด้านการเดินทาง ตั้งแต่การประกาศเครื่องมือครั้งแรกไปจนถึงการเรียกใช้ฟังก์ชันจริงผ่าน Gemini

  • คุณสามารถเขียน AppFunctions ด้วยตนเองหรือใช้เอเจนต์การเขียนโค้ดเพื่อช่วยคุณเริ่มต้นใช้งาน

  • เราได้เปิดตัวทักษะ AppFunctions ที่จะช่วยคุณเริ่มต้นเขียนฟังก์ชัน ซึ่งคุณสามารถเชื่อมต่อกับ Android Studio ผ่าน Android CLI หรือใช้กับเอเจนต์การเขียนโค้ดที่คุณเลือกได้

    android skills add appfunctions
  • ทักษะนี้ทำงานใน 3 ขั้นตอน

  • ขั้นแรก ระบบจะวิเคราะห์ฐานของโค้ดเพื่อระบุเส้นทางของผู้ใช้หลักที่จะสร้างเครื่องมือที่มีประโยชน์ เช่น การเล่นสื่อ การสร้างโน้ต หรือการส่งข้อความ

  • จากนั้นจะสร้างการประกาศ AppFunction ที่แสดงเส้นทางเหล่านั้นพร้อมกับการติดตั้งใช้งานและการสนับสนุนแพลตฟอร์มที่จำเป็น

  • เช่น Gradle และ KSP, XML ข้อมูลเมตาของแอป และ Dependency Injection

  • สุดท้าย ระบบจะปรับแต่งคำอธิบาย KDoc เนื่องจากนี่คือสิ่งที่ Agent ใช้ในการตัดสินใจว่าจะเรียกใช้ฟังก์ชันของคุณเมื่อใดและอย่างไร

  • นี่คือประเภทของผลลัพธ์ที่เรากำลังมองหา

  • นี่เป็นฟังก์ชัน addExpense สำหรับ Jetpacker

    /**
     * Adds an expense to a trip.
     *
     * @param tripId ID of the trip. If null, the active trip is used.
     * @param category Expense category. If null, it's inferred from the title.
     */
    @AppFunctionDeclaration(isDescribedByKDoc = true)
    suspend fun addExpense(
        title: String,
        amount: Double,
        currency: String,
        tripId: String? = null,
        category: String? = null,
    ): ExpenseSerializable = withContext(Dispatchers.IO) {
        if (amount <= 0.0) {
            throw AppFunctionInvalidArgumentException("amount must be greater than zero")
        }
        val trip = resolveTrip(tripId)
            ?: throw AppFunctionElementNotFoundException("No trip with ID $tripId")
        val expense = Expense(trip.id, title, amount, currency,
            category = category ?: inferExpenseCategory(title))
        expenseDao.insertExpense(expense)
        expense.toSerializable(remainingBudgetUsd(trip.id))
    }
  • คำอธิบายคือประโยคบรรทัดเดียวที่ขึ้นต้นด้วยคำกริยาและทรัพยากร เช่น เพิ่มค่าใช้จ่ายในการเดินทาง

  • ฟิลด์การเดินทางและหมวดหมู่เป็นค่า Null ได้เนื่องจากผู้ใช้อาจไม่ได้ระบุค่านี้เสมอไป แต่ฟังก์ชันจะแก้ไขค่าเหล่านี้ในเนื้อหา

  • และเมื่อคำขอไม่สำเร็จ ระบบจะแสดง AppFunctionException เพื่อให้เอเจนต์ทราบว่าเกิดข้อผิดพลาดอะไรขึ้น

การทดสอบฟังก์ชันและ Agent ของแอป

  • ทีนี้เรามาดูเรื่องการทดสอบกัน

  • ตอนนี้ฟังก์ชันของคุณเป็นบริการที่ Agent สามารถใช้ได้แล้ว

  • ดังนั้นคุณจะต้องทดสอบ 2 ส่วน ได้แก่ ลักษณะการทำงานของฟังก์ชันและลักษณะการทำงานของ Agent

  • สำหรับฟังก์ชันของคุณ เราจะเริ่มด้วย Unit Test ที่คุณอาจเขียนไว้แล้ว แต่มี 2-3 สิ่งที่ควรคำนึงถึงเมื่อ Agent เป็นผู้เรียกใช้ฟังก์ชันจริงๆ

  • ก่อนอื่น ค่าเริ่มต้นของ Kotlin จะไม่มีผลที่นี่

  • พารามิเตอร์ที่ละไว้จะแสดงเป็น null, false หรือ 0

  • ดังนั้น ขีดจำกัดที่ก่อนหน้านี้มีค่าเริ่มต้นเป็น 10 จะมีค่าเป็น 0 ในที่นี้

  • การเรียกใช้ยังเริ่มต้นในเทรดหลักและการเรียกใช้ฟังก์ชันจากการทดสอบอาจไม่ชัดเจนเสมอไป ดังนั้นคุณจึงควรเรียกใช้ IO มาตรฐานใน Dispatcher ในเบื้องหลัง

  • เมื่อเกิดข้อผิดพลาด ให้แสดง AppFunctionException

  • ส่วนข้อผิดพลาดอื่นๆ จะส่งไปยัง Agent เป็นข้อผิดพลาดทั่วไปที่ไม่รู้จักพร้อมข้อความข้อยกเว้นดิบที่แนบมา

  • และข้อมูลที่ป้อนจะมาจากโมเดล ดังนั้นคุณควรทดสอบสถานการณ์ที่ผิดปกติ เช่น ช่องว่าง คำพ้องความหมายของดอลลาร์ เช่น USD และค่าที่อยู่นอกช่วง

  • ในฝั่ง Agent คุณจะต้องทราบว่า Agent เลือกเครื่องมือของคุณในเวลาที่เหมาะสมหรือไม่ และเครื่องมือดังกล่าวเรียกใช้ฟังก์ชันได้อย่างถูกต้องหรือไม่

  • Agent แต่ละตัวจะทำงานแตกต่างกันเล็กน้อย คุณจึงควรทดสอบฟังก์ชันกับ Agent ที่คุณคาดว่าจะใช้ฟังก์ชันเหล่านั้น

  • คุณสามารถใช้ Agent ตัวอย่างแบบโอเพนซอร์สของเราเพื่อทดสอบ หรือจะใช้ Gemini เพื่อทดสอบการผสานรวมนี้ก็ได้

การเชื่อมต่อกับ Gemini ด้วยแพลตฟอร์มแอปที่เชื่อมต่อ

  • หากต้องการดูวิธีที่ Gemini โต้ตอบกับ AppFunctions หรือต้องการเปิด Android MCP ในแอป Gemini คุณสามารถทำได้ด้วยแพลตฟอร์มแอปที่เชื่อมต่อ ซึ่งเราเปิดตัวไปเมื่อช่วงเช้าของวันนี้ และปัจจุบันอยู่ในเวอร์ชันตัวอย่าง

  • ดูข้อมูลเพิ่มเติมได้โดยดูการพูดคุยก่อนหน้านี้เกี่ยวกับการขยายแอปไปยัง Gemini และ Search ผ่านแพลตฟอร์มแอปที่เชื่อมต่อ

  • เมื่อคุณเข้าร่วมโปรแกรมเวอร์ชันตัวอย่างแล้ว กระบวนการทดสอบกับ Gemini จะมี 2 ขั้นตอน

  • ก่อนอื่น ใน Google Connected Console เราจะสร้างแอปที่เชื่อมต่อซึ่ง Gemini สามารถใช้งานได้จริง

  • และประการที่ 2 ใน Play Console เราจะต้องอัปโหลด App Bundle ของคุณเพื่อให้เข้าถึง AppFunctions ได้ และคุณสามารถเริ่มในแทร็กทดสอบภายในเพื่อให้ผู้ทดสอบตรวจสอบฟังก์ชันได้จริง

  • มาเริ่มกันที่ Google Connected Console

  • ในส่วนนี้ คุณจะสร้างแอปที่เชื่อมต่อโดยใช้ปุ่ม "สร้างแอป" ที่ด้านขวาบน

  • เมื่อสร้างแอปที่เชื่อมต่อ ให้เลือก Android MCP ในหน้าจอนี้

  • หากคุณกำลังสร้างการติดตั้งใช้งาน MCP ฝั่งเซิร์ฟเวอร์ คุณจะต้องอัปโหลดโค้ดเซิร์ฟเวอร์ที่นี่

  • เมื่อคลิก "ตั้งค่า" แอป Google Play ที่เชื่อมโยงกับบัญชีนักพัฒนาแอปจะแสดงที่นี่เพื่อให้คุณลิงก์กับแอปที่เชื่อมต่อ

  • หากยังไม่มีแอป Google Play คุณจะต้องสร้างแอปใน Play Console ก่อน

  • ในที่นี้ เรามี Jetpacker อยู่แล้ว จึงจะลิงก์กับแอปดังกล่าว

  • ถัดไป ให้เพิ่มรายละเอียดที่ผู้ใช้จะเห็นในส่วนหน้าของ Gemini เมื่อพยายามโต้ตอบกับแอปของคุณ

  • ซึ่งรวมถึงสิ่งต่างๆ เช่น ไอคอนแอป ชื่อที่แสดง ความสามารถที่แอปที่เชื่อมต่อมี และพรอมต์ตัวอย่างที่ผู้ใช้สามารถใช้เพื่อโต้ตอบกับแอปได้จริง

  • โดยควรเป็นคำอธิบายเพื่อช่วยแนะนำผู้ใช้ได้ดีที่สุด

  • เอาล่ะ นั่นคือขั้นตอนการตั้งค่าแอปที่เชื่อมต่อ แต่คุณอาจสงสัยว่าแอปที่เชื่อมต่อจะเข้าถึงฟังก์ชันต่างๆ ได้อย่างไร

  • เนื่องจากฟังก์ชันต่างๆ อยู่ในโค้ดแอป ตอนนี้เราจึงต้องไปที่ Play Console และอัปโหลด App Bundle และสร้างแทร็กทดสอบ เพื่อให้เราทดสอบกับผู้ใช้จริงได้

  • ดังนั้นในหน้าจอนี้ เราจะอัปโหลด App Bundle แล้วจะเห็นว่า Google Play สามารถตรวจจับ AppFunctions ได้ และคุณสามารถลิงก์ AppFunctions กับแอปที่เชื่อมต่อได้โดยตรง

  • ตอนนี้เราจะเห็นแอปที่เชื่อมต่อ Jetpack พร้อมฟังก์ชันที่ผสานรวมแล้ว

  • ถัดไป เราจะต้องเพิ่มผู้ทดสอบ เพื่อให้มีผู้ที่พร้อมตรวจสอบฟังก์ชันของคุณและตรวจสอบว่าฟังก์ชันทำงานได้อย่างถูกต้อง

  • ดังนั้น ให้คลิกเวอร์ชันของแอปที่เชื่อมต่อที่นี่

  • ในหน้าจอนี้ คุณสามารถเลือก "จัดการผู้ทดสอบ" และกำหนดค่ารายชื่อผู้ทดสอบที่จำกัดซึ่งจะเข้าถึงแอปที่เชื่อมต่อใน Gemini ได้เมื่อพร้อมใช้งาน

  • คุณเพิ่มผู้ทดสอบลงในแทร็กทดสอบนี้ได้ด้วยวิธีเดียวกับที่ใช้กับรุ่นอื่นๆ

  • ตรวจสอบว่าคุณมีรายชื่อผู้ทดสอบสำหรับแอปที่เชื่อมต่อเหมือนกับรายชื่อผู้ทดสอบสำหรับแอป Google Play

เดโมการผสานรวมแอป Gemini

  • เอาล่ะ ทีนี้มาทดสอบการผสานรวมกัน

  • นี่คือแอป Jetpacker ในโทรศัพท์ของผู้ใช้ ซึ่งมีการบันทึกการเดินทางสำหรับการประชุมสุดยอดนี้ไว้แล้ว

  • ฉันจะเปิด Gemini เพื่อเชื่อมต่อแอป

  • ฉันต้องตรวจสอบว่าได้ติดตั้งแอปเวอร์ชันเดียวกับที่เราอัปโหลดใน Play ก่อนหน้านี้ไว้ในอุปกรณ์ และเราควรใช้บัญชีสำหรับ Gemini ที่เพิ่มลงในรายชื่อผู้ทดสอบ

  • ในกรณีนี้ ฉันจะพูดถึง Jetpacker และขอหมายเลขยืนยันของห้องพักโรงแรมในเมาน์เทนวิว

  • Gemini จะเรียกใช้ AppFunctions ของ Jetpacker บนอุปกรณ์และตอบว่า "RED11223 ที่โรงแรม Redwood Grove"

  • ถัดไป ฉันจะถามว่า "ฉันมีงบประมาณเหลือเท่าใดสำหรับ ADS"

  • และการเรียกใช้ฟังก์ชันจะแสดงผลเป็น 1,850 บาท

  • ตอนนี้ฉันต้องการเปลี่ยนแปลงบางอย่าง จึงจะขอให้ Gemini เพิ่มค่า Uber ไปสนามบินจำนวน $45 ลงในทริป ADS ของฉัน

  • Gemini จะเรียกใช้ฟังก์ชัน addExpense จากก่อนหน้านี้เพื่อเพิ่มข้อมูลนี้ลงในแผนการเดินทางของฉันโดยตรง

  • และเมื่อเปิด Jetpacker ค่าใช้จ่ายก็จะปรากฏขึ้น และงบประมาณจะลดลงตามนั้น

  • หากต้องการเริ่มสร้างแอปที่เชื่อมต่อสำหรับ Gemini วันนี้ โปรดสมัครรับสิทธิ์เข้าถึงแพลตฟอร์มแอปที่เชื่อมต่อ ซึ่งปัจจุบันอยู่ในเวอร์ชันตัวอย่าง

สรุปและอนาคตของ Agent บนอุปกรณ์เคลื่อนที่

  • โอเค

  • มาสรุปกันเลย

  • เรากำลังอยู่ในช่วงเริ่มต้นของการเปลี่ยนแปลงครั้งใหญ่ในด้านการประมวลผลบนอุปกรณ์เคลื่อนที่ และถึงเวลาแล้วที่คุณจะต้องตรวจสอบว่าแอปของคุณพร้อมแล้ว

  • ดังนั้น ให้เริ่มด้วยการระบุเส้นทางของผู้ใช้ที่มีคุณค่าสูงซึ่งเป็นแกนหลักของแอป

  • แสดงแบ็กเอนด์ของระบบคลาวด์โดยใช้ MCP ฝั่งเซิร์ฟเวอร์ และใช้ Android MCP เพื่อทำงานกับข้อมูลที่เข้ารหัสจากต้นทางถึงปลายทาง, การดำเนินการในเครื่อง, เซ็นเซอร์ของอุปกรณ์ และสิ่งอื่นๆ ในอุปกรณ์

  • หากต้องการสร้างต้นแบบตอนนี้ คุณสามารถดาวน์โหลด Android Studio และ Android CLI ได้

  • คุณสามารถเพิ่มทรัพยากร Dependency ของ AppFunctions เวอร์ชันเบต้าของ AndroidX ลงในไฟล์บิลด์ และทดสอบฟังก์ชันแรกในอุปกรณ์ในเครื่องได้

  • หากต้องการลงทะเบียนเพื่อเริ่มใช้งาน Gemini ในเวอร์ชันที่ใช้งานจริงก่อนใคร คุณสามารถลงชื่อสมัครใช้ผ่านคิวอาร์โค้ดนี้ได้เลย

  • และแน่นอนว่าอย่าลืมดูการพูดคุยที่เกี่ยวข้องเหล่านี้เกี่ยวกับการทำงานกับแพลตฟอร์มแอปที่เชื่อมต่อ รวมถึงวิธีใช้ AppFunctions ภายในกับ ML Kit หรือกับ Agent ของคุณเองเพื่อขับเคลื่อน Agent ในแอปของคุณเอง

  • และสุดท้ายนี้ ขอขอบคุณเป็นอย่างยิ่งที่สละเวลา

  • เราตื่นเต้นที่จะได้เห็นประสบการณ์ที่ยอดเยี่ยมทั้งหมดที่คุณกำลังสร้างขึ้นสำหรับ Android

รหัส

ข้อมูลโค้ด

ข้อมูลโค้ด Kotlin Script ที่ 11:56 ใน "ข้อมูลเจาะลึกทางเทคนิคเกี่ยวกับ MCP ของ Android"
implementation(libs.androidx.appfunctions)
ksp(libs.androidx.appfunctions.compiler)
ข้อมูลโค้ด Kotlin ที่ 12:14 ใน "ข้อมูลเจาะลึกทางเทคนิคเกี่ยวกับ MCP ของ Android"
@RequiresApi(36)
@AndroidEntryPoint
@AppFunctionServiceEntryPoint(
    serviceName = "JetPackerAppFunctionService",
    appFunctionXmlFileName = "jetpacker_app_function_service"
)
abstract class BaseJetPackerAppFunctionService : AppFunctionService() {
    @Inject internal lateinit var tripDao: TripDao
    @Inject internal lateinit var expenseDao: ExpenseDao
    ...
ข้อมูลโค้ด Kotlin ที่ 12:29 ใน "ข้อมูลเจาะลึกทางเทคนิคเกี่ยวกับ MCP ของ Android"
/** Represents an event in a trip itinerary. */
@AppFunctionSerializable(isDescribedByKDoc = true)
data class TimelineEventSerializable(
  /** The event's unique identifier. */
  val id: String,
  /** The trip ID this event belongs to. */
  val tripId: String,
  /** The type of event (e.g., TRANSPORTATION, ACCOMMODATION, FOOD_AND_DRINK). */
  val type: String,
  /** The timestamp of the event. */
  val timestamp: Long,
  /** The title of the event. */
  val title: String,
  /** The location of the event. */
  val location: String,
  /** A description of the event. */
  val description: String?,
  /** The title of the trip this event belongs to. */
  val tripTitle: String? = null,
  /** Optional title of a matched itinerary event when added as a reminder. */
  val matchedEventTitle: String? = null,
  /** Execution status of the tool call (e.g., SUCCESS). */
  val status: String? = "SUCCESS",
  /** Human-readable completion message instructing the agent not to repeat the call. */
  val message: String? = null,
)
ข้อมูลโค้ด Kotlin ที่นาที 12:38 ใน "ข้อมูลเจาะลึกทางเทคนิคเกี่ยวกับ MCP ของ Android"
@AppFunctionDeclaration(isDescribedByKDoc = true)
suspend fun getItinerary(tripId: String? = null): List<TimelineEventSerializable> {
  return withContext(Dispatchers.IO) {
    val trip = resolveTrip(tripId = tripId)
    val targetTripId = trip?.id ?: ActiveTripSession.activeTripId ?: tripId?.takeIf { it.isNotBlank() } ?: "2026-1"
    val dbEvents = eventDao.getEventsForTrip(targetTripId).first()
    val events =
      if (dbEvents.isNotEmpty()) { dbEvents } else {
        val fallback = DummyData.events.filter { it.tripId == targetTripId }
        if (fallback.isNotEmpty()) { eventDao.insertEvents(fallback) }
        fallback
      }
    events.map { it.toSerializable(tripTitle = trip?.title) }
  }
}
ข้อมูลโค้ด Kotlin ที่ 12:44 ใน "ข้อมูลเจาะลึกทางเทคนิคเกี่ยวกับ MCP ของ Android"
/**
 * Retrieves the itinerary events (with their eventId, title, type, and location) for a specific
 * trip. Note: getItinerary returns event summaries only; call getEventDetails(eventId) with a
 * specific event's ID to retrieve ticket prices, hotel nightly rates, baggage allowance, or
 * visitor tips.
 *
 * @param tripId Optional ID of the trip (e.g., '2026-1'). Defaults to the active trip if omitted.
 * @return A list of events in the itinerary.
 */
@AppFunctionDeclaration(isDescribedByKDoc = true)
suspend fun getItinerary(tripId: String? = null): List<TimelineEventSerializable> {
  return withContext(Dispatchers.IO) {
    val trip = resolveTrip(tripId = tripId)
    val targetTripId = trip?.id ?: ActiveTripSession.activeTripId ?: tripId?.takeIf { it.isNotBlank() } ?: "2026-1"
    val dbEvents = eventDao.getEventsForTrip(targetTripId).first()
...
ข้อมูลโค้ด Kotlin ที่ 12:52 ใน "ข้อมูลเจาะลึกทางเทคนิคเกี่ยวกับ MCP ของ Android"
/**
 * @param tripId Optional ID of the trip (e.g., '2026-1').
 *   Defaults to the active trip if omitted.
 * @return A list of events in the itinerary.
 */
@AppFunctionDeclaration(isDescribedByKDoc = true)
suspend fun getItinerary(tripId: String? = null): List<TimelineEventSerializable> {
  return withContext(Dispatchers.IO) {
    // resolveTrip defaults to the active trip if tripId == null
    val trip = resolveTrip(tripId = tripId)
ข้อมูลโค้ด Kotlin ที่นาที 13:30 ใน "ข้อมูลเจาะลึกทางเทคนิคเกี่ยวกับ MCP ของ Android"
@AppFunctionDeclaration(isDescribedByKDoc = true)
suspend fun getItinerary(tripId: String? = null): List<TimelineEventSerializable> {
  return withContext(Dispatchers.IO) {
    val trip = resolveTrip(tripId = tripId)
    val targetTripId = trip?.id ?: ActiveTripSession.activeTripId ?:
    tripId?.takeIf { it.isNotBlank() } ?: "2026-1"
    val dbEvents = eventDao.getEventsForTrip(targetTripId).first()
    val events =
      if (dbEvents.isNotEmpty()) { dbEvents } else {
        val fallback = DummyData.events.filter { it.tripId == targetTripId }
        ...
ข้อมูลโค้ด Kotlin ที่นาที 13:40 ใน "ข้อมูลเจาะลึกทางเทคนิคเกี่ยวกับ MCP ของ Android"
@AppFunctionDeclaration(isDescribedByKDoc = true)
suspend fun send(
    message: Message
): Result {
  ...
  throw AppFunctionException(
    errorCode = AppFunctionException.ErrorCode.ERROR_INVALID_ARGUMENT,
    errorMessage = "To address has invalid syntax!"
  )
}
ข้อมูลโค้ด XML ที่ 14:15 ใน "ข้อมูลเจาะลึกทางเทคนิคเกี่ยวกับ MCP ของ Android"
<AppFunctionAppMetadata
xmlns:appfn="http://schemas.android.com/apk/androidx.appfunctions"

  appfn:description="This app manages trips and travel expenses.

  Operational Patterns:
    - Use 'getAllTrips' to find the correct 'tripId' before
      adding expenses or managing trip details.

  Constraints:
    - Expenses must be associated with a valid trip ID."

  appfn:displayDescription="@string/user_visible_description" />
ข้อมูลโค้ด Kotlin ที่นาที 15:00 ใน "ข้อมูลเจาะลึกทางเทคนิคเกี่ยวกับ MCP ของ Android"
viewModelScope.launch {
    val newState =
        if (enabled) {
            AppFunctionManager.APP_FUNCTION_STATE_ENABLED
        } else {
            AppFunctionManager.APP_FUNCTION_STATE_DISABLED
        }
    appFunctionManager?.setAppFunctionEnabled(id, newState)
}
ข้อมูลโค้ด Kotlin ที่ 15:12 ใน "ข้อมูลเจาะลึกทางเทคนิคเกี่ยวกับ MCP ของ Android"
@AppFunctionSignature(
    scope = AppFunctionMetadata.SCOPE_ACTIVITY,
    appFunctionXmlFileName = "cart_functions"
)
fun interface AddCurrentItemToCart {
    /** Adds the item currently shown on screen to the cart. Returns the new cart size. */
    suspend fun addToCart(quantity: Int): Int
}
ข้อมูลโค้ด Kotlin ที่นาที 15:22 ใน "ข้อมูลเจาะลึกทางเทคนิคเกี่ยวกับ MCP ของ Android"
val addToCartAppFunctionAdapter =
    appFunctionManager.getHandleAppFunctionRequestAdapter<AddCurrentItemToCart>()

coroutineScope.launch {
    appFunctionManager.handleAppFunction(
        addToCartAppFunctionAdapter.adapt { quantity ->
            val newCartSize = cart.addItem(quantity)
            newCartSize
        }
    )
}
ข้อมูลโค้ด Kotlin ที่นาที 15:50 ใน "ข้อมูลเจาะลึกทางเทคนิคเกี่ยวกับ MCP ของ Android"
@AppFunctionDeclaration(isDescribedByKDoc = true)
@AppFunctionAccessLevel(
    level = AppFunctionMetadata.ACCESS_LEVEL_SELF,
    isCompatEnforcementEnabled = true,
)
suspend fun getItinerary(tripId: String? = null): List<TimelineEventSerializable> {
  return withContext(Dispatchers.IO) {
  ...
ข้อมูลโค้ด Shell ที่ 17:05 ใน "ข้อมูลเจาะลึกทางเทคนิคเกี่ยวกับ MCP ของ Android"
android skills add appfunctions
ข้อมูลโค้ด Kotlin ที่นาที 17:54 ใน "ข้อมูลเจาะลึกทางเทคนิคเกี่ยวกับ Android MCP"
/**
 * Adds an expense to a trip.
 *
 * @param tripId ID of the trip. If null, the active trip is used.
 * @param category Expense category. If null, it's inferred from the title.
 */
@AppFunctionDeclaration(isDescribedByKDoc = true)
suspend fun addExpense(
    title: String,
    amount: Double,
    currency: String,
    tripId: String? = null,
    category: String? = null,
): ExpenseSerializable = withContext(Dispatchers.IO) {
    if (amount <= 0.0) {
        throw AppFunctionInvalidArgumentException("amount must be greater than zero")
    }
    val trip = resolveTrip(tripId)
        ?: throw AppFunctionElementNotFoundException("No trip with ID $tripId")
    val expense = Expense(trip.id, title, amount, currency,
        category = category ?: inferExpenseCategory(title))
    expenseDao.insertExpense(expense)
    expense.toSerializable(remainingBudgetUsd(trip.id))
}