مهندس برمجيات
يمكنك الارتقاء بميزات تستخدم الذكاء الاصطناعي في Android إلى المستوى التالي من خلال إنشاء وكلاء مستقلين على الجهاز فقط. في هذه الجلسة، سنتعمّق في واجهات برمجة التطبيقات الجديدة المستندة إلى الوكلاء في حزمة تعلّم الآلة، والتي تم إنشاؤها مباشرةً فوق واجهات برمجة التطبيقات التي تتيح الوصول المباشر إلى النماذج والتي تعرفها مسبقًا.
باستخدام تطبيق Jetpacker المفتوح المصدر، ستتعلّم كيفية ربط الإجراءات المتعدّدة الخطوات في البيانات المحلية، وإدارة الجلسات المتعدّدة الأدوار ذات الحالة مع السياقات المعزولة، وربط Android MCP لتشغيل كل من المساعدين داخل التطبيق والواجهات على مستوى نظام Android باستخدام قاعدة رموز برمجية واحدة.
AgentRunner تنفيذ الأدوات ويوفّر طرقًا للجلسات لإجراء محادثات متعدّدة الأدوار بدون فقدان السياق.النص
اسمي "يولاندا فيرهوف".
أنا مهندس علاقات المطوّرين في Android، وتنضم إليّ اليوم "فيكتوريا ليستاري"، وهي من أعضاء فريق هندسة AICore.
مرحبًا،
شكرًا على انضمامكم إلينا هذا الصباح.
لذلك، عمل فريقنا بشكل وثيق لسنوات من أجل نقل نماذج الذكاء الاصطناعي المتطوّرة من السحابة الإلكترونية وتشغيلها مباشرةً على أجهزة Android.
وتندرج هذه الجهود ضمن حزمة تعلّم الآلة GenAI.
من ناحية، تتضمّن حزمة تعلّم الآلة GenAI واجهات برمجة تطبيقات عالية المستوى لحلول جاهزة، مثل تلخيص النصوص ووصف الصور والتعرّف على الكلام.
من ناحية أخرى، أتحنا أيضًا الوصول المباشر إلى نموذج Gemini Nano الذي يعمل على الجهاز فقط.
واليوم، سنتّخذ خطوة إضافية.
من خلال هذا الوصول المباشر، سنسمح لكم بإنشاء وكلاء داخل التطبيق، يمكنهم الاستدلال عبر جولات متعددة، وحفظ سياق المحادثة، واتخاذ إجراءات في تطبيقكم.
ماذا نعني بذلك يا "فيكتوريا"؟
أجل
حتى الآن، عندما نتحدث عن حالات الاستخدام على الجهاز فقط، فإننا نفكر في مهام أقصر وأبسط ومعزولة، مثل رصد عملية احتيال محتملة أو تلخيص مكالمة هاتفية.
وعندما نريد إنشاء شيء أكثر تعقيدًا، مثل طرح أسئلة حول كتالوج الصور أو طرح أسئلة على "خرائط Google"، نرجع بطبيعة الحال إلى النماذج السحابية الكبيرة.
لذا، توفّر النماذج المستندة إلى السحابة الإلكترونية ميزة المعالجة الهائلة والبيانات في الوقت الفعلي، ولكنّ الذكاء الاصطناعي على الجهاز يفتح آفاقًا جديدة لتقليل وقت الاستجابة ومعالجة البيانات في الوقت الفعلي والمحلي.
إذًا، لقد وسّع Gemini Nano نطاق ما يمكنك فعله على أجهزتك.
ومن الأمثلة على ذلك تطبيق "لقطات Pixel" الذي يتيح لك البحث في لقطات الشاشة على جهازك وطرح أسئلة حولها.
لذلك، عندما نشغّل الوكلاء على الجهاز فقط، نحتاج إلى قفزة كبيرة في إمكانات النموذج، ولهذا السبب نعمل على تطوير Gemini Nano 4 القادر على الاستدلال عبر الخطوات واتّخاذ إجراءات حقيقية.
ولكن هذه الإمكانية الجديدة لا تهم إلا إذا كان لدى المطور أجهزة حقيقية لاستهدافها.
الخبر السار هو أنّ Gemini Nano 4 متاح حاليًا على أجهزة رائدة محدّدة، ويمكنك أيضًا اختباره على العديد من الأجهزة الأخرى باستخدام معاينة المطوّر من AICore.
ونحن أيضًا على المسار الصحيح للوصول إلى 100 مليون جهاز في أوائل العام المقبل.
إذًا، ما معنى ذلك؟
هذا يعني أنّه يمكنك البدء في إنشاء هذه التجارب واختبارها اليوم، مع العلم أنّ ما تنشئه اليوم سيصل إلى جمهور كبير.
لقد ناقشنا Gemini Nano 4 والوصول المحتمل، والآن لنتحدث عن معنى الإمكانات بالذكاء الاصطناعي الوكيل.
وبالتالي، يمكن للذكاء الاصطناعي الوكيل التخطيط للمهام المعقّدة وتقسيمها إلى خطوات متعدّدة، ثم تنفيذ هذه الخطوات في تطبيقك.
لنفترض أنّ نموذج Gemini Nano على الجهاز يمكنه الاستعلام عن قاعدة البيانات المحلية، واسترداد البيانات من الخلفية، وتحليل الأسئلة المعقدة والردّ عليها، وتشغيل سير عمل التطبيق الأصلي على جهازك.
ولكن كيف ينتقل الذكاء الاصطناعي من وضع خطة إلى اتّخاذ إجراءات فعلية؟
وهذا هو موضوعنا اليوم: "استدعاء الدوال".
وهو المحرّك الأساسي للإمكانات بالذكاء الاصطناعي الوكيل.
فهو يترجم اللغة الطبيعية إلى تنفيذ على مستوى النظام، وينقل النموذج من الاستدلال السلبي إلى التنفيذ النشط من خلال اختيار واجهات برمجة التطبيقات المحلية اللازمة لحلّ المهام المعقّدة وتشغيلها.
ويسرّنا أيضًا الإعلان عن أنّ ميزة "استدعاء الدوال" متاحة تلقائيًا في Gemini Nano 4.
وهو متاح حاليًا في معاينة المطوّرين من AICore، وسيتم طرحه قريبًا على جميع الأجهزة المتوافقة مع Nano 4.
من الرائع أن تتوفّر هذه الإمكانات في النموذج، ولكنّها لا تمنح المطوّرين واجهات برمجة التطبيقات بعد.
لذلك، عملنا أيضًا على إضافة واجهات برمجة التطبيقات المناسبة إلى حزمة GenAI في "حزمة تعلّم الآلة" لضمان سهولة إنشاء الوكيل.
لذا، بدأنا بواجهات برمجة التطبيقات للوصول المباشر التي كانت لدينا، وأضفنا إليها أنواع الدوال وأنواع سجلّ المحادثات.
ومع هاتين الإضافتين الجديدتين إلى Prompt API في حزمة تعلّم الآلة، ستتمكّن الآن من إنشاء هذه الإمكانات بالذكاء الاصطناعي الوكيل المنخفضة المستوى في تطبيقك.
ومع ذلك، هناك الكثير من الإجراءات التي عليك اتّخاذها بنفسك في هذه المرحلة.
لذلك، عليك تنفيذ الدوال يدويًا، وحفظ سجلّ المكالمات، وتكرار العملية على مكالمات متعددة.
لذلك، إذا كانت هناك طلبات متعددة تحتاج إلى إجرائها، عليك إنشاء نوع من نظام التكرار بنفسك، وعليك إنشاء حواجز حماية خاصة بك للنظام.
هذا ممكن، ولكننا نريد بالطبع أن نجعل هذه العملية سريعة وسهلة.
لذلك، بالإضافة إلى مكتبة Prompt API، أضفنا أيضًا مكتبة جديدة لتدفقات الوكلاء.
يمكنك العثور عليه باستخدام com.mlkit.genai:agent-api، وهو متاح حاليًا في الإصدار الأولي.
والآن، من خلال واجهة برمجة التطبيقات الجديدة هذه، نساعدك في تنفيذ الكثير من المهام الصعبة.
وبدلاً من تنفيذ الكثير من الخطوات اليدوية، يمكنك إنشاء وكيل باستخدام بضعة أسطر من الرموز البرمجية فقط.
وهذا بالضبط ما سنفعله اليوم.
لذا، سنرى كيف تعمل واجهات برمجة التطبيقات هذه مع نموذج تطبيق Jetpacker.
لقد اطّلعت على Jetpack في مؤتمر Google I/O وخلال هذا المؤتمر.
يمكنكم مسح رمز الاستجابة السريعة هذا، وإذا كنتم مهتمين، يمكنكم إلقاء نظرة على الرمز البرمجي، وتوجيه الوكيل إليه، والتفاعل معه، ومعرفة المزيد حول كيفية تنفيذ كل هذه الميزات.
Jetpacker هو مساعد سفر حديث صمّمناه لعرض الكثير من الميزات الذكية المختلفة.
يستخدم المتصفّح حاليًا نماذج على الجهاز ونماذج سحابية لجميع أنواع واجهات برمجة التطبيقات المستندة إلى الطلبات، ولكن ما سنفعله الآن هو إضافة وكيل إليه.
بالنسبة إلى هذا الوكيل، Jetpacker، سنستخدم واجهة برمجة التطبيقات الجديدة لوكيل الذكاء الاصطناعي التوليدي في حزمة تعلّم الآلة التي تحدثنا عنها للتو، والتي ستستخدم نظام AI Core الأساسي الذي يعمل على أجهزة Android للتواصل مع نموذج Gemini Nano.
يمكنني التحدّث كثيرًا، وسنتحدّث كثيرًا، ولكن بالطبع نريد عرض هذه الميزة.
لذا، لننتقل إلى الهاتف ونعرض هذا كعرض توضيحي.
لديّ هاتف Pixel 11 Pro هنا.
وأعتقد أنّه يمكننا التبديل إلى Pro، ومن المفترض أن تتمكّن من رؤيته.
ها نحن ذا، ها هو ذا.
كما ترون، تطبيق Jetpacker قيد التشغيل هنا.
يعرض التطبيق مجموعة من رحلاتنا القادمة، ويمكنني الاطّلاع على رحلتنا الحالية، وفي رحلتنا الحالية، يمكنكم رؤية أنّ التاريخ هو 29 أكتوبر، لذا لدينا ورشة عمل حول الذكاء الاصطناعي على الجهاز فقط، وعرض توضيحي، ثم غدًا لديّ رحلة بحرية في خليج غولدن غيت، ولديّ زيارة قادمة إلى المتحف.
كل هذا رائع.
والآن، يمكننا الانتقال إلى شاشة النفقات حيث نتتبّع ميزانية هذه الرحلة.
وكما ترون في أسفل يسار الشاشة، هناك رمز الشرارة الصغير الجديد الذي يستند إلى الذكاء الاصطناعي الوكيل، وهو الوكيل الذي أنشأناه على الجهاز فقط.
لنبدأ بأمر بسيط جدًا.
أعتقد أنّه يمكننا أن نطلب منه إضافة نفقات معيّنة.
لنفترض أنّنا نريد إضافة نفقات لسيارة الأجرة إلى ماونتن فيو، وكانت 45 دولارًا.
لقد بدأنا
يتم الآن إعداد النفقات، وقد تمت إضافتها.
رائع.
حسنًا، نجح العرض التوضيحي 1.
لنرجع إلى الشرائح ونلقي نظرة على كيفية عمل ذلك في الواقع.
أولاً، كما لاحظتم، كنت أستخدم صوتي، وهذا الصوت يستخدم أيضًا نموذج Gemini Nano من خلال واجهة برمجة التطبيقات للتعرّف على الكلام لتحويل ما كنت أقوله إلى نص.
وبالتالي، يكون هذا النص رائعًا، ويمكننا إرساله إلى نموذج Gemini Nano.
ولكن ماذا سيفعل؟
مثلاً، أطلب منه إضافة شيء إلى النفقات، وأخبره بشيء عن 45 دولارًا، لكنه لا يعرف حقًا كيف يتصرف، أليس كذلك؟
في هذه المرحلة، سيقول لي فقط، "لا أعرف ما الذي تتحدث عنه".
لذا، ما نريد فعله هو إضافة القليل إلى هذا الطلب.
يمكننا إضافة أدوات إلى الطلب، مثل أدوات الحصول على ميزانية الرحلة وبرنامج الرحلة وما إلى ذلك. وإذا أرسلنا هذه الأدوات إلى نموذج Nano باستخدام واجهات برمجة التطبيقات، سيفهم تلقائيًا أنّه بدلاً من عرض نص، يمكنه الآن عرض استدعاء دالة.
في حالتنا، سيقول النموذج: "بدلاً من الرد بنص، سأضيف نفقات بقيمة 45 دولارًا أمريكيًا لسيارة الأجرة إلى ماونتن فيو".
هذا رائع، ولكن يا "فيكتوريا"، كيف يعمل ذلك في الواقع؟
كيف يمكننا إنشاء ذلك؟
أجل
إذًا، كيف نحدد الأدوات في الرمز البرمجي؟
لذا، هذا ما سأشرحه.
لذا، نستخدم في Android MCP مكتبة Jetpack AppFunctions لتحديد هذه الأدوات ونشرها.
لذا، يمكن لمكتبة AppFunctions عرض خدمات تطبيقك وبياناته وإجراءاته في سجلّ نظام التشغيل Android حتى نتمكّن من إعادة استخدام الوظائف نفسها للإعلان عن الأدوات ووصفها واستخدامها في تطبيقك.
ويتيح ذلك أيضًا للمستخدم إكمال هذه المهمة من خلال الوكلاء والتفاعلات على مستوى النظام.
لذا، سأقدّم لكم مثالاً ملموسًا في الرمز البرمجي.
/**
* Adds an expense to a trip.
*
* @param tripId Identifier of the trip (e.g., "paris-2026").
* @param amount Expense amount in dollars.
* @param description Short description of what the expense was for.
*/
@AppFunctionDeclaration(isDescribedByKDoc = true)
@AppFunctionAccessLevel(
level = ACCESS_LEVEL_SELF,
isCompatEnforcementEnabled = true,
)
suspend fun addExpense(tripId: String, amount: Double, description: String) {
// Save expense to local DB / repository...
}يمكنك أن ترى أنّ هذا مثال على AppFunctionDeclaration.
يمكنك أن ترى أن تعليق KDoc في الأعلى يصف ما تفعله الدالة ومعلماتها، كل ذلك بلغة طبيعية.
لذلك، من أفضل الممارسات تقديم وصف واضح قدر الإمكان لأنّنا سنرسله إلى النموذج.
ويمكنك أن ترى أدناه تعليقًا توضيحيًا، وهو @AppFunctionAccessLevel.
وبالتالي، يحدّد هذا نطاق ظهور الدالة.
في هذا المثال، لدينا ACCESS_LEVEL_SELF، ما يعني أنّ هذه الدوال التي نكتبها لا تظهر إلا لتطبيقك ووكلائه.
بعد ذلك، يمكننا تحديد دالة التعليق وتنفيذها، ويمكننا كتابة أكبر عدد ممكن من دوال التطبيق وتجميعها كلها في فئة خدمة واحدة.
حسنًا
لدينا الآن الأدوات ويمكننا كتابتها، وهذا أمر رائع.
ولكن لإنشاء وكيل، نحتاج إلى أكثر من مجرد واجهات AppFunctions.
لدينا ثلاثة أنواع أخرى من الأجزاء التي من المهم معرفتها عند كتابة وكيل.
أولاً، نحدّد الوكيل.
لذا، نحدّد تعليمات النظام، ونحدّد القيود والإعدادات، وكيفية العمل معها.
ولدينا أيضًا AgentRunner.
AgentRunner هو الذي ينفّذ الوكيل نيابةً عنا.
إذًا، هذا يعالج آلية التكرار التي كنا نتحدث عنها.
وأخيرًا، لدينا AgentEvents، وهي طريقة تتيح للنظام، من خلال واجهة برمجة التطبيقات، إعلامنا بما يفعله في الوقت الحالي.
وبالتالي، سيخبرنا عندما يعرض نصًا، وعندما يستدعي أداة، وما إلى ذلك، حتى نتمكن من عرض ذلك في واجهة المستخدم.
حسنًا
لِنلقِ نظرة على الرمز الفعلي.
حسنًا
قبل أن نبدأ في كتابة الرمز، علينا أولاً التحقّق مما إذا كانت ميزة "استدعاء الدوال" متاحة على جهازك، لأنّها لا تتوفّر إلا على Gemini Nano 4.
val model = Generation.getClient()
val features = model.getAvailableFeatures()
if(FUNCTION_CALLING in features) {
// Device is supported!
} else {
// Graceful fallback
}أولاً، علينا التحقّق من ذلك من خلال استدعاء النموذج التوليدي getAvailableFeatures.
ويمكننا ببساطة التحقّق مما إذا كانت ميزة "استدعاء الدوال" مدرَجة في الميزات.
وإذا لم يكن الأمر كذلك، فيمكننا تنفيذ إجراء احتياطي سلس.
حسنًا
والآن، لننتقل إلى الرمز البرمجي.
if(FUNCTION_CALLING in features) {
// 1. Build the agent configuration
val agent = Agent.builder()
.setName("Assistant")
.setInstructions("You are a helpful on-device assistant.")
.enableAppFunctions()
.build()
// 2. Initialize the runner
val runner = AgentRunner(agent = agent)
// 3. Create your prompt
val prompt = content { text(userPrompt)}
// 4. Start the agent
runner.runSingleTurnAsync(prompt).textTokens().collect { responseMessage += it }
}نعلم أنّ ميزة "استدعاء الدوال" متاحة على جهازنا، ويمكننا ببساطة البدء في إنشاء الوكيل باستخدام Agent.builder.
وبعد ذلك يمكننا تحديد اسمها.
يمكننا setInstructions لتحديد السياق والشخصية.
وأخيرًا، نستدعي enableAppFunctions.
لذا، سيؤدي استدعاء هذه الدالة إلى تكرار البحث واكتشاف جميع دوال AppFunctions التي أدرجناها سابقًا تلقائيًا، وقد أنشأنا الوكيل للتو.
بعد ذلك، علينا تهيئة AgentRunner.
لذا، سيتولى مشغّل الوكيل هذا إدارة حلقة التنفيذ نيابةً عنا.
نستدعيها ببساطة عن طريق استدعاء دالة إنشائية وتمرير الوكيل إليها.
وأخيرًا، يمكننا تضمين الطلب الصوتي الذي تم تحويله إلى نص في مجموعة محتوى لإنشاء الطلب.
ثم نستدعي المشغّل.
نستدعي هذه الدالة runSingleTurnAsync في الطلب، وسيؤدي ذلك إلى بدء الوكيل وتشغيل الاستدلال وتنفيذ عملية استدلال الذكاء الاصطناعي بالكامل.
وإذا أردنا فقط بثّ الردّ النصي النهائي للنموذج في واجهة المستخدم، ما علينا سوى استدعاء textTokens، وسيجمع الردّ فور وصوله.
هذا كل ما في الأمر.
هذه بالفعل بضعة أسطر من الرموز البرمجية، لذا لم نكن نمزح عندما قلنا ذلك.
باستخدام هذه الأسطر القليلة من الرموز، يمكنكم إنشاء وكيل.
سينفّذ هذا الوكيل ما تريده.
وسوف يعطينا ردًا نصيًا.
في العرض التوضيحي الأول، كنا نستخدم أمرًا بسيطًا جدًا، أليس كذلك؟
على سبيل المثال، أعطيتُه أمرًا واحدًا ونفّذ إجراءً واحدًا فقط، لذا فهو ليس وكيلاً كاملاً بعد، أليس كذلك؟
لذا، دعونا نضفي بعض الإثارة على الأمور.
في الجزء التالي، أريد أن أعطي أمرًا، ولكن أريد أن أجعله أكثر صعوبة على الوكيل.
لذا، لنعد إلى جهازي التجريبي، ولنرى ما إذا كان بإمكاننا جعله أكثر صعوبة قليلاً.
لقد وصلنا إلى وجهتنا.
رائع.
كما قلت، لدينا زيارة إلى المتحف غدًا.
وبالنسبة إلى زيارة المتحف، يمكنكم الاطّلاع على سعر الدخول، وهو 25 دولار أمريكي.
لنرَ الآن ما إذا كان الوكيل قادرًا على معرفة ذلك بنفسه.
لذا، سأعود إلى شاشة النفقات وسأعطيها أمرًا، وسأقول: "أريد إضافة تذكرة المتحف إلى نفقاتي".
لقد بدأنا
والآن، كما ترون، يتحقّق من برنامج رحلتي، ويعثر على زيارة المتحف، ويضيفها إلى نفقاتي.
إذًا، اتّخذ النموذج ثلاث خطوات لتحقيق هدفه، وقدّم لي أيضًا ردًا نصيًا نهائيًا.
والآن، يتم تنفيذ كل ذلك تلقائيًا.
لقد رأيت السرعة الحقيقية لهذا النموذج على الجهاز.
لنعد الآن إلى الشرائح ونلقي نظرة على كيفية عمل ذلك.
كما ترون، لقد غيّرنا بعض الأمور مقارنةً بالعرض التوضيحي الأول.
في هذه الحالة، كما ترون، لم أذكر المتحف أو النفقات، لذا عندما تلقّى النموذج الطلب والأدوات، حاول تحديد ما يجب فعله أولاً.
لذا، يقرّر هنا أنّه بحاجة إلى معرفة المزيد عن برنامج رحلتي الحالي، فيستدعي getItinerary.
يتلقّى ردًا ثم يعرف أنّ هناك زيارة لمتحف في برنامج الرحلة هذا، ومن المحتمل أن يكون هذا هو المتحف الذي كانت تتحدث عنه.
لذا، لنبحث عن تفاصيل هذا الحدث المحدّد.
ومن بين جميع البيانات الوصفية الأخرى لهذا الحدث المحدّد، سيظهر أنّ القيمة هي 25 دولار أمريكي.
يفهم ذلك، ويعود، ويفهم أنّه يحتاج إلى استدعاء إضافة نفقات.
لذا، نرى هنا خطوات متعددة تحدث واحدة تلو الأخرى.
لذا، فيكتوريا، هل يمكنك التعمق أكثر في ذلك؟
أجل
ما وصفته "يولاندا" للتو هو مثال ملموس على العرض التوضيحي الذي قدّمته، ولكن سأتناول هنا الموضوع من منظور أكثر تجريدًا ومفاهيمية.
سيحدث ذلك في كل مرة نستخدم فيها ميزة "استدعاء الدوال" المتعدّدة الخطوات.
أولاً، هذا هو تطبيقنا، وهذا هو نموذج Gemini Nano.
وسيبدأ المستخدم بإرسال الطلب وقائمة الأدوات المتاحة إلى النموذج.
بعد ذلك، سيستخدم النموذج هذه المعلومات لتحديد ما يجب إرساله.
لنفترض أنّه في الحالة الأولى، يرسل النموذج طلبًا لاستخدام أداة، لنسمّيه "طلب الأداة 1".
بعد ذلك، سينفّذ التطبيق طلب الأداة هذا، وسيحصل على ردّ من الأداة.
بعد ذلك، ما سيتم إرساله إلى النموذج هو في الواقع سجلّ المحادثة الكامل، والذي يتضمّن الطلب الأصلي وقائمة الأدوات وطلب الأداة الأول واستجابة الأداة الأولى، إلى النموذج.
بعد ذلك، يستخدم النموذج هذه المعلومات مرة أخرى لتحديد الخطوة التالية.
لذا، يمكن أن يستمر في العودة ذهابًا وإيابًا حتى نحصل على النتيجة النهائية.
لنفترض أنّ النموذج سيعرض طلب الأداة رقم n، ثم يعرض التطبيق سجلّ المحادثة بالكامل مرة أخرى.
وعندما يرى النموذج أنّ الطلب قد تم تنفيذه، سيرسل ردًا نهائيًا إلى التطبيق، وبعد ذلك يمكن للتطبيق أن يعرض في واجهة المستخدم أنّه تم تنفيذ الطلب.
حسنًا
سأشرح الآن كيفية تنفيذ ذلك في الرمز البرمجي.
/**
* Retrieves the itinerary events (ID, title, location) for a trip.
* Note: Call getEventDetails(eventId) to retrieve ticket prices or notes.
*/
@AppFunctionDeclaration(isDescribedByKDoc = true)
@AppFunctionAccessLevel(level = ACCESS_LEVEL_SELF, isCompatEnforcementEnabled = true)
suspend fun getItinerary(tripId: String): List<EventSummary> { ... }
/**
* Retrieves details for a specific event, including admission ticket prices.
*/
@AppFunctionDeclaration(isDescribedByKDoc = true)
@AppFunctionAccessLevel(level = ACCESS_LEVEL_SELF, isCompatEnforcementEnabled = true)
suspend fun getEventDetails(eventId: String): EventDetails { ... }لنتذكّر المثال من رمز الخطوة الواحدة، وهذا هو رمز الخطوات المتعددة، لذا يبدو مشابهًا جدًا، وأهم نقطة هي كتابة KDoc بوضوح شديد.
لذا، في هذا المثال، تشير الدالة getItinerary بوضوح إلى أنّها تعرض فقط ملخّصًا عامًا للأحداث.
لذا، نذكر أيضًا أننا بحاجة إلى استدعاء getEventDetails باستخدام eventId إذا أردنا الحصول على تفاصيل رحلة أكثر تحديدًا.
بعد ذلك، يتضمّن getEventDetails أيضًا تعليق KDoc، يشير إلى أنّه يمكنه استرداد التفاصيل، مثل أسعار التذاكر والملاحظات الأخرى.
لذا، من خلال استخدام التعليق ووصفه بوضوح، يمكننا توجيه النموذج لمعرفة كيفية تفاعل الأدوات مع بعضها البعض.
حسنًا
هذه هي الطريقة التي ننشئ بها الوكيل لتنفيذ هذه الحلقة، أي حلقة الوكيل.
val agent = Agent.builder()
.setName("Assistant")
.setInstructions("You are a helpful travel assistant. Active tripId: $tripId")
.enableAppFunctions()
.setMaxTurns(5) // Guardrail against infinite tool loops
.build()
val runner = AgentRunner(agent = agent)
// Automatically loops through all 3 tool calls in a single user turn!
runner.runSingleTurnAsync(prompt).collect { event ->
when (event) {
is AgentEvent.FunctionCall -> showToolRunning(event.name)
is AgentEvent.FunctionResponse -> showToolCompleted(event.name)
is AgentEvent.TextMessage -> responseMessage += event.text
}
}يبدو مشابهًا جدًا لمثال الخطوة الواحدة، مع إضافتين فقط هنا.
أولاً، علينا ضبط الحد الأقصى لعدد الجولات، وفي هذا المثال، سنضبطه على خمسة.
إذًا، ما هي وظيفته؟
وبالتالي، يمنع ذلك النموذج من التكرار بشكل لا نهائي.
لنفترض أنّ الطلب غامض أو أنّ النموذج يستمر في استدعاء أداة تلو الأخرى ثم يعود إلى الأداة الأولى، فإذا ضبطنا هذا الخيار، سيتوقف النموذج عن التكرار.
بعد ذلك، نستدعي الدالة نفسها لتشغيلها، runSingleTurnAsync.
لذلك، على الرغم من أنّها تُسمّى "جولة واحدة"، فإنّها في الواقع تعني طلبًا واحدًا من المستخدم.
لذا، يتوقع المستخدم النهائي أن يرسل طلب بحث واحدًا، وأن يتلقّى إجابة واحدة، بدون الاهتمام كثيرًا بالتفاصيل مثل ما حدث في الخلفية، وعدد مرات التكرار، وما إلى ذلك.
لذا، يمكننا الاستمرار في استخدام الدالة نفسها.
وبما أنّ استدعاء الأدوات المتعدّدة الخطوات يستغرق وقتًا أطول، رأيتم أيضًا في العرض التوضيحي أنّ الأداة تعرض الوظيفة التي تستدعيها حاليًا، وما يتم تنفيذه حاليًا، وما إذا تم إنجازه.
لذلك، نريد عرض مستوى التقدّم المباشر في واجهة المستخدم.
لذلك، بدلاً من جمع الردّ النصي فقط، يمكننا عرض اسم الأداة، event.name، عندما يُصدر AgentEvent FunctionCall أو FunctionResponse، وعند الانتهاء، يمكننا عرض TextMessage كما في السابق.
رائع!
لذلك، نطلق على هذه العملية اسم "تشغيل غير متزامن"، وهو ما يفترض بالطبع أنّ هناك نوعًا آخر من واجهة برمجة التطبيقات، أليس كذلك؟
أجل
لذا، بالطبع لا يكفي تنفيذ خطوة واحدة فقط.
نريد تنفيذ عملية متعددة الخطوات.
لنتناول الآن الجلسات المتعددة الأدوار، وأعتقد أنّه يمكننا العودة إلى العرض التوضيحي وجعله أكثر صعوبة.
كنا نتحدث عن كيفية زيارة المتحف، ولكن كان لدينا أيضًا رحلة بحرية في خليج غولدن غيت.
لذا، أريد فقط معرفة ما إذا كانت لديّ ميزانية كافية قبل حجزها فعليًا.
لِنطرح سؤالاً.
لنرى.
لذلك أفكّر في اصطحاب فريقي في رحلة بحرية في خليج غولدن غيت غدًا، واعتقدت أنّ تكلفتها تبلغ حوالي 180 دولار أمريكي.
إذا فعلت ذلك، فماذا ستكون ميزانيتي المتبقية في الواقع؟
إذًا، ما الذي يفعله؟
إنها تجيب عما ستكون عليه ميزانيتي المتبقية.
وبالتالي، يفهم برنامج الرحلة الحالي.
فهو يمتلك هذه المعلومات ويمكنه إخبارك بها.
ولكن يمكنني أيضًا طرح سؤال متابعة هنا.
حسنًا، هذا جيد.
هل يمكنك إضافته إلى نفقاتي؟
ها نحن ذا، وسيتم المتابعة.
يجب أن تكون العملة، رائع، تأثير تجريبي.
حسنًا، إذًا هو بالدولار.
$180.
لقد بدأنا
هذه هي المرة الأولى التي يحدث فيها ذلك، وهذا رائع.
حسنًا، ما الذي يفعله؟
يخبرني أن الأمر استغرق ثلاث محاولات وأضاف إلى النفقات وما يفعله أيضًا هو أنه يسمح لي بتحديث هذه الجلسة.
لديّ زر إعادة تحميل صغير في أعلى الصفحة، وإذا نقرت عليه، يتم محو السجلّ بالكامل ويمكنني البدء من جديد.
لذا، أعتقد أنه يمكننا العودة إلى الشرائح.
لدينا العروض التوضيحية العاملة، وكلها تفعل ما نريد، ولذا أعتقد أن ما يمكننا فعله هو العودة إلى الرمز.
أجل
هذا رائع جدًا.
لذا تلاحظ أن جولاندا لم تذكر، إنها تقول فقط، أوه، إنها بالدولار، ولا تقول إنها لتذكرة جولدن جيت باي كروز.
فكيف يعرف النموذج كل هذا السياق؟
هذا يختلف قليلاً عمّا عرضته في مقتطفَي الرمز السابقَين.
// 1. Get or create an isolated session for the current trip
val session = runner.getOrCreateSession(tripId)
// 2. Run a turn (history is preserved automatically across turns)
session.runAsync(prompt).collect { event ->
when (event) {
is AgentEvent.FunctionCall -> showToolRunning(event)
is AgentEvent.FunctionResponse -> showToolCompleted(event)
is AgentEvent.TextMessage -> responseMessage += event.text
}
}
// 3. Clear history when the user taps "New conversation"
session.delete()لذا، بدلاً من استخدام AgentRunner وrunSingleTurnAsync، علينا إنشاء جلسة.
لذلك، نستدعي هنا runner.getOrCreateSession مع tripId.
إذًا، لماذا نحتاج إلى إنشاء جلسة؟
لأنّ runSingleTurnAsync سيعيد ضبط الذاكرة، وبالتالي لن يكون لديه سياق.
لن نتمكّن من إجراء محادثة متعددة الجولات.
لذلك، عندما تريد إجراء محادثة متعدّدة الجولات، نستخدم الجلسة، أي جلسة الوكيل.
بعد ذلك، نرسل tripId.
لماذا نحتاج إلى ذلك؟
لأنك رأيته من قبل، تطبيق Jetpacker، فإنه يدير رحلات متعددة.
هناك رحلة إلى ADS، ورحلة إلى باريس، وربما رحلة إلى طوكيو.
لذلك نريد الاحتفاظ بالسياق ضمن الرحلة.
لا نريد أن نُفسد علاقتنا ببعضنا، أليس كذلك؟
وبدلاً من استدعاء runSingleTurnAsync، لدينا runAsync.
هذا مختلف.
يتم تنفيذ ذلك في الجلسة، ثم يظهر الطلب نفسه.
ويمكننا بعد ذلك إصدار AgentEvent وFunctionCall وFunctionResponse وTextMessage كما في السابق، فالأمر نفسه.
وعندما نقرت "يولاندا" على زر المحادثة الجديدة، أي زر إعادة التحميل، يمكننا فعليًا الحصول على session.delete.
أجل
حسنًا
لقد نفّذنا كل ذلك باستخدام واجهات برمجة التطبيقات الجديدة المستندة إلى الوكلاء، وهذا أمر رائع وسيعمل بشكل مذهل في العديد من حالات الاستخدام.
بالطبع، نريد دائمًا تسهيل المهام السريعة أو إتاحة المهام السهلة، ولكننا نريد أيضًا إتاحة الحالات الأكثر تعقيدًا لك.
لذلك، نريد التأكّد من أنّه إذا لزم الأمر، يمكنك أيضًا استخدام واجهات برمجة التطبيقات التي تتيح الوصول المباشر لإنشاء نسخة منخفضة المستوى من هذه العناصر.
لذا، ربما يمكنك يا "فيكتوريا" شرح معنى ذلك ومتى سنعود إلى مستوى الوصول المباشر هذا مرة أخرى؟
نعم، بالتأكيد يا "جولاندا".
قد تكونون على دراية بـ Prompt API التي تتيح الوصول المباشر إلى Gemini Nano.
لذلك، أتحنا أيضًا ميزة استدعاء الدوال هنا، ولدينا حالتا استخدام نفضل فيهما استخدام واجهة برمجة التطبيقات هذه بدلاً من سير العمل الأسهل المستند إلى الوكلاء.
لذا فإن الحالة الأولى هي حالة استخدام بسيطة، حالة استخدام بسيطة للغاية.
في سير العمل المستند إلى الوكلاء، نرسل المحادثة الكاملة والطلب والأدوات وطلب الأداة واستجابة الأداة وكل شيء إلى النموذج.
وقد تكون هناك بعض حالات الاستخدام التي لا نريد فيها تنفيذ ذلك، بل نريد فقط إرسال الطلب، على سبيل المثال.
لنفترض أنّنا نريد إرسال طلب إلى النموذج، فيردّ باستدعاء أداة، ثم لا نريد عرض استدعاء الأداة، بل نريد فقط إرسال طلب آخر إلى النموذج، لكي نتمكّن من تعديل السجلّ هناك، كما لو كان لدينا تحكّم كامل في السجلّ.
سأقدّم لك مثالاً أكثر وضوحًا هنا.
// Turn 1: Execute function directly without a second model call
val turn1 = content(Content.Role.USER) {
text("Change the background to a sunset")
}
val response1 = generativeModel.generateContent(
generateContentRequest(turn1) { setTools(tools) }
)
executeFunctionCalls(response1)
// Turn 2: Send only user prompts to save context tokens
val customHistory = listOf(
turn1,
content(Content.Role.USER) { text("Oh no, I meant a beach") }
)
val response2 = generativeModel.generateContent(
GenerateContentRequest.Builder(customHistory).setTools(tools).build()
)لنفترض أنّ لدينا تطبيقًا يمكنه تغيير مظهر خلفية جهازك.
لنفترض أنّ المستخدم أرسل طلب البحث التالي: "تغيير الخلفية إلى غروب الشمس".
وبعد ذلك، نفّذ تطبيقنا هذه الوظيفة، وتم تغيير الخلفية إلى غروب الشمس.
وبعد ذلك، غيّر المستخدم رأيه وقال: "لا، كنت أقصد شاطئًا".
ما يحدث هنا هو أنّنا لا نحتاج إلى إرسال الحالة الكاملة التي غيّر فيها الجهاز الخلفية إلى غروب الشمس، ثم يريد الآن تغييرها إلى شاطئ.
لا، لسنا بحاجة إلى ذلك.
نحتاج فقط إلى إرسال الطلب الأول والطلب الثاني، لأن الطلب الأول يحتوي على معلومات الخلفية.
لذا يعرف أنّه يريد تغيير الخلفية إلى شاطئ.
لذا يمكننا استخدام Prompt API لإجراء ذلك.
لدينا تحكّم كامل في السجلّ، ونحفظ الرموز المميّزة السياقية على الجهاز فقط، لأنّ الجهاز يحتوي على نافذة رموز مميّزة محدودة أكثر، وبهذه الطريقة يمكننا حفظ المزيد من الرموز المميّزة.
أما حالة الاستخدام الثانية، فهي في الواقع النقيض تمامًا.
لنفترض أنّ لدينا حالات استخدام معقّدة لا يمكن لواجهة برمجة التطبيقات المستندة إلى الوكلاء التعامل معها بعد، مثل أنظمة الوكلاء المتعددين التي تتفاعل فيها عدة وكلاء فرعيين مع بعضها لحلّ مهمة معقّدة، أو نظام هجين يجمع بين النماذج على الجهاز فقط والنماذج السحابية، أو إذا أردنا تخصيص الذاكرة وضغط السياق.
يتعلّق ذلك بالسجلّ، مثل التحكّم الكامل في السجلّ الذي ذكرته سابقًا.
لنفترض أنّنا نريد، بسبب محدودية نافذة السياق، عدم إرسال كل شيء إلى النموذج، بل نريد فقط تلخيص الجزء الأكثر أهمية، أو ربما الجزء الأخير، ليكون لدينا تحكّم كامل في ذلك، ثم ننتقل إلى استخدام واجهة برمجة التطبيقات للوصول المباشر.
وفي حالات الاستخدام هذه، يمكننا استخدام أُطر عمل مثل Agent Development Kit أو ADK أو Crew، أو يمكنك أيضًا إنشاء إطار عمل مخصّص للتنسيق.
حسنًا
هذا كل ما لدينا عن واجهة برمجة التطبيقات المستندة إلى الوكلاء.
ومع ذلك، هناك شيء آخر أردنا التطرق إليه.
لقد تحدّثنا عن الوكلاء داخل التطبيق طوال الوقت، لذا أنتم تبنون تجربة وكيل داخل تطبيقكم، ولكننا تحدّثنا أيضًا عن كيفية استخدامنا لـ AppFunctions، ولماذا تستخدمون AppFunctions إذا كان الشيء الوحيد الذي تفعلونه هو داخل تطبيقكم؟
نعلم أنّ المطوّرين لا يحبّون تكرار المهام، أليس كذلك؟
لا نحب نسخ الرموز ولصقها، ولا نحب تكرار أي منطق عمل.
لذا، باستخدام AppFunctions، بدلاً من السماح لوكيلنا داخل التطبيق فقط باستخدامها، يمكننا من خلال تغيير بسيط جدًا إتاحة أدواتنا أيضًا لوكلاء النظام.
فكيف يبدو ذلك مع الرمز إذًا؟
أجل
لذا، من خلال AppFunctions، نحتاج فقط إلى كتابة الرمز مرة واحدة، ثم يمكننا عرضه على مستويات مختلفة من الطبقات كما نريد.
تذكَّروا أنّه قبل استخدام ACCESS_LEVEL_SELF، كانت الدوال التي كتبناها تظهر فقط لوكيلنا داخل التطبيق، ولكن يمكننا أيضًا تغيير مستوى الوصول هذا إلى ACCESS_LEVEL_ANDROID_TRUSTED.
سيؤدي ذلك إلى عرض الدوال للوكلاء الموثوق بهم في نظام التشغيل Android.
لذلك لا حاجة لإعادة كتابة الدوال أو الأوامر أو منطق قاعدة البيانات.
تعمل AppFunctions كصوت لتطبيقك داخليًا وخارجيًا.
نريد عرضًا توضيحيًا لذلك أيضًا، أليس كذلك؟
حسنًا، لنعد إلى الهاتف، وللعرض التوضيحي النهائي، دعنا نخرج فعليًا من تطبيق Jetpacker.
لذلك يمكننا الخروج، ويمكننا الانتقال إلى Gemini.
لذا، في Gemini، يمكننا استدعاء @Jetpacker.
هذه الميزة متاحة حاليًا في إصدار تجريبي محدود، لذا لن تكون متاحة للجميع اليوم، ولكن يمكنكم اختبارها وتجربتها.
لنرَ ما يمكننا فعله باستخدام Jetpacker.
ما الإجراءات التي علينا اتّخاذها؟
شربتُ قهوة هذا الصباح، أليس كذلك؟
هل يمكنك إضافة قهوة بقيمة 7 دولارات إلى نفقات رحلة ADS، من فضلك؟
لقد بدأنا
ويمكنك ملاحظة أنّه يتصل بتطبيق Jetpacker.
وهذا كل ما في الأمر.
أضافت النفقات لي.
لقد رأينا كيف يمكننا استخدام كل شيء في Jetpacker.
الرائع في Jetpacker هو أنّه يمكننا تفعيل "وضع الطيران" أيضًا.
لم نعرض ذلك اليوم، ولكن يمكننا تفعيل وضع الطيران، وكل ما رأيتموه في العروض التوضيحية، وأريد عرضه لأنّني أريد التأكّد من أنّكم تثقون بي جميعًا، لذا لنوقف شبكة Wi-Fi.
هل لدى أي شخص مثال تجريبي رائع، مثال على نفقات يمكنني إضافتها؟
لنرى.
ربما يمكننا القيام بما هو؟
سيكون استئجار السيارة أمرًا رائعًا، أليس كذلك؟
حسنًا
لنجرّب مثلاً: "أريد إضافة نفقات بقيمة 300 دولار أمريكي لاستئجار السيارة".
ها نحن ذا، وقد حصلنا على ما نريد.
حسنًا
رائع.
حسنًا، لنعد إلى الشرائح.
كنا نتحدث عن AppFunctions.
أعلم أننا تطرقنا إليها بشكل خفيف جدًا.
إذا أردت معرفة المزيد، يمكنك مشاهدة محاضرة أخرى من "دان" و"سارة"، حيث يقدّمان تفاصيل أكثر حول كيفية تفعيل هذه الميزة واختبارها، والأدوات المتاحة، وما إلى ذلك.
لقد تناولنا اليوم الكثير من المواضيع المختلفة.
لقد رأينا الخطوة الواحدة، والخطوات المتعددة، والجولات المتعددة، وأخيرًا رأينا أيضًا كيف يمكنك استخدام System AppFunction.
والآن، أعتقد أنّ لدينا بعض النصائح قبل أن نغادر.
أجل
لذا، تذكَّر هذه النصائح الثلاث عند البدء في إنشاء هذه التجارب المستندة إلى الوكلاء.
لذا فإنّ أولها هو الحفاظ على تركيز مجموعات الأدوات.
لذا، تذكَّر أنّ النماذج على الجهاز فقط لها إمكانات محدودة بسبب حجمها الأصغر.
لذا، بدلاً من تسجيل عشرات الأدوات، من الأفضل استخدام عدد أقل من الأدوات.
وكما ذكرتُ سابقًا، من المهم كتابة أوصاف واضحة في KDoc لأنّ النموذج يعتمد عليها لتحديد ما سيتم عرضه.
وبعد ذلك، يمكننا أيضًا ضبط حدّ أقصى لعدد الجولات.
يمنع ذلك أيضًا النموذج من التكرار بشكل لا نهائي إذا حدث التباس.
وهذا يعني أنّنا قد نستنزف بطارية المستخدم.
لذا، إذا كان لدينا هذا، فيمكننا توفير بطارية المستخدم.
وأخيرًا، علينا التحقّق من توفّر ميزة "استدعاء الدوال" على الجهاز، وتنفيذ إجراء احتياطي سلس.
على سبيل المثال، إذا لم تكن متوافقة، يمكنك الرجوع إلى النماذج السحابية أو واجهة المستخدم العادية.
حسنًا، أعتقد أنّه يمكننا إنهاء هذه المحادثة من خلال مراجعنا.
لذا، سأعرض لك رموز الاستجابة السريعة مرة أخرى.
يُرجى إلقاء نظرة.
أعتقد أنّ من الأمور التي يجب وضعها في الاعتبار أنّ لدينا أيضًا مهارات Android لهذا الغرض.
لذلك، لست بحاجة إلى التعمّق في كل التفاصيل الدقيقة.
لدينا مستندات شاملة ومهارة Android تتوافق مع واجهة برمجة التطبيقات الجديدة هذه المستندة إلى الذكاء الاصطناعي الوكيل.
لذا ألقِ نظرة على ذلك.
ننصحك بتنزيل "معاينة المطوّر لخدمة AICore" لتتمكّن من اختبار كل هذه الميزات، ونتمنى لك تجربة ترميز ممتعة.
/**
* Adds an expense to a trip.
*
* @param tripId Identifier of the trip (e.g., "paris-2026").
* @param amount Expense amount in dollars.
* @param description Short description of what the expense was for.
*/
@AppFunctionDeclaration(isDescribedByKDoc = true)
@AppFunctionAccessLevel(
level = ACCESS_LEVEL_SELF,
isCompatEnforcementEnabled = true,
)
suspend fun addExpense(tripId: String, amount: Double, description: String) {
// Save expense to local DB / repository...
}val model = Generation.getClient()
val features = model.getAvailableFeatures()
if(FUNCTION_CALLING in features) {
// Device is supported!
} else {
// Graceful fallback
}if(FUNCTION_CALLING in features) {
// 1. Build the agent configuration
val agent = Agent.builder()
.setName("Assistant")
.setInstructions("You are a helpful on-device assistant.")
.enableAppFunctions()
.build()
// 2. Initialize the runner
val runner = AgentRunner(agent = agent)
// 3. Create your prompt
val prompt = content { text(userPrompt)}
// 4. Start the agent
runner.runSingleTurnAsync(prompt).textTokens().collect { responseMessage += it }
}/**
* Retrieves the itinerary events (ID, title, location) for a trip.
* Note: Call getEventDetails(eventId) to retrieve ticket prices or notes.
*/
@AppFunctionDeclaration(isDescribedByKDoc = true)
@AppFunctionAccessLevel(level = ACCESS_LEVEL_SELF, isCompatEnforcementEnabled = true)
suspend fun getItinerary(tripId: String): List<EventSummary> { ... }
/**
* Retrieves details for a specific event, including admission ticket prices.
*/
@AppFunctionDeclaration(isDescribedByKDoc = true)
@AppFunctionAccessLevel(level = ACCESS_LEVEL_SELF, isCompatEnforcementEnabled = true)
suspend fun getEventDetails(eventId: String): EventDetails { ... }val agent = Agent.builder()
.setName("Assistant")
.setInstructions("You are a helpful travel assistant. Active tripId: $tripId")
.enableAppFunctions()
.setMaxTurns(5) // Guardrail against infinite tool loops
.build()
val runner = AgentRunner(agent = agent)
// Automatically loops through all 3 tool calls in a single user turn!
runner.runSingleTurnAsync(prompt).collect { event ->
when (event) {
is AgentEvent.FunctionCall -> showToolRunning(event.name)
is AgentEvent.FunctionResponse -> showToolCompleted(event.name)
is AgentEvent.TextMessage -> responseMessage += event.text
}
}// 1. Get or create an isolated session for the current trip
val session = runner.getOrCreateSession(tripId)
// 2. Run a turn (history is preserved automatically across turns)
session.runAsync(prompt).collect { event ->
when (event) {
is AgentEvent.FunctionCall -> showToolRunning(event)
is AgentEvent.FunctionResponse -> showToolCompleted(event)
is AgentEvent.TextMessage -> responseMessage += event.text
}
}
// 3. Clear history when the user taps "New conversation"
session.delete()// Turn 1: Execute function directly without a second model call
val turn1 = content(Content.Role.USER) {
text("Change the background to a sunset")
}
val response1 = generativeModel.generateContent(
generateContentRequest(turn1) { setTools(tools) }
)
executeFunctionCalls(response1)
// Turn 2: Send only user prompts to save context tokens
val customHistory = listOf(
turn1,
content(Content.Role.USER) { text("Oh no, I meant a beach") }
)
val response2 = generativeModel.generateContent(
GenerateContentRequest.Builder(customHistory).setTools(tools).build()
)