Google ADK for Kotlin 实战:Android 设备端 Agent 开发指南
2026/9/19 5:30:48 网站建设 项目流程

1. 从 Gemini 到 ADK:Google 为什么把 Agent 框架搬进安卓

1.1 ADK for Kotlin 和 Python 版的分工

前两年做移动端 AI 应用,最头疼的一件事就是"一个真正会干活的 Agent"始终是云端专属。客户要的不是一个聊天机器人,而是能自己判断、自己调用工具、自己完成多步骤任务的智能体。这种能力在服务端已经有不少成熟方案,但在 Android 端,想要把 Agent 跑在设备里,基本只能自己拼:Prompt 编排、Function Calling 封装、上下文管理、状态持久化……每一层都得自己造轮子,而且造出来的东西还未必能扛住真实场景。

Google 在 2025 年的 I/O 大会上正式发布的Agent Development Kit(ADK),就是来解决这个问题的。它是一套第一方 Agent 开发框架,分两个版本:ADK for Python面向服务端,可以部署到 Cloud Run、Vertex AI 或者自建环境;ADK for Kotlin则是把整套 Agent 运行时搬进了 Android 进程,开发者用 Kotlin DSL 定义 Agent、工具、记忆和事件回调,让 Agent 在设备端自主完成多轮推理和工具调用。

这套分工逻辑其实很清晰:Python 版负责重活儿、大并发、复杂的多 Agent 编排;Kotlin 版负责"贴身"的场景——数据在设备上、响应要够快、隐私要可控、甚至断网也要能用。两者不是替代关系,而是按场景选型。

对比项ADK for PythonADK for Kotlin
运行环境服务端(Cloud Run / 容器 / 自建)Android 进程内
模型接入Gemini 及第三方模型Gemini 系列为主
典型场景高并发 SaaS、复杂编排、后台任务设备端助手、隐私敏感场景
与 App 集成方式通过 API/REST 暴露给客户端以 SDK 依赖直接嵌入
状态与记忆外部数据库或内存管理设备端 MemoryManager

1.2 设备端 Agent 的三个不可替代场景

为什么非要把 Agent 放到设备端?我实际做下来,体会最深的是这三个场景,云端方案怎么都绕不过去。

第一是隐私敏感数据。健康、通讯录、位置这类数据,用户天然不愿意传到服务器上。设备端 Agent 可以直接读取本地数据并完成推理循环,整个过程中原始数据不出设备,只把必要的推理请求发给模型服务。这对很多行业应用来说是上线的硬性前提。

第二是低延迟交互。Agent 要跟用户像真人助理一样对话,来回延迟超过一两秒就很出戏。设备端方案里,Agent 的决策循环、工具调用、结果拼装都在本机完成,只有模型推理本身依赖网络,整体交互的流畅度明显比把完整 Agent 生命周期放到云端再绕回来高一截。

第三是结合设备能力的自动化。日历、闹钟、剪贴板、传感器、本地数据库,这些能力天然在设备上,云端 Agent 想调用还得绕一道接口。ADK for Kotlin 里,工具直接写在 App 进程内,Agent 说"帮我查一下明天上午有没有空",工具函数就能直接查本地日历数据库,零网络往返。

2. 拆解 ADK for Kotlin 的运行时骨架

2.1 AgentRuntime:全局唯一的调度中枢

开始写代码之前,得先把 ADK for Kotlin 的运行时模型搞清楚。我第一次看文档的时候被一堆抽象概念绕晕了,后来自己画了一遍调用关系才明白,核心就是AgentRuntime这个单例。

AgentRuntime 是整个框架的调度中枢,负责管理 Agent 的生命周期。会话启动、消息发送、事件分发、并发控制全都要经过它。文档里说的"runtime"不是虚的——每条用户消息进来,都由 runtime 分配给对应的 Agent 会话,再把 LLM 推理、工具调用、事件回调这些环节串起来。官方设计里用AgentRuntime.getSingleton()获取实例,全局唯一,这也意味着一个 App 进程里可以同时跑多个 Agent,由 runtime 统一协调,而不是每个 Agent 各自为政。

