Умные приложения

Как добавить в приложение агентные возможности с помощью ML Kit GenAI

Колонки

О приложении

О выступлении

Создавайте автономных агентов на устройстве, чтобы расширить возможности ИИ-функций Android. На этом занятии мы рассмотрим новые агентские API ML Kit, созданные на основе уже знакомых вам API прямого доступа к моделям.

На примере приложения Jetpacker с открытым исходным кодом вы узнаете, как объединять в цепочки многоэтапные действия с локальными данными, управлять многоэтапными сеансами с изолированными контекстами и подключать Android MCP, чтобы использовать одну базу кода для работы с помощниками в приложении и на уровне системы Android.

Сводка

  • Gemini Nano 4 поддерживает вызов функций, что позволяет встроенным моделям преобразовывать естественный язык в действия и управлять локальными API.
  • Библиотека Jetpack AppFunctions определяет инструменты с помощью описаний KDoc на естественном языке. Изменение уровней доступа может сделать эти функции доступными для системных агентов, таких как Gemini.
  • Новая библиотека агентных потоков (альфа-версия) в ML Kit GenAI упрощает создание агентов, обрабатывая цикл выполнения и вызовы инструментов.
  • AgentRunner координирует выполнение инструментов и предоставляет методы сеанса для многоэтапных разговоров без потери контекста.
  • Прямой доступ через Prompt API по-прежнему доступен для простого управления историей или сложных настроек, таких как мультиагентные и гибридные системы.

Умение

Ресурсы

Gemini Notebook

Темы

  • ml-kit
  • on-device-ai
  • gemini-nano
  • ai-agents
  • вызов функций
  • app-functions
  • aicore

Расшифровка

Введение в ИИ на устройстве и Gemini Nano

  • Меня зовут Иоланда Верхуф.

  • Я инженер по связям с разработчиками в 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, мы берем на себя большую часть работы.

  • Вместо того чтобы выполнять множество действий вручную, вы сможете создать агента, написав всего несколько строк кода.

  • Именно этим мы сегодня и займемся.

Демонстрация агента Jetpacker и вызов функций

  • Давайте посмотрим, как эти 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 для разработчиков, чтобы протестировать все эти функции. Успехов в программировании!

Код

Фрагменты кода

Фрагмент кода на Kotlin в видео "Демонстрация агента Jetpacker и вызов функций"
/**
 * 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...
}
Фрагмент кода на Kotlin в видео "Создание и реализация исполнителя ИИ-агента"
val model = Generation.getClient()
val features = model.getAvailableFeatures()
if(FUNCTION_CALLING in features) {
  // Device is supported!
} else {
  // Graceful fallback
}
Фрагмент кода на Kotlin в видео "Создание и реализация исполнителя ИИ-агента"
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 }

}
Фрагмент кода на Kotlin в разделе "Архитектура и выполнение многоэтапного агента" (17:23)
/**
* 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 { ... }
Фрагмент кода на Kotlin в разделе "Архитектура и выполнение многоэтапного агента" (18:23)
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
 }
}
Фрагмент кода на Kotlin в разделе "Управление сеансами многоэтапных разговоров" (22:13)
// 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()
Фрагмент кода на Kotlin в видео "Продвинутые рабочие процессы с агентами и интеграция инструментов"
// 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()
)