מהנדס תוכנה
כדי לשפר את תכונות ה-AI ב-Android, אתם יכולים לפתח סוכנים אוטונומיים במכשיר. בסשן הזה נתעמק בממשקי ה-API החדשים של ML Kit, שמבוססים ישירות על ממשקי ה-API לגישה ישירה למודלים שאתם כבר מכירים.
באמצעות אפליקציית Jetpacker בקוד פתוח, תלמדו איך לשרשר פעולות בכמה שלבים בנתונים מחנויות מקומיות, לנהל סשנים עם שמירת מצב בכמה שלבים עם הקשרים מבודדים, ולחבר את Android MCP כדי להפעיל גם עוזרים דיגיטליים באפליקציה וגם פלטפורמות ברמת המערכת של Android באמצעות בסיס קוד אחד.
AgentRunner מתאם את הפעלת הכלים ומספק שיטות סשן לשיחות עם כמה פניות בלי לאבד את ההקשר.תמליל
קוראים לי יולנדה ורהוף.
אני מהנדס קשרי מפתחים ב-Android, והיום הצטרפה אליי ויקטוריה לסטארי, חברה בצוות ההנדסה של AICore.
שלום לכולם,
תודה שהצטרפתם אלינו הבוקר.
לכן, הצוותים שלנו משתפים פעולה באופן הדוק כבר שנים כדי להוציא מודלים מתקדמים של AI מהענן ולהפעיל אותם ישירות במכשירי Android.
המאמצים האלה מקובצים בחבילת ML Kit GenAI.
מצד אחד, בחבילת ML Kit GenAI יש ממשקי API ברמה גבוהה לפתרונות מוכנים לשימוש, כמו סיכום טקסט, תיאור תמונה וזיהוי דיבור.
מצד שני, פתחנו גם גישה ישירה למודל במכשיר, Gemini Nano.
והיום אנחנו עושים צעד נוסף.
אז עם הגישה הישירה הזו, אנחנו בעצם נאפשר לכם לבנות סוכנים בתוך האפליקציה, סוכנים שיכולים להסיק מסקנות על פני כמה שלבים, שיכולים לשמור את ההקשר של השיחה, ושיכולים לבצע פעולות באפליקציה שלכם.
אז למה אנחנו בעצם מתכוונים, ויקטוריה?
כן.
עד עכשיו, כשדיברנו על תרחישים לדוגמה במכשיר, חשבנו על משימות קצרות, פשוטות ומבודדות, כמו זיהוי תרמית אפשרית או סיכום של שיחת טלפון.
כשאנחנו רוצים לבנות משהו מורכב יותר, כמו לשאול שאלות על קטלוג התמונות שלכם או לשאול את מפות Google, אנחנו פונים באופן טבעי למודלים גדולים בענן.
לכן, למודלים בענן יש יתרון של עיבוד עצום ונתונים בזמן אמת, אבל AI במכשיר פותח אפשרויות חדשות לעיבוד נתונים מחנויות מקומיות בזמן אמת ובזמן אחזור קצר.
אז Gemini Nano כבר הרחיב את מה שאתם יכולים לעשות במכשירים שלכם.
דוגמה אחת היא "Pixel צילומי מסך", שמאפשרת לחפש צילומי מסך מקומיים ולשאול שאלות לגביהם.
לכן, כשאנחנו מריצים סוכנים במכשיר, אנחנו צריכים קפיצת מדרגה משמעותית ביכולות של המודל, וזו הסיבה שאנחנו מפתחים את Gemini Nano 4, שיכול להסיק מסקנות לאורך שלבים ולבצע פעולות אמיתיות.
אבל היכולת החדשה הזו רלוונטית רק אם למפתחים יש מכשירים אמיתיים לטרגט.
אז החדשות הטובות הן ש-Gemini Nano 4 כבר זמין במכשירים נבחרים, ואפשר גם לבדוק אותו במכשירים רבים נוספים באמצעות תצוגה מקדימה למפתחים של AICore.
אנחנו גם בדרך להגיע ל-100 מיליון מכשירים בתחילת השנה הבאה.
אז מה זה אומר?
זה אומר שאתם יכולים להתחיל לפתח ולבדוק את החוויות האלה כבר היום, בידיעה שמה שאתם מפתחים היום יגיע לקהל עצום.
אז דיברנו על Gemini Nano 4, דיברנו על פוטנציאל החשיפה, ועכשיו בואו נדבר על מה המשמעות של יכולות אג'נטיות.
לכן, AI אקטיבי יכול לתכנן משימות מורכבות, לחלק אותן לכמה שלבים ואז לבצע את השלבים האלה באפליקציה.
לדוגמה, מודל Gemini Nano במכשיר יכול לשלוח שאילתה למסד הנתונים המקומי, לאחזר נתונים מהקצה העורפי, להסיק מסקנות ולענות על שאלות מורכבות, וגם להפעיל תהליך עבודה של אפליקציית נייטיב במכשיר.
אבל איך ה-AI עובר מתכנון לפעולות אמיתיות?
וזה הנושא של היום: קריאה להפעלת פונקציות.
זה המנוע העיקרי של היכולות האג'נטיות.
הוא מתרגם שפה טבעית לפעולות במערכת, ומעביר את המודל מחשיבה פסיבית לביצוע פעיל על ידי בחירה והפעלה של ממשקי ה-API המקומיים שנדרשים כדי לפתור את המשימות המורכבות.
אנחנו גם שמחים להודיע שקריאה להפעלת פונקציות נתמכת באופן מובנה ב-Gemini Nano 4.
הוא זמין כרגע בגרסת תצוגה מקדימה למפתחים של AICore, ובקרוב יושק בכל המכשירים שנתמכים ב-Nano 4.
אז זה נהדר שיש לנו יכולות כאלה במודל, אבל יכולת במודל עדיין לא נותנת לכם, כמפתחים, את ממשקי ה-API.
לכן, אנחנו עובדים גם על הוספת ממשקי ה-API המתאימים לחבילת ML Kit GenAI כדי לוודא שפיתוח הסוכן יהיה קל ופשוט.
אז התחלנו עם ממשקי ה-API לגישה ישירה שכבר היו לנו, והוספנו להם סוגי פונקציות וסוגי היסטוריית שיחות.
בעזרת שתי התוספות החדשות האלה ל-Prompt API של ML Kit, תוכלו עכשיו לשלב באפליקציה שלכם את היכולות האג'נטיות האלה ברמה נמוכה.
אבל יש הרבה דברים שצריך לעשות בעצמכם בשלב הזה.
לכן, צריך להפעיל את הפונקציות באופן ידני, לשמור את היסטוריית השיחות ולעבור על כמה שיחות.
אז אם יש כמה קריאות שצריך לבצע, צריך לבנות בעצמך מערכת לולאות כלשהי, וצריך לבנות מעקות בטיחות משלך למערכת.
זה אפשרי, אבל כמובן שאנחנו רוצים לעשות את זה מהר ובקלות.
לכן, בנוסף לספריית ה-Prompt API הזו, הוספנו גם ספרייה חדשה של תהליכי עבודה מבוססי-סוכן.
אפשר למצוא אותו באמצעות com.mlkit.genai:agent-api, והוא זמין כרגע בגרסת אלפא.
עכשיו, עם ה-API החדש הזה, אנחנו עושים הרבה מהעבודה הקשה בשבילכם.
במקום שתצטרכו לבצע הרבה שלבים ידניים, תוכלו ליצור נציג באמצעות כמה שורות קוד בלבד.
וזה בדיוק מה שנעשה היום.
אז נראה איך ממשקי ה-API האלה פועלים בפועל עם האפליקציה לדוגמה שלנו, Jetpacker.
כבר ראיתם את Jetpacker ב-Google I/O ובמהלך הכנס הזה.
אבל מה שאתם יכולים לעשות זה לסרוק את קוד ה-QR הזה, ואם אתם מעוניינים, אתם יכולים להסתכל על הקוד, אתם יכולים להפנות את הנציג שלכם לקוד, אתם יכולים ליצור אינטראקציה עם הקוד, ואתם יכולים לקבל מידע נוסף על האופן שבו הטמענו את כל התכונות האלה.
Jetpacker הוא עוזר נסיעות מודרני שבנינו כדי להציג הרבה תכונות חכמות שונות.
אז היא כבר משתמשת במודלים במכשיר ובמודלים בענן לכל מיני Prompt API, אבל עכשיו אנחנו הולכים להוסיף לה סוכן.
ובסוכן הזה, Jetpacker, אנחנו משתמשים ב-API החדש של סוכן ה-AI הגנרטיבי ב-ML Kit שדיברנו עליו, והוא ישתמש במערכת AI Core הבסיסית שפועלת במכשירי Android כדי לתקשר עם המודל של Gemini Nano.
אני יכול לדבר כמה שאני רוצה, ואנחנו נדבר הרבה, אבל כמובן שאנחנו רוצים להראות את זה.
אז בואו נעבור לטלפון ונראה את זה בהדגמה.
יש לי כאן מכשיר Pixel 11 Pro.
אני חושב שאנחנו יכולים לעבור ל-Pro, ואתם אמורים לראות את זה.
הנה, זה כאן.
וכמו שאתם רואים, אפליקציית Jetpacker פועלת כאן.
יש כאן כמה נסיעות קרובות, ואני יכולה להסתכל על הנסיעה הנוכחית שלנו. בנסיעה הנוכחית, אפשר לראות שהיום ה-29 באוקטובר, אז יש לנו את הסדנה בנושא AI במכשיר, הדגמה, ומחר יש לי שיט במפרץ גשר הזהב וביקור במוזיאון.
כל זה נהדר.
עכשיו אנחנו יכולים לעבור למסך ההוצאות שבו אנחנו עוקבים אחרי התקציב של הנסיעה הזו.
כמו שאתם יכולים לראות בפינה השמאלית התחתונה, יש את הסמל החדש של הסוכן, וזה הסוכן במכשיר שבנינו.
נתחיל עם פקודה די פשוטה.
אני חושב שאנחנו יכולים לבקש ממנו להוסיף הוצאה ספציפית.
למשל, אפשר להגיד: "תוסיף הוצאה על המונית למאונטיין ויו, והיא עלתה 45 דולר".
זהו.
הוא מכין את ההוצאה ומוסיף אותה.
מדהים.
אוקיי, הדגמה 1 עבדה.
בואו נחזור לשקפים ונראה איך זה עבד בפועל.
אוקיי, אז קודם כול, כמו ששמתם לב, השתמשתי בקול שלי, והקול הזה בעצם משתמש גם במודל Gemini Nano באמצעות ה-API לזיהוי דיבור כדי לתמלל את מה שאמרתי לטקסט.
הטקסט הזה מצוין, ואנחנו יכולים לשלוח אותו למודל Gemini Nano.
אבל מה הוא יעשה?
למשל, אני אומר לו להוסיף משהו להוצאות, אני אומר לו משהו על 45 דולר, אבל הוא לא באמת יודע מה לעשות, נכון?
בשלב הזה, הוא פשוט יגיד לי, "אני לא יודע על מה אתה מדבר".
אז מה שאנחנו רוצים לעשות זה להוסיף קצת להנחיה הזו.
אנחנו יכולים להוסיף כלים לפרומפט, אז אנחנו יכולים להוסיף כל מיני כלים, במקרה הזה, אני מוסיף קבלת תקציב נסיעה, מסלול נסיעה וכו'. ואם תעבירו את זה למודל Nano עם ממשקי ה-API שלנו, הוא יבין באופן אוטומטי שבמקום להחזיר טקסט, הוא יכול עכשיו להחזיר בקשה להפעלת פונקציה.
אז במקרה שלנו, הוא בעצם יגיד, "טוב, במקום להגיב בטקסט, אני אוסיף הוצאה של 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, שזו דרך של המערכת, של ה-API, להודיע לנו מה היא עושה בפועל.
לכן, הוא יגיד לנו מתי הוא מחזיר טקסט, מתי הוא מבצע קריאת API לכלי וכו', כדי שנוכל להציג את זה בממשק המשתמש שלנו.
אוקיי.
בואו נסתכל על הקוד עצמו.
אוקיי.
לפני שנתחיל לכתוב את הקוד, אנחנו צריכים לבדוק אם התכונה "קריאה להפעלת פונקציות" זמינה במכשיר שלכם, כי היא נתמכת רק ב-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.
אז מריץ הסוכנים הזה יעזור לנו לנהל את לולאת ההרצה.
אנחנו פשוט קוראים לו על ידי קריאה למדריך, constra-- constructor ומעבירים אליו את הנציג.
ולבסוף, אנחנו יכולים לעטוף את הפקודה הקולית המתומללת בבלוק תוכן כדי ליצור את ההנחיה.
ואז אנחנו מפעילים את ה-runner.
אנחנו קוראים לפונקציה הזו runSingleTurnAsync בהנחיה, והיא תפעיל את הנציג, תריץ את ההסקה ותבצע את כל ההסקה של ה-AI.
ואם אנחנו רוצים להעביר בסטרימינג רק את התשובה הסופית של המודל בטקסט לממשק המשתמש שלנו, אנחנו פשוט קוראים ל-textTokens, והוא יאסוף את התשובה כשהיא תגיע.
זהו.
אלה באמת רק כמה שורות קוד, אז לא צחקנו כשאמרנו את זה.
אז עם כמה שורות קוד, יש לכם נציג.
הסוכן הזה יעשה מה שאתם רוצים.
הוא ייתן לנו תשובה בטקסט.
אבל כמובן שבהדגמה הראשונה שהראינו, השתמשנו רק בפקודה פשוטה מאוד, נכון?
למשל, נתתי פקודה אחת והוא ביצע פעולה אחת וזהו, אז הוא עדיין לא ממש סוכן מלא, נכון?
אז בואו נתקדם קצת.
בחלק הבא, אני רוצה לתת פקודה, אבל אני רוצה להקשות קצת על הסוכן.
אז בואו נחזור למכשיר ההדגמה שלי, ונראה אם אנחנו יכולים להקשות קצת יותר.
אנחנו כבר שם.
מדהים.
אז כמו שאמרתי, תכננו ביקור במוזיאון למחר.
ובביקור במוזיאון, אם אפתח את זה, אפשר לראות שמחיר הכניסה הוא 25$.
עכשיו נראה אם הנציג שלנו באמת מסוגל להבין את זה בעצמו.
אז אני חוזר למסך ההוצאות ונותן לו פקודה, ואומר, "תוסיף את הכרטיס למוזיאון להוצאות שלי".
זהו.
ועכשיו, כמו שאתם רואים, הוא בודק את מסלול הנסיעה שלי, מוצא את הביקור במוזיאון ומוסיף אותו להוצאות שלי.
אז כאן נדרשים שלושה שלבים כדי להגיע ליעד, וקיבלתי גם תשובה סופית בטקסט.
שוב, כל זה קורה באופן אוטומטי.
ראיתם את המהירות האמיתית של המודל הזה במכשיר.
אז בואו נחזור לשקפים ונראה איך זה עובד.
כמו שאתם רואים, שינינו קצת את הדברים בהשוואה להדגמה הראשונה שלנו.
במקרה הזה, כמו שאתם רואים, לא סיפרתי לו על המוזיאון, לא סיפרתי לו על ההוצאה, ולכן, כשהמודל קיבל את ההנחיה ואת הכלים, הוא מנסה להחליט מה לעשות קודם.
אז כאן, הוא מחליט שהוא צריך לדעת יותר על מסלול הנסיעה הנוכחי שלי, ולכן הוא יקרא לפונקציה getItinerary.
הוא מקבל תשובה ואז הוא יודע, אה, יש ביקור במוזיאון במסלול הנסיעה הזה, כנראה שעל זה היא דיברה.
בואו נחפש את הפרטים של האירוע הספציפי הזה.
ובתוך כל המטא-נתונים של האירוע הספציפי הזה, הוא יראה שהערך הוא 25$.
הוא מבין, חוזר אחורה ומבין שהוא צריך להפעיל את הפונקציה להוספת הוצאה.
אז כאן אנחנו רואים כמה שלבים שמתרחשים אחד אחרי השני.
אז ויקטוריה, את יכולה להרחיב קצת בנושא?
כן.
אז מה שיולנדה תיארה עכשיו הוא דוגמה קונקרטית להדגמה שהיא הראתה, אבל כאן אני אקח את זה קצת יותר לשכבה מופשטת ורעיונית.
זה יקרה בכל פעם שנבצע קריאה להפעלת פונקציות בכמה שלבים.
אז קודם כל, זו האפליקציה שלנו, וזה המודל של Gemini Nano.
המשתמש מתחיל בשליחת השאילתה ורשימת הכלים הזמינים למודל.
המודל ישתמש במידע הזה כדי להחליט מה לשלוח בחזרה.
ואז, נניח שבמקרה הראשון, הוא שולח בחזרה קריאת API לכלי, נקרא לה קריאת API לכלי 1.
ואז האפליקציה תבצע את קריאת ה-API לכלי ותקבל תגובה מהכלי.
ואז, מה שיישלח בחזרה למודל הוא למעשה היסטוריית השיחות המלאה, שכוללת את השאילתה המקורית, רשימת הכלים, קריאת ה-API הראשונה לכלי והתגובה הראשונה מהכלי.
ואז המודל משתמש שוב במידע הזה כדי להחליט מה לעשות הלאה.
לכן, יכול להיות שהתהליך יימשך עד שנקבל את התוצאה הסופית.
אז נניח שהמודל יחזיר את קריאת ה-API לכלי ה-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 כמו קודם.
מדהים!
אז אנחנו קוראים להרצה הזו "הרצה אסינכרונית של סבב יחיד", מה שכמובן מניח שיש גם סוג אחר של API, נכון?
כן.
כמובן שלא מספיק לבצע רק שלב אחד.
אנחנו רוצים לבצע כמה שלבים.
אז בואו נעבור לסשנים עם כמה פניות, ואני חושב שאנחנו יכולים לחזור להדגמה שלי ולהפוך אותה לקצת יותר קשה.
אז דיברנו על זה שיש לנו ביקור במוזיאון, אבל גם את השיט במפרץ גשר הזהב.
אז לגבי זה, אני רוצה להבין אם יש לי מספיק תקציב לפני שאני מזמינה בפועל.
בואו נשאל שאלה.
בואו נראה.
אני חושב לקחת את הצוות שלי לשיט במפרץ גשר שער הזהב מחר, וחשבתי שזה עולה בערך 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 כמו קודם, זה אותו דבר.
ואז, כש-Jolanda לחצה על הכפתור של השיחה החדשה, כפתור הרענון, אנחנו יכולים בעצם לקבל session.delete.
כן.
אוקיי.
אז עשינו את כל זה עם ממשקי ה-API החדשים של הסוכנים, וזה מדהים וזה יעבוד בצורה מדהימה בהרבה מהתרחישים לדוגמה שלכם.
אבל כמובן, אנחנו תמיד רוצים לעשות דברים מהירים, או לאפשר דברים קלים, אבל אנחנו גם רוצים לאפשר לכם את המקרים המורכבים יותר.
לכן, אנחנו רוצים לוודא שאם אתם צריכים, אתם יכולים להשתמש גם בממשקי ה-API לגישה ישירה כדי ליצור גרסה ברמה נמוכה יותר של הדברים האלה.
אז אולי, ויקטוריה, תוכלי להסביר מה זה אומר ומתי נרד שוב לרמת הגישה הישירה הזו?
כן, בטח, Jolanda.
יכול להיות שאתם כבר מכירים את Prompt API, שמאפשר גישה ישירה כזו.
לכן, הוספנו תמיכה בקריאה להפעלת פונקציות גם כאן, ויש לנו שני תרחישים לדוגמה שבהם נעדיף להשתמש ב-API הזה במקום בתהליך הפשוט יותר של הסוכן.
הראשון הוא תרחיש שימוש פשוט, פשוט מאוד.
אז אתם זוכרים שבתהליך העבודה של הסוכן, אנחנו שולחים את כל השיחה, השאילתה, הכלים, קריאת ה-API לכלי, התגובה של הכלי, הכול בחזרה למודל.
יכול להיות שיש תרחישים לדוגמה שבהם אנחנו לא רוצים לעשות את זה, אלא רק לשלוח את ההנחיה, נניח.
אז נניח שאנחנו רוצים לשלוח הנחיה למודל, והוא מחזיר קריאת API לכלי, ואז אנחנו לא רוצים להחזיר את קריאת ה-API לכלי, אנחנו רק רוצים לשלוח עוד הנחיה למודל, כדי שנוכל לערוך את ההיסטוריה שם, כאילו יש לנו שליטה מלאה על ההיסטוריה.
הנה דוגמה קונקרטית יותר.
// 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 כדי לעשות את זה.
יש לנו שליטה מלאה על ההיסטוריה, אנחנו שומרים את הטוקן של ההקשר במכשיר, כי במכשיר יש חלון טוקנים מוגבל יותר, וככה אנחנו יכולים לשמור יותר טוקנים.
תרחיש השימוש השני הוא למעשה ההפך הגמור.
למשל, אם יש לנו תרחישים מורכבים שה-API של הסוכן עדיין לא תומך בהם, כמו מערכות מרובות סוכנים שבהן כמה סוכני משנה מתקשרים זה עם זה כדי לפתור משימה מורכבת, או מערכת היברידית שבה יש גם מודלים במכשיר וגם מודלים בענן, או אם אנחנו רוצים זיכרון מותאם אישית ודחיסת הקשר.
אז זה קשור להיסטוריה, כמו השליטה המלאה בהיסטוריה שציינתי קודם.
נניח שאנחנו רוצים, בגלל שחלון ההקשר מוגבל, אנחנו לא רוצים לשלוח הכל בחזרה למודל, אנחנו רק רוצים לסכם את החלק הכי חשוב, או אולי את החלק האחרון, אז יש לנו שליטה מלאה על זה, ואז אנחנו הולכים ומשתמשים ב-API לגישה ישירה.
לתרחישים לדוגמה כאלה, אנחנו יכולים להשתמש במסגרות כמו ערכת הפיתוח לסוכנים (ADK) או Crew, או ליצור מסגרת תזמור מותאמת אישית משלנו.
אוקיי.
זה כל מה שהיה לנו על ה-API של הסוכן.
אבל יש עוד משהו שרצינו לדבר עליו.
דיברנו כל הזמן על סוכנים בתוך האפליקציה, אז אתם בונים חוויה משלכם בתוך האפליקציה שלכם, אבל דיברנו גם על איך אנחנו משתמשים ב-AppFunctions, ולמה שתשתמשו ב-AppFunctions אם הדבר היחיד שאתם עושים הוא בתוך האפליקציה שלכם?
אנחנו יודעים שמפתחים כמוכם לא ממש אוהבים לעשות דברים פעמיים, נכון?
אנחנו לא אוהבים להעתיק ולהדביק קוד, ואנחנו לא אוהבים לשכפל לוגיקה עסקית.
אז עם AppFunctions, במקום לאפשר רק לסוכן בתוך האפליקציה להשתמש בהן, בעזרת שינוי פשוט מאוד, אנחנו יכולים להפוך את הכלים שלנו לזמינים גם לסוכני המערכת.
אז איך זה נראה בקוד?
כן.
אז עם AppFunctions, אנחנו צריכים לכתוב רק פעם אחת, ואז אנחנו יכולים לחשוף אותן לרמות שונות של שכבות כרצוננו.
אתם זוכרים שקודם היה לנו את ACCESS_LEVEL_SELF, שחושף את הפונקציות שכתבנו רק לסוכן בתוך האפליקציה, אבל אנחנו יכולים גם לשנות את רמת הגישה הזו ל-ACCESS_LEVEL_ANDROID_TRUSTED.
כך הפונקציות ייחשפו לסוכנים מהימנים ב-Android OS.
כך שאין צורך לשכתב את הפונקציות, הפקודות או הלוגיקה של מסד הנתונים.
AppFunctions כבר משמשת כקול של האפליקציה שלכם, גם באופן פנימי וגם באופן חיצוני.
אנחנו רוצים לראות הדגמה גם של זה, נכון?
אוקיי, נחזור לטלפון, ובהדגמה האחרונה נצא מאפליקציית Jetpacker.
אז אנחנו יכולים לצאת, ולעבור ל-Gemini.
אז ב-Gemini, אנחנו יכולים להזין @Jetpacker.
חשוב לציין שהאפשרות הזו זמינה כרגע רק בגרסת בטא מוגבלת, אז היא לא תהיה זמינה לכולם כבר היום, אבל אפשר לבדוק אותה ולנסות אותה.
בואו נראה מה קורה עם Jetpacker.
מה נעשה היום?
אה, שתיתי קפה הבוקר, נכון?
תוסיף בבקשה קפה ב-7 $להוצאות הנסיעה של ADS?
זהו.
ואפשר לראות שהוא מתחבר לאפליקציית Jetpacker.
וזהו.
ההוצאה נוספה בשבילי.
אז בעצם ראינו איך אנחנו יכולים להשתמש בכל מה שיש ב-Jetpacker.
הדבר היפה ב-Jetpacker הוא שאנחנו יכולים גם להפעיל את מצב הטיסה.
אז לא ממש הראינו את זה היום, אבל אנחנו יכולים פשוט להפעיל את מצב הטיסה, וכל מה שראיתם בהדגמות, ואני רוצה להציג את זה, כי אני רוצה לוודא שכולכם סומכים עליי, אז בואו נכבה את ה-Wi-Fi.
יש למישהו דוגמה טובה להוצאה שאני יכול להוסיף?
בואו נראה.
אולי אפשר לעשות את "מה זה?"
השכרת רכב יכולה להיות נחמדה, נכון?
אוקיי.
לדוגמה, "תוסיף הוצאה של 300 $על השכרת רכב".
הנה, הוא הבין.
אוקיי.
מדהים.
אוקיי, נחזור לשקפים.
דיברנו על AppFunctions.
אני יודע שנגענו בזה ממש בקצרה.
אז אם אתם רוצים לדעת עוד, יש גם שיחה נוספת של דן ושרה שמפרטים הרבה יותר על איך אתם יכולים להפעיל את זה, איך אתם יכולים לבדוק את זה, אילו כלים יש לנו וכו'.
אז היום עברנו על הרבה דברים שונים.
ראינו את Single-Step, ראינו את Multi-Step, ראינו את Multi-Turn, ולבסוף ראינו גם איך אפשר להשתמש ב-System AppFunction.
עכשיו, לפני שנסיים, יש לנו כמה טיפים.
כן.
אז זכרו את שלושת הטיפים האלה כשאנחנו מתחילים לבנות את החוויות האלה של סוכנים.
הראשון הוא להתמקד בערכות הכלים.
חשוב לזכור שהיכולות של מודלים במכשיר מוגבלות בגלל הגודל הקטן שלהם.
לכן, במקום לרשום עשרות כלים, עדיף לבחור מספר קטן יותר של כלים.
וכמו שאמרתי קודם, כתבו תיאורי KDoc ברורים כי זה מה שהמודל משתמש בו כדי להחליט מה הוא הולך להחזיר.
ואז אנחנו יכולים גם להגדיר מעקה בטיחות מקסימלי לפנייה.
זה גם מונע מהמודל להיכנס ללולאה אינסופית אם הוא מתבלבל.
המשמעות היא שאנחנו עלולים לרוקן את הסוללה של המשתמש.
כך שאם יש לנו את זה, אנחנו יכולים לחסוך בסוללה של המשתמש.
ולבסוף, אנחנו צריכים לבדוק אם המכשיר כבר תומך בקריאה להפעלת פונקציות, ולהטמיע חלופה חלקה.
לדוגמה, אם אין תמיכה, אפשר להפנות למודלים בענן או לממשק המשתמש הרגיל.
אוקיי, נראה לי שאנחנו יכולים לסיים את הטיפול בזה בעזרת המשאבים שלנו.
אז שוב, אני אראה לכם את קודי ה-QR.
כדאי לבדוק את זה.
אני חושב שאחד הדברים שחשוב לזכור הוא שיש לנו גם מיומנויות Android בשביל זה.
כך שלא צריך להתעמק בכל פרט ופרט.
יש לנו תיעוד מלא ומיומנות Android שמתאימה ל-API החדש הזה.
כדאי לבדוק את זה.
כדאי להוריד את גרסת ה-Developer Preview של 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()
)