我在实践中的理解是:Agent 是"定义",runtime 是"执行"。Agent 描述了一个智能体是什么样——叫什么、用什么模型、有什么工具、系统提示词是什么;runtime 负责让这个定义真正跑起来。这个思路跟服务端框架里的"路由 + 处理器"很像,只不过 Agent 的处理逻辑不是固定的函数,而是模型驱动的动态决策。

2.2 Agent 与 Tool 的定义方式

ADK for Kotlin 对 Agent 的定义走的是 Kotlin DSL 风格。最基础的LlmAgent只需要三个要素:名字、系统提示词、绑定的模型。比如:

val assistantAgent = agent { name = "assistant" instruction = "你是一个生活助手,用简体中文回答用户问题。" model = gemini2Flash() }

agent {}这个 DSL 块会在编译期帮你做很多校验,比如工具函数必须实现对应的输入输出格式、模型名称必须是已注册的模型类型等。model = gemini2Flash()这种写法背后是框架内置的模型工厂,对应 Gemini 2.5 Flash。除了 Flash,还支持 Pro 级别的模型,按场景选就行。

工具的定义则是通过tool()DSL,本质是实现了Invocable接口的类。一个工具包含三样东西:名字(模型用来识别的唯一标识)、描述(模型判断何时调用这个工具的依据)、处理函数(实际执行的逻辑)。这个设计跟 Function Calling 的通用模型是一致的,只是 ADK 帮你把注册、校验、上下文传递这些脏活都封装好了。

2.3 事件流:观察 Agent 的"思考过程"

这是 ADK for Kotlin 里我觉得最实用的设计——AgentEvent 事件流。Agent 不是黑盒,它在每次推理循环中产生的关键节点都会以事件的形式对外暴露。我用的版本里主要的事件类型有这些:

  • GENERATED_CONTENT:模型生成了一段内容
  • REQUEST_CONTENT:模型发出了调用工具的请求
  • LOCAL_TOOL_START/LOCAL_TOOL_END:某个本地工具开始/结束执行
  • AGENT_COMPLETE:整个 Agent 会话的一次完整响应结束

事件流的意义在于,UI 层可以非常直观地把 Agent 的"思考过程"呈现给用户:它先说了什么、调用了哪个工具、工具返回了什么、最终结论是什么。我做 demo 的时候直接把这些事件按顺序打在界面上,效果相当直观。而且事件回调是流式的,消息是一段段冒出来的,配合 Compose 的状态管理非常顺手。

3. 写一个能跑的最小 Agent:从依赖到界面

3.1 依赖引入与项目配置

说再多概念,不如先跑通一个 Hello World。ADK for Kotlin 以 Maven 依赖的形式发布,包名是com.google.android.adk:adk-toolkit。在build.gradle.kts里加一行:

dependencies { implementation("com.google.android.adk:adk-toolkit:1.0.0") }

需要注意几个前置条件。Android 项目的minSdk建议至少 26 以上,因为框架内部用到了较新的 Java/Kotlin 特性。同时要在 Manifest 里声明网络权限,Agent 要跟模型服务通信,这个是刚需:

<uses-permission android:name="android.permission.INTERNET" />

模型 API Key 的配置是个关键点,我的建议是不要硬编码。可以放到local.properties里,通过 BuildConfig 注入,或者用开源项目的BuildKonfig方案统一管理。总之千万别把 Key 提交到代码仓库,我做项目习惯在.gitignore里把含 Key 的文件都排掉。另外,在使用前要确认测试设备可以正常访问 Google 的模型服务接口,否则 Agent 会一直在初始化阶段超时——这个问题我在后面踩坑部分会详细说。

3.2 最小 Agent 代码逐行解释

跑通最小 Agent,核心就三件事:创建 Agent、启动 runtime、发消息并监听事件。下面这段代码里,我加了很多注释,照着看就行:

