ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Google ADK for Kotlin 实战:Android 设备端 Agent 开发指南

2026/9/19 5:31:19 拓冰建站 浏览量
Google ADK for Kotlin 实战:Android 设备端 Agent 开发指南 1. 从 Gemini 到 ADKGoogle 为什么把 Agent 框架搬进安卓1.1 ADK for Kotlin 和 Python 版的分工前两年做移动端 AI 应用最头疼的一件事就是一个真正会干活的 Agent始终是云端专属。客户要的不是一个聊天机器人而是能自己判断、自己调用工具、自己完成多步骤任务的智能体。这种能力在服务端已经有不少成熟方案但在 Android 端想要把 Agent 跑在设备里基本只能自己拼Prompt 编排、Function Calling 封装、上下文管理、状态持久化……每一层都得自己造轮子而且造出来的东西还未必能扛住真实场景。Google 在 2025 年的 I/O 大会上正式发布的Agent Development KitADK就是来解决这个问题的。它是一套第一方 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 依赖直接嵌入状态与记忆外部数据库或内存管理设备端 MemoryManager1.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:nameandroid.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 MutableStateFlowListAgentEvent(emptyList()) val agentEvents: StateFlowListAgentEvent _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 层比我想象中简单。因为事件已经打包成 StateFlowCompose 里用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 schemaEmptyInput() ) { _: 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 工具返回与调用循环工具定义好之后还有一个隐性的调用循环需要理解清楚。当模型决定调用工具时流程是这样的模型输出一个REQUEST_CONTENT事件里面包含要调用的工具名和参数ADK runtime 在本进程内执行对应的工具处理函数工具返回值被包装成 JSON 结构作为上下文追加到会话里模型拿到工具返回结果后继续推理最终生成面向用户的答案这个循环体现在事件流里就是LOCAL_TOOL_START和LOCAL_TOOL_END。我调试时踩过一个坑工具返回的结构如果不够规范模型会听不懂。比如你返回一段自由文本模型可能要把文本重新解析一遍才能用容易产生幻觉。正确做法是返回结构化 JSON 键值对让模型拿到即用。上面代码里的datetime、timezone、weekday三个字段就是一次成型模型直接引用即可。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 工具包的接入与边界最近社区里 MCPModel 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 定义 工具 事件流 记忆这套骨架是稳定的理解透这套骨架接口怎么变都不慌。