智能应用

借助机器学习套件生成式 AI,在应用中解锁智能体功能

扬声器

关于

演讲简介

通过构建自主设备端智能体,让 Android AI 功能更上一层楼。在本讲座中,我们将深入探讨机器学习套件的新智能体 API,这些 API 直接基于您已经熟悉的直接模型访问 API 构建。

您将使用开源 Jetpacker 应用,学习如何跨本地数据串联多步操作、如何通过隔离的上下文管理有状态的多轮会话,以及如何连接 Android MCP,从而通过单个代码库同时为应用内助理和 Android 系统级界面提供支持。

摘要

  • Gemini Nano 4 原生支持函数调用,让设备端模型能够将自然语言转换为操作并驱动本地 API。
  • Jetpack AppFunctions 库通过自然语言 KDoc 描述来定义工具。更改访问权限级别可能会将这些函数公开给 Gemini 等系统代理。
  • 机器学习套件生成式 AI 中新推出的智能体流程库(Alpha 版)可处理执行循环和工具调用,从而简化智能体构建。
  • AgentRunner 负责协调工具执行,并提供会话方法,以便在不丢失上下文的情况下进行多轮对话。
  • 对于简单的历史记录控制或多代理和混合系统等复杂设置,您仍然可以通过 Prompt API 直接访问。

技能

资源

Gemini Notebook

主题

  • ml-kit
  • on-device-ai
  • gemini-nano
  • ai-agents
  • 函数调用
  • app-functions
  • aicore

转写文稿

设备端 AI 和 Gemini Nano 简介

  • 我叫 Jolanda Verhoef。

  • 我是 Android 团队的开发技术推广工程师,今天与我一起演讲的是 AICore 工程团队的 Victoria Lestari。

  • 大家好!

  • 感谢大家今天上午的参与。

  • 因此,多年来,我们的团队一直密切协作,致力于将前沿 AI 模型从云端迁移到 Android 设备上直接运行。

  • 这些工作都归入机器学习套件生成式 AI 套件。

  • 一方面,在这个机器学习套件生成式 AI 套件中,我们提供了用于一站式解决方案的高级 API,例如文本摘要、图片描述和语音识别。

  • 另一方面,我们还开放了对设备端模型 Gemini Nano 的直接访问。

  • 今天,我们更进一步。

  • 因此,通过这种直接访问,我们实际上允许您构建应用内智能体,这些智能体可以跨多个轮次进行推理,可以保存对话上下文,并且可以在您的应用中执行操作。

  • Victoria,这到底是什么意思?

  • 没错

  • 到目前为止,当我们谈论设备端用例时,我们想到的是更短、更简单、孤立的任务,例如检测可能的欺骗手段或总结通话内容。

  • 而当我们需要构建更复杂的应用时,例如询问有关照片目录的问题,或向 Google 地图提问,我们自然会求助于大型云端模型。

  • 因此,云端模型确实具有强大的处理能力和实时数据优势,但设备端 AI 为低延迟、实时和本地数据处理开辟了新的可能性。

  • Gemini Nano 已经拓展了您在设备上可以执行的操作。

  • 例如,借助“Pixel 截屏记事”,您可以搜索本地屏幕截图并询问相关问题。

  • 因此,要在设备端运行智能体,我们需要在模型能力上实现重大飞跃,这也是我们打造 Gemini Nano 4 的原因,它能够跨步骤进行推理并采取实际行动。

  • 但只有当开发者拥有可供测试的真实设备时,这项新功能才有意义。

  • 好消息是,Gemini Nano 4 已在部分旗舰设备上推出,您还可以通过 AICore 开发者预览版在更多设备上进行测试。

  • 我们也有望在明年年初达到 1 亿台设备。

  • 那么,这意味着什么?

  • 这意味着,您现在就可以开始构建和测试这些体验,因为您知道,您现在构建的内容将覆盖庞大的受众群体。

  • 我们已经讨论了 Gemini Nano 4,也讨论了其潜在覆盖范围,现在我们来谈谈智能体功能到底意味着什么。

  • 因此,智能体 AI 能够规划复杂任务,将这些任务分解为多个步骤,然后在您的应用中执行这些步骤。

  • 举例来说,设备上的 Gemini Nano 模型可以查询本地数据库、从后端检索数据、推理并回答复杂问题,还可以在设备上触发原生应用工作流。

  • 但 AI 究竟是如何从制定计划过渡到采取实际行动的呢?

  • 这就是今天的主题:函数调用。

  • 它是智能体功能的核心引擎。

  • 它将自然语言转换为系统执行,并通过选择和驱动解决复杂任务所需的本地 API,将模型从被动推理转变为主动执行。

  • 我们还很高兴地宣布,Gemini Nano 4 原生支持函数调用。

  • 该功能目前在 AICore 开发者预览版中提供,并将很快在所有支持 Nano 4 的设备上推出。

  • 模型具备这些功能固然很好,但作为开发者,你还需要 API 才能使用这些功能。

  • 因此,我们一直在努力为机器学习套件生成式 AI 套件添加合适的 API,以确保构建智能体变得轻而易举。

  • 因此,我们首先从已有的直接访问 API 入手,并为其添加了函数类型和对话记录类型。

  • 现在,借助机器学习套件 的 Prompt API 中新增的这两项功能,您将能够将这种低级智能体功能构建到您的应用中。

  • 不过,届时您需要自行完成很多工作。

  • 因此,您需要手动执行函数、保存调用历史记录,并自行循环执行多次调用。

  • 因此,如果您需要进行多次调用,就必须自行构建某种循环系统,并且必须为该系统构建自己的防护机制。

  • 虽然可以这样做,但我们当然希望这个过程快速而简单。

  • 因此,除了这个 Prompt API 库之外,我们还添加了一个新的智能体工作流库。

  • 您可以使用 com.mlkit.genai:agent-api 找到它,目前它处于 Alpha 版阶段。

  • 现在,借助这个新 API,我们可以为您完成大量繁重的工作。

  • 这样,您无需执行大量手动步骤,只需几行代码即可构建智能体。

  • 这正是我们今天要做的事情。