class AgentViewModel : ViewModel() { // runtime 全局唯一,先拿到单例 private val runtime = AgentRuntime.getSingleton() // 用 DSL 定义 Agent private val agent = agent { name = "life_assistant" instruction = """ 你是运行在安卓设备上的智能助理。 回答请使用简体中文,尽量简洁直接。 如果用户询问的设备相关信息,调用对应的本地工具获取。 """.trimIndent() model = gemini2Flash() } // 把所有事件收集到一个 StateFlow 里供 UI 层使用 private val _agentEvents = MutableStateFlow<List<AgentEvent>>(emptyList()) val agentEvents: StateFlow<List<AgentEvent>> = _agentEvents.asStateFlow() fun sendMessage(text: String) { viewModelScope.launch { // 每次对话前启动一个新的会话 runtime.startSession(agent) // 发送消息,事件通过回调流式返回 runtime.sendMessage(agent, text) { event -> _agentEvents.update { it + event } } } } }

这段代码里最值得说的是sendMessage的第二个参数。它不是一个"返回最终结果"的阻塞调用,而是一个事件回调。你发出去的每条消息,Agent 内部可能经历多次"模型生成 → 调工具 → 再生成"的循环,每次循环的关键节点都会回调一次。这种设计对流式 UI 特别友好,你在界面上能实时看到 Agent 的完整动作,而不是干等一个最终字符串。

startSession也很关键。每次开启新的对话之前调用它,相当于告诉 runtime:给这个 Agent 开一个全新的会话上下文。这样多轮对话之间不会串场。如果想做真正的多轮连续对话,后面记忆那节会提到怎么配置。

3.3 Compose 里的调用姿势

业务代码写完之后,UI 层比我想象中简单。因为事件已经打包成 StateFlow,Compose 里用collectAsStateWithLifecycle收集就行。我实际界面长这样(简化版):

@Composable fun AgentScreen(viewModel: AgentViewModel) { val events by viewModel.agentEvents.collectAsStateWithLifecycle() var input by remember { mutableStateOf("") } LazyColumn { items(events) { event -> when (event) { is AgentEvent.GeneratedContent -> MessageBubble(event.content) is AgentEvent.LocalToolEnd -> Text("工具执行完成:${event.toolName}", style = MaterialTheme.typography.bodySmall) } } } Row { TextField(value = input, onValueChange = { input = it }) Button(onClick = { viewModel.sendMessage(input); input = "" }) { Text("发送") } } }

一个小建议:AgentEvent.GeneratedContent里拿到的内容如果是流式的,可以在界面上做一个"正在生成"的动画指示器。体验上会有很大提升,否则用户看到一堆事件突然冒出来会很困惑。

4. 工具函数实战:把 Agent 从聊天机器人变成"干活的人"

4.1 定义一个查询类工具的完整流程

跑通空白 Agent 只是第一步,真正让 Agent 有价值的,是工具。我以一个"查询设备当前时间"的工具为例,完整走一遍定义流程。工具的核心是给模型一个可调用的"函数",模型在推理过程中判断该不该调、怎么传参。

val getLocalTimeTool = tool( name = "get_local_time", description = "获取设备当前的时间、时区和星期信息。当用户询问现在几点、日期、时区时使用。", inputSchema = schema<EmptyInput>() ) { _: EmptyInput, _: ToolContext -> val now = LocalDateTime.now() json { "datetime" to now.toString() "timezone" to ZoneId.systemDefault().id "weekday" to now.dayOfWeek.toString() } }

name必须是机器可读的英文标识,模型靠它精准匹配工具;description是给模型看的中文说明,它决定了模型在什么场景下触发这个工具。很多新手会忽略 description 的重要性,以为随便写写就行——实际上description 属于提示词的一部分。同样是查询时间的工具,description = "查询当前时间"description = "当用户问日期、星期、时区等时间相关信息时使用",模型的触发准确率完全不是一个级别。我一般会把触发条件和典型问法都写进去。

4.2 工具返回与调用循环

工具定义好之后,还有一个隐性的调用循环需要理解清楚。当模型决定调用工具时,流程是这样的:

  1. 模型输出一个REQUEST_CONTENT事件,里面包含要调用的工具名和参数
  2. ADK runtime 在本进程内执行对应的工具处理函数
  3. 工具返回值被包装成 JSON 结构,作为上下文追加到会话里
  4. 模型拿到工具返回结果后继续推理,最终生成面向用户的答案

这个循环体现在事件流里就是LOCAL_TOOL_STARTLOCAL_TOOL_END。我调试时踩过一个坑:工具返回的结构如果不够规范,模型会"听不懂"。比如你返回一段自由文本,模型可能要把文本重新解析一遍才能用,容易产生幻觉。正确做法是返回结构化 JSON 键值对,让模型拿到即用。上面代码里的datetimetimezoneweekday三个字段就是一次成型,模型直接引用即可。

4.3 工具设计的三条经验

第一个 Demo 跑通之后,我又加了几个工具,总结出三条非常实在的经验。

经验一:description 要写"触发场景",不要写"函数逻辑"。模型不是编译器,它不关心你函数内部怎么实现,它只想知道"什么时候该用你"。描述里包含触发条件、典型问法、使用边界,触发率会明显提升。

经验二:工具内部要做防御。设备的真实状态往往跟模型预设的前提不一致。比如用户问"帮我查一下最近的咖啡店",模型可能直接把用户的经纬度参数传给你,但你的工具根本收不到定位——因为还没授权。这时候工具要主动返回一个明确的错误信息,让模型能够"下台阶",去引导用户授权。我在工具里统一用error字段返回错误原因,模型看到后会自动组织话术。

经验三:别贪多,工具越少越好。工具列表每多一个,模型决策空间就大一分,选错工具的概率也大一分。我会把高频的、强相关的工具优先暴露,低频的单独做一个"更多功能"入口再动态挂载。

5. 对话记忆、多模态与 MCP 技能扩展

5.1 MemoryManager 与多轮记忆

前面startSession创建的会话是"单次独立"的,如果想做真正的连续对话——用户上一轮说"帮我设个晚上七点的提醒",下一轮说"改成八点"——就需要记忆能力。ADK for Kotlin 提供了一个MemoryManager组件专门管这件事。

val configuration = sessionConfig { memoryManager = MemoryManager( maxConversationMessages = 10 ) } runtime.startSession(agent, configuration)

maxConversationMessages是控制上下文窗口的参数。不是越多越好:消息越多,传给模型的 token 越多,推理延迟越高,费用也越高。我自己的经验是,移动端场景 10 条左右比较合适,既能覆盖常见多轮对话的上下文需求,又不会让首字延迟明显变长。如果要跨越 App 重启保持记忆,还需要把记忆持久化到本地数据库,ADK 框架内提供了扩展点,不过这块我还在摸索,等稳定了再单独写。

5.2 多模态输入的接入方式

多模态是"ai agent 多模态"这类热词背后最实际的诉求。ADK for Kotlin 对这类输入的处理逻辑其实很直接:消息本身可以带附件。用户拍一张照片,问"这上面的营养成分表帮我算一下总热量",这条消息除了文本之外还关联了一张图片。

接入方式是在发送消息时把图片的 URI 或者二进制数据放到请求的附件位。模型服务端识别出图片内容后,会结合文本指令做推理。这个能力在本地工具链上是个放大器——比如结合相机拍照、结合相册选图、结合 OCR 工具,Agent 能干的活儿一下子多出很多。

不过多模态也要注意一个现实问题:图片会显著拉长上下文。我建议在发图前先在设备端做压缩,把分辨率压到合理的范围,再去请求模型,能省不少 token 和时间。

5.3 MCP 工具包的接入与边界

最近社区里 MCP(Model Context Protocol)的讨论非常多,ADK 对 MCP 也做了支持。简单理解,MCP 是一种标准化的工具协议,让 Agent 可以复用第三方 MCP 服务器暴露的工具集,不用每个服务商都重新写一套适配。在 ADK for Kotlin 里可以通过 MCP 工具包,把远程 MCP 服务器上的工具挂载到本地 Agent 上。

它的价值在于生态复用,但我要给个明确提醒:移动端接入远程 MCP 要克制。MCP 服务器在远端,每次工具调用就是一次完整网络往返,移动网络的延迟、断网、超时都会直接影响 Agent 的完成质量。我自己的取舍是:高频核心能力用本地工具,低频长尾能力才考虑 MCP 远程挂载。别为了"看起来时髦"把所有工具都怼到 MCP 上,实测对体验伤害很大。

6. 实战中的坑与排查清单

6.1 密钥与网络问题——首发命中率最高的问题

我第一天跑 demo,遇到最典型的两个报错:一个是PERMISSION_DENIED,一个是超时。前者基本是 API Key 配置不对,可能没放到正确的位置,或者 Key 的服务没启用对应的模型;后者十有八九是网络连通性问题。排查的时候我建议按这个顺序来:先确认 Key 在服务端控制台的生效状态,再确认模型名称拼写正确,最后确认设备和网络环境能正常访问 Google 模型服务接口。大多数情况下,前两步就能解决掉 80% 的报错。

另外推荐一个超实用的调试姿势:把发给模型服务的原始请求和响应打到 logcat 里,提前定位是参数问题还是网络问题,比对着堆栈猜快得多。

6.2 协程与生命周期冲突

ADK 的sendMessage是挂起函数,事件回调在内部协程里执行。如果直接把它丢到 GlobalScope 里跑,App 一进后台协程还在跑,轻则浪费流量,重则崩溃。我踩过一次:用户发完消息立刻退出界面,协程里还要更新 StateFlow,结果界面销毁后继续写入,导致状态异常。

正确做法是把 Agent 调用绑定到 ViewModel 的viewModelScope上,或者干脆在onStop里取消掉还在进行的请求。对交互类 Agent,我倾向于一种折中设计:用户离开界面时取消当前会话,但保留会话 ID,下次回来可以恢复上下文重新开始,不用每次都从零开新会话。

6.3 模型输出格式不稳定

Agent 的本质依然是 LLM,输出永远有概率不稳定。你会发现有时候工具调用参数传错了、有时候 JSON 输出多了几个说明文字、有时候模型干脆在"想"而不是"答"。针对这个问题,我总结出一个三层兜底策略:

  • 校验层:工具输入解析失败时不要崩溃,返回结构化错误信息让模型自纠。
  • 重试层:对关键任务设置一次温和的重试,给模型第二次机会。
  • 回退层:如果连续几次循环模型都没有完成有效输出,Agent 主动告知用户"这个问题我暂时处理不了",而不是无限循环空转。

这个兜底策略让我的 demo 从"时好时坏"变成了"稳定可用"。Agent 开发里,处理模型输出的异常分支,跟处理正常路径一样重要

6.4 内存、电量与冷启动优化

设备端 Agent 常驻内存这件事,在低端机型上尤其要小心。每次模型推理的上下文、工具返回的临时数据,都会占用不少内存。我的优化经验是:

  • 会话结束及时释放上下文引用,不要长期持有大对象
  • 图片类的多模态输入用完立即清理中间缓存
  • 长时间不用的 Agent 会话主动调用stopSession,避免后台常驻
  • 电量方面,Agent 请求会唤醒网络和计算单元,我做了个简单的"节流"——连续对话时限制请求频率,用户停止输入 2 秒后才发送,避免频繁的无效推理

从冷启动的角度,AgentRuntime的初始化其实比较重,第一次实例化会有明显的延迟。所以我把初始化放到了Application.onCreate里预加载一次,而不是用户点击对话框时才创建。这样首轮对话的响应速度能快不少。

最后分享一个我一直在用的工作方法:开发阶段先在模拟器上跑通核心逻辑,再上真机调性能。模拟器的网络环境相对稳定,适合做功能验证;真机上的功耗、延迟、内存才是真实场景。ADK for Kotlin 的优势在于它把整套 Agent 能力做成了标准的安卓 SDK 组件,你能用常规的 Android 开发手段去调试、监控、优化它——这也是我愿意在新项目里持续使用它的原因。现在这个框架还在快速迭代,接口细节每次升级可能都有微调,但核心的"运行时 + Agent 定义 + 工具 + 事件流 + 记忆"这套骨架是稳定的,理解透这套骨架,接口怎么变都不慌。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询