Инженер по связям с разработчиками
Разработчик ПО
Создавайте автономных агентов на устройстве, чтобы расширить возможности ИИ-функций Android. На этом занятии мы рассмотрим новые агентские API ML Kit, созданные на основе уже знакомых вам API прямого доступа к моделям.
На примере приложения Jetpacker с открытым исходным кодом вы узнаете, как объединять в цепочки многоэтапные действия с локальными данными, управлять многоэтапными сеансами с изолированными контекстами и подключать Android MCP, чтобы использовать одну базу кода для работы с помощниками в приложении и на уровне системы Android.
AgentRunner координирует выполнение инструментов и предоставляет методы сеанса для многоэтапных разговоров без потери контекста.Расшифровка
Меня зовут Иоланда Верхуф.
Я инженер по связям с разработчиками в Android, и сегодня ко мне присоединилась Виктория Лестари, которая входит в команду инженеров AICore.
Здравствуйте!
Спасибо, что присоединились к нам сегодня утром.
Поэтому наши команды уже много лет работают над тем, чтобы перенести современные ИИ-модели из облака на устройства Android.
Все эти усилия объединены в пакете ML Kit GenAI.
С одной стороны, в ML Kit GenAI есть высокоуровневые API для готовых решений, таких как краткий пересказ текста, описание изображений и распознавание речи.
С другой стороны, мы также открыли прямой доступ к модели Gemini Nano, которая работает на устройстве.
Сегодня мы сделаем ещё один шаг.
Благодаря этому вы сможете создавать агентов, которые будут работать в приложении, рассуждать на протяжении нескольких шагов, сохранять контекст разговора и выполнять действия в приложении.
Виктория, что это значит?
Да.
До сих пор, когда мы говорили о сценариях использования на устройстве, мы имели в виду более короткие, простые и изолированные задачи, такие как обнаружение вероятного мошенничества или обобщение телефонного звонка.
А когда мы хотим создать что-то более сложное, например, задать вопросы о вашем каталоге фотографий или спросить Google Карты, мы, естественно, обращаемся к большим облачным моделям.
Таким образом, облачные модели предлагают преимущество огромной обработки и данных в реальном времени, но ИИ на устройстве открывает новые возможности для низкой задержки и обработки данных в реальном времени и локально.
Таким образом, Gemini Nano уже расширила возможности ваших устройств.
Например, в приложении "Pixel Скриншоты" можно искать скриншоты, сохраненные на устройстве, и задавать вопросы о них.
Поэтому для запуска агентов на устройстве нам нужен значительный скачок в возможностях модели. Именно поэтому мы создаем Gemini Nano 4, которая способна рассуждать на разных этапах и предпринимать реальные действия.
Но эта новая возможность имеет значение только в том случае, если у разработчика есть реальные устройства для таргетинга.
Хорошая новость в том, что Gemini Nano 4 уже доступна на некоторых флагманских устройствах, а также вы можете протестировать ее на многих других устройствах, используя предварительную версию AICore для разработчиков.
Мы также планируем достичь отметки в 100 миллионов устройств в начале следующего года.
Что это значит?
Это значит, что вы можете начать создавать и тестировать такие решения уже сегодня, зная, что они будут доступны огромной аудитории.
Мы обсудили Gemini Nano 4 и потенциальный охват. Теперь давайте поговорим о том, что такое агентные возможности.
Агентный ИИ может планировать сложные задачи, разбивать их на несколько шагов, а затем выполнять эти шаги в вашем приложении.
Например, наша модель Gemini Nano на устройстве может запрашивать локальную базу данных, извлекать данные из серверной части, рассуждать и отвечать на сложные вопросы, а также запускать рабочий процесс нативного приложения на вашем устройстве.
Но как ИИ переходит от планирования к реальным действиям?
И это тема сегодняшнего занятия – вызов функций.
Это основа агентных возможностей.
Он переводит естественный язык в системные команды и переводит модель от пассивных рассуждений к активному выполнению, выбирая и запуская локальные 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 – это современный помощник для путешественников, который мы создали, чтобы продемонстрировать множество различных интеллектуальных функций.
Оно уже использует модели на устройстве и в облаке для всех видов API подсказок, но теперь мы собираемся добавить в него агента.
Для Jetpacker мы будем использовать новый API агента GenAI в ML Kit, о котором только что говорили. Он будет использовать базовую систему AI Core, которая работает на устройствах Android, для связи с моделью Gemini Nano.
Я могу говорить сколько угодно, и мы ещё много чего обсудим, но, конечно, лучше один раз увидеть.
Давайте переключимся на телефон и посмотрим, как это работает.
У меня есть Pixel 11 Pro.
И я думаю, что мы можем переключиться на Pro, и вы сможете это увидеть.
Вот так.
Как видите, у нас запущено приложение Jetpacker.
Здесь есть список наших предстоящих поездок, и я могу посмотреть текущую поездку. В текущей поездке вы можете видеть, что сейчас 29 октября, поэтому у нас есть семинар по ИИ на устройстве, демонстрационная витрина, а завтра у меня круиз по заливу Золотые Ворота и посещение музея.
Это здорово.
Теперь мы можем перейти к экрану расходов, где мы отслеживаем бюджет этой поездки.
Как вы видите в правом нижнем углу, там есть новая иконка агента. Это агент на устройстве, которого мы создали.
Начнем с довольно простой команды.
Думаю, мы можем попросить его добавить определенный расход.
Добавь расход на такси до Маунтин-Вью, 45 долларов.
Так вот…
Он готовит расходы и добавляет их.
Потрясающе.
Итак, первая демонстрация прошла успешно.
Давайте вернемся к слайдам и посмотрим, как это работает.
Итак, во-первых, как вы заметили, я использовал свой голос, и этот голос также использует модель 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, и AgentEvents – это способ для системы, для 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 }
}Мы знаем, что вызов функций поддерживается на нашем устройстве, и теперь мы можем просто написать start the a-- build the agent, используя Agent.builder.
Затем мы можем задать его название.
Мы можем setInstructions, чтобы определить контекст и образ.
И наконец, мы вызываем enableAppFunctions.
Итак, эта функция будет вызывать, будет циклически перебирать и автоматически обнаруживать все AppFunctions, которые мы только что перечислили ранее, и мы только что создали агента.
Далее нам нужно инициализировать AgentRunner.
Итак, этот агент-раннер будет управлять циклом выполнения за нас.
Мы просто вызываем его, вызывая инструктора, constra-- конструктор и передавая ему агента.
И наконец, мы можем обернуть расшифрованную голосовую команду в блок контента, чтобы создать запрос.
Затем мы вызываем исполнителя.
Мы вызываем эту функцию 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, как и раньше.
Прекрасно!
Итак, мы называем этот запуск асинхронным запуском с одним поворотом, что, конечно, предполагает, что существует и другой вид 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.
Когда Иоланда нажала кнопку "Новый чат", а затем кнопку "Обновить", мы увидели session.delete.
Да.
Окей.
Мы сделали все это с помощью новых API агентов, и это потрясающе. Они будут отлично работать во многих ваших сценариях использования.
Конечно, мы всегда стремимся упростить и ускорить работу, но мы также хотим, чтобы вы могли решать и более сложные задачи.
Поэтому мы хотим убедиться, что при необходимости вы по-прежнему можете использовать API прямого доступа для создания более низкоуровневой версии этих вещей.
Виктория, может быть, вы объясните, что это значит и когда мы снова перейдем на этот уровень прямого доступа?
Да, конечно, Иоланда.
Возможно, вы уже знакомы с Prompt 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 прямого доступа.
Для этого можно использовать такие фреймворки, как Agent Development Kit (ADK) или Crew, или создать собственный фреймворк оркестрации.
Окей.
Это все, что мы хотели рассказать об Agentic API.
Однако есть ещё кое-что, о чём мы хотели бы упомянуть.
Мы все время говорили об агентах в приложении, то есть вы создаете собственный агентский опыт внутри своего приложения, но мы также говорили о том, как мы используем 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.
Я знаю, что мы затронули эту тему очень поверхностно.
Если вы хотите узнать больше, посмотрите выступления Дэна и Сары. Они подробно рассказывают, как включить и протестировать эту функцию, какие инструменты мы предлагаем и т. д.
Сегодня мы рассмотрели много разных вещей.
Мы рассмотрели одношаговые, многошаговые и многоэтапные вызовы, а также узнали, как использовать системную функцию AppFunction.
В заключение мы хотим дать вам несколько советов.
Да.
Помните об этих трех советах, когда начнете создавать агентские решения.
Первый – это фокус на наборах инструментов.
Поэтому помните, что возможности моделей на устройстве ограничены из-за их меньшего масштаба.
Поэтому вместо десятков инструментов лучше зарегистрировать несколько.
И, как я уже говорил, пишите понятные описания KDoc, потому что именно на их основе модель решает, что возвращать.
Затем мы также можем установить максимальное ограничение на количество ходов.
Это также предотвращает бесконечный цикл, если модель запутается.
Это может привести к быстрой разрядке батареи.
Это позволяет экономить заряд батареи.
И наконец, нам нужно проверить, поддерживается ли вызов функций на устройстве, и реализовать резервный вариант.
Например, если она не поддерживается, вы можете использовать облачные модели или стандартный интерфейс.
Думаю, мы можем завершить разговор.
Ещё раз показываю QR-коды.
Посмотрите, что вышло.
Думаю, стоит помнить, что у нас также есть Android Skills.
Так что вам не нужно погружаться в каждую мелочь.
Мы подготовили полную документацию и навык Android, которые помогут вам начать работу с новым API.
Поэтому посмотрите на это.
Скачайте предварительную версию 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()
)