Jetpacker 智能体演示和函数调用

  • 接下来,我们将通过示例应用 Jetpacker 了解这些 API 的实际运作方式。

  • 现在,您已经在 Google I/O 大会和本次大会期间看到了 Jetpacker。

  • 不过,您可以扫描此二维码,如果您感兴趣,可以查看代码,将智能体指向代码,与代码互动,并详细了解我们如何实际实现所有这些功能。

  • Jetpacker 是一款现代旅行助理应用,我们开发这款应用主要是为了展示许多不同的智能功能。

  • 因此,它已经在使用设备端模型和云端模型来支持各种 Prompt API,但我们现在要构建的是为其添加一个智能体。

  • 对于 Jetpacker 这个智能体,我们将使用刚才提到的新 ML Kit GenAI 智能体 API,该 API 将使用在 Android 设备上运行的底层 AI Core 系统与 Gemini Nano 模型进行通信。

  • 现在,我可以畅所欲言,我们也会聊很多,但当然,我们希望展示这一点。

  • 现在,我们切换到手机,实际演示一下。

  • 我这里有一部 Pixel 11 Pro。

  • 我想我们可以切换到 Pro,您应该能看到它。

  • 好了,就是这样。

  • 如您所见,Jetpacker 应用正在运行。

  • 它显示了我们即将进行的一系列行程,我可以查看当前行程。在当前行程中,你可以看到今天是 10 月 29 日,所以我们有设备端 AI 研讨会、演示展示,然后明天我有金门湾游轮之旅,接下来还有博物馆参观。

  • 这些都很棒。

  • 现在,我们可以前往费用界面,在这里跟踪此行程的预算。

  • 如右下角所示,有一个新的智能体小闪光,这就是我们构建的设备端智能体。

  • 我们先从一个非常简单的命令开始。

  • 我想我们可以让它添加一笔特定支出。

  • 假设我们要添加一笔费用:打车去山景城,费用为 45 美元。

  • 就是这里

  • 它正在准备费用,并添加了费用。

  • 太棒了。

  • 好,演示 1 成功了。

  • 我们回到幻灯片,看看实际效果如何。

  • 首先,正如您所注意到的,我使用的是语音,而该语音实际上也是使用 Gemini Nano 模型,通过语音识别 API 将我所说的内容转写为文本。

  • 这段文本很合适,我们可以将其发送给 Gemini Nano 模型。

  • 但它会做什么呢?

  • 比如,我让它在支出中添加内容,告诉它 45 美元,但它并不知道该怎么做,对吧?

  • 此时,它只会告诉我:“我不知道你在说什么。”

  • 因此,我们需要对该提示进行一些补充。

  • 我们可以在提示中添加工具,可以添加各种工具,在本例中,我添加了获取行程预算、行程等。如果您通过我们的 API 将这些信息传递给 Nano 模型,它会自动理解,现在它能够返回函数调用,而不是返回文本。

  • 因此,在我们的示例中,它实际上会说:“好吧,我不会用文字回复,而是会添加一笔 45 美元的费用,用于支付前往山景城的出租车费。”

  • 这很好,但 Victoria,这具体是如何运作的呢?

  • 如何构建此系统?

  • 没错

  • 那么,如何在代码中定义工具呢?

  • 这就是我要解释的内容。

  • 因此,在 Android MCP 中,我们使用 Jetpack AppFunctions 库来定义和发布这些工具。

  • 因此,AppFunctions 库能够将应用的服务、数据和操作公开到 Android OS 注册表中,以便我们能够重复使用完全相同的功能来声明和描述工具,并在整个应用中使用它们。

  • 这样,用户还可以通过智能体和系统级互动来完成此任务。

  • 因此,我将通过代码给您举一个具体的例子。

    /**
     * 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,这意味着我们正在编写的这些函数仅对您的应用(即应用的代理)可见。

  • 之后,我们就可以实际定义和实现挂起函数本身,并且可以编写尽可能多的应用函数,并将它们全部归入一个服务类中。

构建和实现 AI 智能体运行程序

  • 好的。

  • 现在,我们已经有了工具,也可以编写这些工具,这很好。

  • 当然,要构建智能体,我们需要的不仅仅是这些 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 }
    
    }
  • 我们知道设备支持函数调用,然后就可以使用 Agent.builder 轻松构建智能体。

  • 然后,我们可以设置其名称。

  • 我们可以setInstructions来定义背景信息和角色。

  • 最后,我们调用 enableAppFunctions。

  • 因此,此函数将循环遍历并自动发现我们之前列出的所有 AppFunctions,而我们刚刚构建了智能体。

  • 接下来,我们需要初始化 AgentRunner。

  • 因此,这个智能体运行器将为我们管理执行循环。

  • 我们只需调用一个构造函数并将智能体传递给它即可。

  • 最后,我们可以将转写后的语音指令封装在内容块中,以创建提示。

  • 然后调用运行程序。

  • 我们对提示调用 runSingleTurnAsync 函数,这将启动智能体、运行推理并完成整个 AI 推理过程。

  • 如果我们只想将模型的最终文本回复流式传输到界面中,只需调用 textTokens,它就会在收到回复时收集回复。

  • 就是这样。

  • 这确实只有几行代码,所以我们之前说只需几行代码时,并不是在开玩笑。

  • 因此,只需这几行代码,您就拥有了一个智能体。

  • 该智能体将执行您想要的操作。

  • 它会给出文本回答。

  • 当然,在第一个演示中,我们只是执行了一个非常简单的命令,对吧?

  • 比如我发出一个指令,它执行一个操作,然后就结束了,所以它还不是一个真正成熟的智能体,对吧?

  • 所以,我们来增加一点趣味。

  • 接下来,我想发出一个指令,但我想让智能体稍微增加一点难度。

多步骤智能体架构和执行

  • 现在,我们回到演示设备,看看能否让它更难一点。

  • 我们已经到了。

  • 太棒了。

  • 所以,正如我所说,我们计划明天参观博物馆。

  • 对于博物馆参观,如果我打开它,您可以看到门票价格是 25 美元。

  • 现在,我们来看看智能体是否真的能够自行找出答案。

  • 现在,我返回到费用界面,然后发出指令:“将我的博物馆门票添加到费用中。”

  • 就是这里

  • 现在,如您所见,它正在检查我的行程,找到博物馆参观活动,并将其添加到我的费用中。

  • 因此,它在这里采取了三个步骤来实际实现其目标,并且还给出了最终的文本回答。

  • 同样,这一切都是自动完成的。

  • 您看到了该模型在设备上的实际运行速度。

  • 现在,我们回到幻灯片,看看其运作方式。

  • 如您所见,与第一个演示相比,我们做了一些调整。

  • 在本例中,如您所见,我没有告诉它博物馆的信息,也没有告诉它费用,因此,当模型收到提示和工具时,它会尝试决定先做什么。

  • 因此,在这里,它决定需要详细了解我的当前行程,因此它会调用 getItinerary。

  • 它会收到回复,然后就知道,哦,这个行程中有一个博物馆参观活动,这可能就是她所说的那个。

  • 现在,我们来查找该特定事件的详细信息。

  • 在特定事件的所有其他元数据中,它会看到金额为 25 美元。

  • 它理解了,然后返回,并理解它需要调用“添加支出”工具。

  • 在这里,我们可以看到多个步骤依次发生。

  • Victoria,您能详细说明一下这方面吗?

  • 没错

  • Jolanda 刚才描述的是她刚刚展示的演示的具体示例,但在这里,我将把它带入一个更具概念性的抽象层。

  • 因此,每次执行多步函数调用时,都会发生这种情况。

  • 首先,这是我们的应用,这是 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 函数明确表示它仅返回事件的高级摘要。

  • 因此,我们还提到,如果想要获取更具体的行程详情,我们需要使用 eventId 调用 getEventDetails。

  • 然后,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
     }
    }
  • 它看起来与单步示例非常相似,只是这里增加了两项内容。

  • 首先,我们需要设置最大轮数,在本例中,我们将其设置为 5。

  • 那么,它有什么作用呢?

  • 这样可以防止模型陷入无限循环。

  • 假设提示不明确,或者它不断在调用一个工具和另一个工具之间来回切换,那么如果我们设置了此参数,它就会停止,而不会一直循环。

  • 然后,我们仍然调用同一个函数 runSingleTurnAsync 来运行它。

  • 因此,虽然它被称为单轮,但实际上,一轮实际上意味着一个用户查询。

  • 因此,最终用户的期望是,我发送一个查询,你返回一个答案,而无需真正关心底层发生了什么,比如循环了多少次,等等。

  • 因此,我们可以继续使用同一函数。

  • 此外,由于多步工具调用需要更多时间,您在演示中也看到了,它实际上会显示此时此刻正在调用的函数、此时此刻正在执行的操作以及是否已完成。

  • 因此,我们希望在界面中显示实时进度。

  • 因此,我们不仅可以收集文本回答,还可以在 AgentEvent 发出 FunctionCall 或 FunctionResponse 时显示工具名称 event.name,并在完成后像以前一样返回 TextMessage。

管理多轮对话会话

  • 很好!

  • 因此,我们将这种运行方式称为异步单轮运行,这当然意味着还有另一种 API,对吧?

  • 没错

  • 因此,仅执行一个步骤当然是不够的。

  • 我们希望执行多步操作。

  • 接下来,我们来试试多轮对话。我想我们可以回到我的演示,让它变得更难一些。

  • 我们刚才讨论了如何安排博物馆参观,但我们还有金门湾游轮活动。

  • 所以,对于这个项目,我想在实际预订之前先弄清楚我的预算是否足够。

  • 我们来提个问题。

  • 我们来看看。

  • 我打算明天带团队去金门湾游轮,我以为价格大概是 180 美元。

  • 如果我这样做,那么我的剩余预算实际上是多少?

  • 那么,它在做什么呢?

  • 它回答了我的剩余预算是多少。

  • 因此,它了解当前的行程。

  • 它有这些信息,可以告诉你。

  • 不过,我也可以在这里提出后续问题。

  • 好的,这很好。

  • 你能把它添加到我的支出中吗?

  • 好了,现在可以继续了。

  • 哦,它需要货币,太酷了,演示效果。

  • 好的,所以是以美元为单位。

  • 180 美元。

  • 就是这里

  • 这是第一次发生这种情况,太酷了。

  • 好吧,那它在做什么?

  • 它告诉我,这次对话进行了三轮,并已计入费用。此外,它还允许我刷新此会话。

  • 顶部有一个类似刷新按钮的图标,点击它即可清除完整历史记录,重新开始。

  • 现在,我们可以回到幻灯片了。

  • 我们已经有了可运行的演示,它们都能实现我们想要的效果,所以我想我们可以回到代码部分。

  • 没错

  • 这真的非常棒。

  • 您会注意到,Jolanda 没有提及,只是说,哦,这是美元,她没有说这是金门湾游轮票。

  • 那么,模型如何知道所有上下文?

  • 这与我在前两个代码段中展示的内容略有不同。

    // 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。

  • 因此,我们在这里使用 tripId 调用 runner.getOrCreateSession。

  • 那么,为什么需要创建会话?

  • 因为 runSingleTurnAsync 会重置记忆,所以它不会有上下文。

  • 我们将无法进行多轮对话。

  • 因此,当您想要进行多轮对话时,我们会使用会话,即智能体会话。

  • 然后,我们传递 tripId。

  • 为什么需要这样做?

  • 您之前已经见过 Jetpacker 应用,它可管理多个行程。

  • 有 ADS 行程、巴黎行程,可能还有东京行程。

  • 因此,我们希望将上下文保留在行程中。

  • 我们不想互相干扰,对吧?

  • 因此,我们不调用 runSingleTurnAsync,而是使用 runAsync。

  • 所以,这与以往不同。

  • 因此,此操作在会话中运行,然后我们仍然会看到相同的提示。

  • 然后,我们仍然可以像以前一样发出 AgentEvent、FunctionCall、FunctionResponse 和 TextMessage,它们是相同的。

  • 当 Jolanda 点击“新对话”按钮(即刷新按钮)时,我们实际上可以调用 session.delete。

  • 没错

  • 好的。

  • 我们通过新的智能体 API 实现了所有这些功能,这非常棒,并且在许多应用场景中都能发挥出色作用。

  • 当然,我们始终希望让简单的事情变得更简单,但我们也希望让您能够处理更复杂的情况。

  • 因此,我们希望确保,如果您需要,您仍然可以使用直接访问 API 来构建这些内容的更低级别版本。

  • Victoria,您能否解释一下,直接访问权限级别是什么意思?在什么情况下,我们会再次降到这个级别?

高级 Agentic Workflows 和工具集成

  • 好的,没问题,Jolanda。

  • 您可能已经熟悉了 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 来实现这一点。

  • 我们对历史记录拥有完全控制权,我们将上下文 token 保存在设备端,因为设备端的 token 窗口更有限,这样我们可以节省更多 token。

  • 第二个用例实际上是另一个极端。

  • 因此,如果我们的应用场景比较复杂,智能体 API 尚不支持,例如,如果我们需要多智能体系统,其中有多个子智能体相互交互来解决复杂任务,或者需要混合系统,其中既有设备端模型,也有云端模型,或者我们需要自定义记忆和上下文压缩。

  • 这与历史记录有关,就像我之前提到的对历史记录的完全控制一样。

  • 假设我们想使用直接访问 API,因为上下文窗口有限,我们不想把所有内容都发回模型,只想总结最重要的部分,或者最新的部分,这样我们就能完全控制。

  • 对于此类用例,我们可以使用智能体开发套件 (ADK) 或 Crew 等框架,也可以创建自己的自定义编排框架。

  • 好的。

  • 以上就是关于智能体 API 的全部内容。

  • 不过,我们还想谈谈另外一件事。

  • 我们一直在讨论应用内智能体,也就是在自己的应用中构建智能体体验,但我们也讨论了如何使用 AppFunctions。如果您的所有操作都在自己的应用内进行,为什么还要使用 AppFunctions 呢?

  • 我们知道,像您这样的开发者并不喜欢重复劳动,对吧?

  • 我们不喜欢复制粘贴代码,也不喜欢重复任何业务逻辑。

  • 因此,借助 AppFunctions,我们只需进行非常简单的更改,就能让系统智能体也能使用我们的工具,而不仅仅是让应用内智能体使用。

  • 那么,这在代码中是如何体现的呢?

  • 没错

  • 因此,借助 AppFunction,我们只需编写一次,然后就可以根据需要将其公开给不同级别的层。

  • 您可能还记得,之前我们使用的是 ACCESS_LEVEL_SELF,它只会向应用内代理公开我们编写的函数,但我们也可以将该访问权限级别更改为 ACCESS_LEVEL_ANDROID_TRUSTED。

  • 这样,Android OS 中的可信智能体就能使用这些函数。

  • 因此,您无需重写函数、命令或数据库逻辑。

  • AppFunctions 已经充当应用对内和对外的语音接口。

  • 我们也想看看这方面的演示,对吧?

  • 好的,我们回到手机上,在最后一个演示中,我们退出 Jetpacker 应用。

  • 我们可以退出,然后进入 Gemini。

  • 因此,在 Gemini 中,我们可以调用 @Jetpacker。

  • 现在,需要注意的一点是,此功能目前仅面向少量用户提供 Beta 版,因此并非所有人都能在今天使用,但您可以测试并试用。

  • 我们来看看 Jetpacker。

  • 我该怎么做?

  • 哦,我今天早上喝了咖啡,对吧?

  • 请在 ADS 行程费用中添加 7 美元的咖啡费用。

  • 就是这里

  • 您可以看到,它连接到了 Jetpacker 应用。

  • 大功告成!

  • 它帮我添加了这笔支出。

  • 我们基本上已经了解了如何使用 Jetpack 中的所有内容。

  • Jetpacker 的一个优点是,我们还可以开启飞行模式。

  • 我们今天还没有真正展示过这一点,但我们可以直接开启飞行模式,然后演示中看到的所有功能,我想展示一下,因为我确实想确保大家信任我,所以我们把 Wi-Fi 关掉。

  • 有没有人能提供一个很好的演示示例,比如我可以添加的一项费用?

  • 我们来看看。

  • 也许我们可以做《What is it?》

  • 哦,租车服务应该不错,对吧?

  • 好的。

  • 我们来试试“添加一笔 300 美元的租车费用”。

  • 好了,它已经识别出来了。

  • 好的。

  • 太棒了。

  • 好的,我们回到幻灯片。

  • 我们刚才讨论了 AppFunctions。

  • 我知道我们只是非常、非常粗略地提到了这一点。

  • 如果您想了解更多信息,还可以观看 Dan 和 Sarah 的另一场演讲,他们会更详细地介绍如何实际启用此功能、如何进行测试、我们有哪些工具等。

  • 今天我们介绍了很多不同的内容。

  • 我们已经了解了单步、多步和多轮对话,最后还了解了如何使用系统 AppFunction。

  • 现在,我想在结束之前分享一些提示。

最佳实践和结论

  • 没错

  • 因此,在开始构建智能体体验时,请牢记这三个提示。

  • 第一个是保持工具集的专注度。

  • 因此,请记住,设备端模型由于规模较小,其功能有限。

  • 因此,与其注册数十个工具,不如选择较少的工具。

  • 正如我之前所说,请编写清晰的 KDoc 说明,因为模型会根据这些说明来决定要返回的内容。

  • 然后,我们还应该设置最大转向护栏。

  • 这还可以防止模型在混淆时陷入无休止的循环。

  • 这意味着我们可能会耗尽用户的电池电量。

  • 因此,如果我们有这个功能,就可以节省用户的电池电量。

  • 最后,我们需要检查设备是否已支持函数调用,并实现优雅降级。

  • 例如,如果不支持,您可以参考云端模型或标准界面。

  • 好的,我想我们可以利用我们的资源来完成这项工作。

  • 我再展示一次二维码。

  • 请查看。

  • 我认为需要注意的一点是,我们还为此提供了 Android Skills。

  • 因此,您无需深入探究每一个细节。

  • 我们提供了完整的文档和 Android 技能,可与这个新的智能体 API 搭配使用。

  • 请务必查看。

  • 请下载 AICore 开发者预览版,以便测试所有这些功能。祝您编码愉快!

代码

代码段

“Jetpacker 代理演示和函数调用”中 09:53 处的 Kotlin 代码段
/**
 * 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...
}
“构建和实现 AI 智能体运行程序”中 11:41 处的 Kotlin 代码段
val model = Generation.getClient()
val features = model.getAvailableFeatures()
if(FUNCTION_CALLING in features) {
  // Device is supported!
} else {
  // Graceful fallback
}
“构建和实现 AI 智能体运行程序”中 12:07 处的 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 }

}
“多步智能体架构和执行”中 17:23 处的 Kotlin 代码段
/**
* 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 { ... }
“多步智能体架构和执行”中 18:23 处的 Kotlin 代码段
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
 }
}
“管理多轮对话会话”中 22:13 处的 Kotlin 代码段
// 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()
“Advanced agentic workflows and tool integration”(高级智能体工作流和工具集成)中 25:02 处的 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()
)