软件工程师
通过构建自主设备端智能体,让 Android AI 功能更上一层楼。在本讲座中,我们将深入探讨机器学习套件的新智能体 API,这些 API 直接基于您已经熟悉的直接模型访问 API 构建。
您将使用开源 Jetpacker 应用,学习如何跨本地数据串联多步操作、如何通过隔离的上下文管理有状态的多轮会话,以及如何连接 Android MCP,从而通过单个代码库同时为应用内助理和 Android 系统级界面提供支持。
AgentRunner 负责协调工具执行,并提供会话方法,以便在不丢失上下文的情况下进行多轮对话。转写文稿
我叫 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 了解这些 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,这意味着我们正在编写的这些函数仅对您的应用(即应用的代理)可见。
之后,我们就可以实际定义和实现挂起函数本身,并且可以编写尽可能多的应用函数,并将它们全部归入一个服务类中。
好的。
现在,我们已经有了工具,也可以编写这些工具,这很好。
当然,要构建智能体,我们需要的不仅仅是这些 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,您能否解释一下,直接访问权限级别是什么意思?在什么情况下,我们会再次降到这个级别?
好的,没问题,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 开发者预览版,以便测试所有这些功能。祝您编码愉快!
/**
* 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()
)