ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

ADK for Kotlin 实战:KSP 注解与协程构建 Android AI Agent

ADK for Kotlin 实战:KSP 注解与协程构建 Android AI Agent 1. 为什么 ADK for Kotlin 值得每一个 Android 开发者认真看一眼Google 把 Agent Development Kit 的 Kotlin 版本放出来这件事在 Android 圈子里其实比想象中更有分量。过去一年大家都在聊 AI Agent但真正落到移动端、能用 Kotlin 写得顺手、还能和现有 Android 工程无缝共存的方案少得可怜。大部分教程要么是 Python 生态的 LangChain、AutoGPT 那一套要么是云端服务调个 API 就完事跟 Android 开发者日常写的东西隔了一层。ADK for Kotlin 的出现等于把 Agent 的开发范式直接搬进了 Kotlin 世界你可以用协程、用 KSP、用 Gradle 插件像写普通 Android 库一样去构建一个能思考、能调用工具、能维护记忆的智能体。这个内容适合谁如果你已经会写 Kotlin做过 Android 或者后端 Kotlin 服务想搞清楚 AI Agent 到底怎么落地那这篇就是给你准备的。如果你只是听说过 Agent 但没动过手也没关系我会从最基础的概念开始拆把 ADK 的核心构件、KSP 注解处理、工具注册、记忆管理这些环节全部讲透。读完你至少能做到两件事第一明白 ADK for Kotlin 的架构设计为什么长这样第二自己动手跑通一个能调用本地工具、能记住上下文的最小 Agent。需要提前说明的是ADK 本身还在快速迭代Kotlin 版本相对 Python 版本年轻不少有些 API 可能过几个月就变了。所以我在讲具体写法的时候会更侧重设计思路和关键机制而不是死记某个函数签名。你理解了原理后面版本怎么变都能跟上。2. ADK for Kotlin 的整体设计与核心思路拆解2.1 Agent 到底是什么为什么 Kotlin 需要一个专门的开发套件先把概念对齐。所谓 AI Agent你可以把它理解成一个会自己决定下一步做什么的程序。普通的函数调用是你写死逻辑先查数据库再拼字符串再返回。Agent 不一样你给它一个目标它自己判断该调用哪个工具、该不该追问、要不要记住刚才的结果。这个判断的过程通常由大语言模型驱动但模型本身不会帮你管理工具、不会帮你维护对话历史、不会帮你处理并发这些脏活累活就是 ADK 要解决的。那为什么不用 Python 那套因为 Kotlin 开发者的工程环境完全不同。Android 工程有严格的构建流程、有 KSP 注解处理、有协程调度、有 ProGuard 混淆你硬塞一个 Python 运行时进去根本不现实。ADK for Kotlin 的设计目标就是让 Agent 成为 Kotlin 工程里的一个普通模块用 Gradle 依赖进来用注解声明工具用挂起函数处理异步编译期就能发现大部分错误。这一点非常关键它意味着 Agent 不再是外挂而是你 App 的一部分。2.2 KSP 注解处理在 ADK 里扮演的角色KSP 是 Kotlin Symbol Processing 的缩写你可以把它当成 Kotlin 版的注解处理器。ADK for Kotlin 大量依赖 KSP 来做代码生成这是它和 Python 版本最大的差异之一。在 Python 里你定义一个工具通常是写个函数再加个装饰器运行时反射去扫描。Kotlin 这边不一样你在函数上标一个注解KSP 在编译期就扫描出来自动生成注册代码、参数描述、类型映射。这样做的好处很直接。第一性能好运行时不需要反射扫描整个包。第二安全工具的参数类型在编译期就校验了你写错类型编译不过。第三对 ProGuard 友好生成的代码是显式引用不会被混淆掉。我实测下来一个中等规模的 Agent 工程KSP 生成的代码大概几百行编译时间增加在一秒以内完全可以接受。注意KSP 的版本必须和你的 Kotlin 版本严格对应差一个小版本都可能报错。建议在libs.versions.toml里统一管理 KSP 和 Kotlin 的版本号别在 build.gradle 里硬编码。2.3 工具、记忆、规划三件套的职责划分ADK 的核心抽象其实就三块Tool、Memory、Planner。Tool 是 Agent 能调用的能力比如查天气、读文件、发请求。Memory 是 Agent 的上下文存储分短期记忆和长期记忆。Planner 是决策逻辑决定下一步调用哪个工具。这三块的边界设计得很清楚。Tool 只负责做一件事不关心谁调用它。Memory 只负责存和取不关心存的是什么。Planner 只负责决策不直接执行。这种单一职责的划分让每一块都可以独立替换。比如你觉得默认的 Planner 不够聪明可以换成自己基于规则加模型的混合策略觉得内存记忆不够用可以换成基于数据库的持久化记忆。我在项目里就把 Memory 换成了 Room 实现Agent 重启后还能记得上次聊到哪体验提升非常明显。2.4 为什么选择协程而不是回调或 RxJavaADK for Kotlin 全面基于协程所有工具调用、模型请求、记忆读写都是挂起函数。这个选择我认为是对的。Agent 的执行流程本质上是串行加分支的用协程写出来就是普通的顺序代码可读性极高。你想想如果用回调一个先查天气再根据天气决定要不要带伞的逻辑会嵌套成什么样。RxJava 虽然能组合但操作符太多团队里不是每个人都熟。协程还有个好处是结构化并发。Agent 执行过程中可能同时调用多个工具用coroutineScope加async就能并行任何一个失败都能正确取消其他任务。我在处理一个需要同时查三个数据源的 Agent 时用协程并行把响应时间从 1.2 秒压到了 400 毫秒左右。这个在移动端体验上差别很大。3. 核心细节解析与实操要点3.1 环境准备Kotlin、KSP、Gradle 的版本对齐动手之前先把环境弄干净。ADK for Kotlin 对版本比较敏感我踩过的坑基本都出在版本不匹配上。你需要确认三件事Kotlin 版本、KSP 版本、Gradle 版本。KSP 的版本号格式是Kotlin版本-KSP版本比如 Kotlin 是 2.0.21那 KSP 就得是2.0.21-1.0.28这种格式前面的部分必须和 Kotlin 完全一致。Gradle 建议用 8.5 以上因为 ADK 的插件用了一些新的 Gradle API。JDK 用 17这是目前 Android 和 Kotlin 生态的共识版本。如果你还在用 JDK 11可能会遇到一些奇怪的编译错误。// libs.versions.toml [versions] kotlin 2.0.21 ksp 2.0.21-1.0.28 adk 0.1.0 agp 8.5.0 [plugins] ksp { id com.google.devtools.ksp, version.ref ksp } adk { id com.google.adk.kotlin, version.ref adk }在模块的build.gradle.kts里应用插件注意 KSP 插件要在 Kotlin 插件之后应用顺序错了会报错。plugins { alias(libs.plugins.kotlin.android) alias(libs.plugins.ksp) alias(libs.plugins.adk) } dependencies { implementation(com.google.adk:adk-core:0.1.0) ksp(com.google.adk:adk-compiler:0.1.0) }提示adk-compiler一定要用ksp配置而不是implementation否则注解处理器不会运行你的工具类不会被注册运行时会提示找不到工具。3.2 定义一个工具注解、参数、返回值的设计规范工具是 Agent 的手和脚。在 ADK for Kotlin 里定义一个工具就是写一个带注解的挂起函数。注解里要写清楚这个工具是干什么的因为这段描述会直接喂给模型模型靠它来判断什么时候调用。Tool(description 查询指定城市的当前天气返回温度和天气状况) suspend fun getWeather( Param(description 城市名称例如北京、上海) city: String ): WeatherResult { // 实际实现可以是网络请求 return weatherApi.query(city) }这里有几个细节值得说。第一description要写得像给同事解释一样别写获取天气这种太泛的模型看不懂什么时候该用。第二参数描述同样重要模型需要知道每个参数填什么。第三返回值类型最好是结构化的 data classADK 会自动把它序列化成模型能理解的格式。我见过有人把工具写成返回 String然后在里面拼一大段 JSON这样模型解析起来容易出错。用 data class让框架去处理序列化稳定得多。另外工具函数尽量保持无副作用或者副作用可控因为模型可能会重复调用同一个工具如果你的工具是扣款这种操作一定要加幂等保护。3.3 记忆管理短期上下文与长期存储的分层策略Memory 这块是很多人容易忽略的地方。ADK 默认提供的是内存记忆Agent 运行期间有效进程一杀就没了。对于移动端来说这显然不够。你需要根据场景选择记忆策略。短期记忆就是当前对话的上下文通常用一个滑动窗口保存最近 N 轮对话。窗口大小要权衡太大 token 消耗高太小模型记不住前文。我一般设 10 到 20 轮具体看你的模型上下文长度。长期记忆则是跨会话的比如用户的偏好、历史订单这些需要持久化。ADK 的 Memory 接口设计得很干净你实现MemoryStore接口就行。我用 Room 实现了一个核心就是两张表一张存会话一张存消息。查询的时候按会话 ID 和时间排序取最近 N 条。class RoomMemoryStore(private val dao: MessageDao) : MemoryStore { override suspend fun save(sessionId: String, message: Message) { dao.insert(message.toEntity(sessionId)) } override suspend fun recall(sessionId: String, limit: Int): ListMessage { return dao.queryRecent(sessionId, limit).map { it.toMessage() } } }注意长期记忆要考虑隐私和清理策略。用户注销账号时相关的记忆数据必须能彻底删除。别把敏感信息明文存本地至少做个加密。3.4 Planner 的决策流程与工具选择逻辑Planner 是 Agent 的大脑。ADK 默认的 Planner 是基于模型的 ReAct 风格模型输出思考过程然后决定调用哪个工具拿到结果后再思考循环直到得出最终答案。这个流程听起来简单实际调优空间很大。关键点在于工具的数量和描述质量。如果你给模型 50 个工具它选择准确率会明显下降。我的经验是单个 Agent 的工具控制在 10 个以内超过就拆成多个 Agent 或者做分层路由。工具描述要避免语义重叠比如查天气和获取气象信息这种模型会纠结。另外 Planner 的循环次数要设上限防止模型陷入死循环。ADK 默认好像是 10 次我一般设 5 次因为大部分任务 3 次以内就能完成超过 5 次基本是模型卡住了不如直接返回错误让上层处理。4. 实操过程与核心环节实现4.1 从零搭建一个最小可运行 Agent我们来做点实际的。目标是一个能查天气、能算数的 Agent。先建工程按前面的版本配置好依赖。然后定义两个工具。Tool(description 计算两个数字的和) fun add( Param(description 第一个加数) a: Double, Param(description 第二个加数) b: Double ): Double a b Tool(description 查询城市天气) suspend fun weather( Param(description 城市名) city: String ): String { // 模拟实现 delay(300) return $city 今天晴25 度 }然后创建 Agent 实例。ADK 的入口通常是Agent.builder()或者类似的 DSL把工具注册进去指定模型和记忆。val agent Agent.builder() .name(assistant) .model(GeminiModel(gemini-1.5-flash)) .memory(InMemoryStore()) .tools(ToolRegistry.fromAnnotated()) // KSP 生成的注册表 .build()ToolRegistry.fromAnnotated()这个方法就是 KSP 生成的它会自动扫描所有带Tool注解的函数并注册。你不需要手动一个个加这是注解处理带来的便利。调用的时候用挂起函数val response agent.run(北京天气怎么样另外帮我算下 3 加 5) println(response.text)跑起来之后你会看到 Agent 先调用 weather 工具再调用 add 工具最后把两个结果整合成一句话返回。整个过程是自动的你只给了一个自然语言输入。4.2 工具参数的类型映射与序列化细节这里展开讲一下参数映射因为这是实际开发中出错最多的地方。ADK 需要把模型的输出通常是 JSON映射到你的函数参数上。支持的类型包括基本类型、String、枚举、以及嵌套的 data class。有个坑是 Kotlin 的可空类型。如果你的参数是String?模型可能不传这个参数ADK 会传 null。但如果你的参数是String非空模型没传就会报错。所以对于可选参数一定要用可空类型加默认值。Tool(description 搜索商品) suspend fun search( Param(description 关键词) keyword: String, Param(description 页码默认 1) page: Int 1, Param(description 分类可选) category: String? null ): ListProduct { ... }枚举类型也支持但要注意模型可能输出枚举值的大小写不一致。ADK 内部做了忽略大小写的匹配但如果你用自定义的序列化器得自己处理。我一般建议枚举值用全大写加下划线描述里写清楚可选值模型基本不会错。4.3 多轮对话中的上下文注入与截断多轮对话是 Agent 的常见场景。ADK 的 Memory 会在每次run的时候自动把历史消息注入到模型请求里。但这里有个问题历史太长会超出模型的上下文窗口。你需要做截断。截断策略有两种按条数截断和按 token 数截断。按条数简单但不精确按 token 数精确但需要额外的 tokenizer。我一般用混合策略先按条数取最近 20 条再估算 token 数如果超过阈值就从最老的开始丢。suspend fun buildContext(sessionId: String): ListMessage { val recent memory.recall(sessionId, 20) var tokens 0 val result mutableListOfMessage() for (msg in recent.reversed()) { val t estimateTokens(msg.content) if (tokens t MAX_TOKENS) break tokens t result.add(0, msg) } return result }提示系统提示词system prompt要单独计算不能被截断掉否则 Agent 的行为会完全跑偏。4.4 在 Android 工程中集成与线程调度把 Agent 集成到 Android App 里核心是线程调度。Agent 的run是挂起函数必须在协程里调用。网络请求、数据库操作这些工具内部也要切到 IO 调度器。viewModelScope.launch { val response withContext(Dispatchers.IO) { agent.run(userInput) } _uiState.value response.text }注意别在主线程调用模型请求会直接 ANR。另外 Agent 实例最好是单例因为初始化有一定开销而且 Memory 需要跨调用共享。可以用 Hilt 或者简单的 object 单例来管理。如果 Agent 执行时间较长建议加个进度提示。ADK 支持流式输出你可以监听每个中间步骤把正在查询天气...这种状态显示给用户体验会好很多。5. 常见问题与排查技巧实录5.1 工具不被识别KSP 没跑或注解写错最常见的报错是运行时提示tool not found。九成情况是 KSP 没正确运行。排查步骤先看 build 目录下有没有生成的ToolRegistry类如果没有说明 KSP 没跑。检查ksp依赖配置是否正确插件是否应用。如果生成了但工具还是找不到检查注解的包名是不是 ADK 指定的那个有时候 IDE 自动导入会导错包。还有一种情况是工具函数定义在 object 或者 companion object 里KSP 对这两种场景的支持需要额外配置。我建议工具函数就放在顶层或者普通类的实例方法里别搞太复杂。5.2 模型不调用工具描述写得太模糊模型该调工具的时候不调或者调错工具基本都是描述问题。我做过一个对比测试把查询天气改成查询指定城市的实时天气状况包括温度、湿度、风力用于回答用户关于天气的问题工具调用准确率从 60% 提升到了 90% 以上。描述要具体要说明使用场景要列出返回的信息类型。另外参数描述也别偷懒。city: String这种模型可能传北京市朝阳区也可能传北京如果你的 API 只认北京就得在描述里写清楚只填城市名不要带区县。5.3 记忆膨胀导致响应变慢跑了一段时间后发现 Agent 越来越慢多半是记忆没清理。内存记忆如果不设上限会一直增长每次请求都要把全部历史塞给模型token 消耗和延迟都会飙升。一定要设窗口大小并且定期清理过期会话。如果是持久化记忆还要考虑数据库索引。按 sessionId 和时间查询这两个字段要建联合索引否则数据量大了查询会很慢。我有个项目忘了建索引几千条消息后查询要几百毫秒加上索引后降到几毫秒。5.4 常见问题速查表问题现象可能原因排查方向工具找不到KSP 未运行检查 build 目录生成代码模型不调工具描述模糊优化 Tool 和 Param 描述响应越来越慢记忆膨胀设置窗口大小清理历史编译报错版本冲突KSP 与 Kotlin 不匹配对齐版本号运行时序列化失败参数类型不支持改用基本类型或 data class主线程卡顿未切 IO 调度器用 withContext 包裹5.5 几个我踩过的坑和独家技巧第一个坑是 ProGuard。Release 包混淆后工具全失效因为 KSP 生成的注册代码引用了工具类的名字被混淆了就找不到。解决办法是在 proguard 规则里 keep 住所有带Tool注解的类和方法。ADK 应该提供了 consumer rules但保险起见自己再加一条。第二个技巧是给工具加超时。模型调用工具时如果工具卡住整个 Agent 就挂在那了。用withTimeout包一层超时就返回错误信息给模型让它决定是重试还是换方案。Tool(description ...) suspend fun riskyTool(...): String withTimeout(5000) { // 实际逻辑 }第三个技巧是日志。Agent 的决策过程是黑盒出问题很难查。ADK 支持回调可以在每次工具调用前后打日志记录输入输出。我一般会把日志写到文件出问题时翻日志比猜快得多。第四个经验是别迷信大模型。有些简单任务用规则就能判断的没必要让模型决策。比如用户输入明显是问候语直接返回固定回复省一次模型调用又快又省钱。ADK 的 Planner 可以自定义你可以在模型前面加一层规则路由。6. 从能跑到好用性能与体验优化6.1 减少模型调用次数的几个手段Agent 的成本和延迟主要花在模型调用上。减少调用次数是最有效的优化。手段有几个一是合并工具把多个相关的小工具合成一个带参数的减少模型的选择次数。二是缓存对于相同输入直接返回缓存结果尤其是那些幂等的查询类工具。三是预判在 Planner 里加规则某些明显意图直接走固定流程。我做过一个客服 Agent原本平均 4.2 次模型调用通过合并工具和加规则路由降到了 2.1 次响应时间从 3 秒降到 1.5 秒成本也降了一半。6.2 流式输出与用户感知优化移动端用户对等待很敏感。ADK 支持流式返回模型每生成一段就推给 UI。这样用户能很快看到第一个字感知上的等待时间大幅缩短。实现上用 Flow 接收UI 层收集。agent.runStream(input).collect { chunk - _uiState.update { it chunk.text } }配合中间步骤的提示比如正在查询...、正在整理...用户会觉得 Agent 在认真工作而不是卡死了。这个体验细节在移动端特别重要。6.3 离线降级与错误兜底网络不稳定是移动端的常态。Agent 依赖模型服务断网就废了。你需要设计降级策略。简单的做法是缓存常见问题的答案断网时直接返回。复杂一点的是本地小模型兜底Google 的 AI Edge 那套可以在设备上跑轻量模型虽然能力弱但能保证基本可用。错误兜底也很关键。模型服务超时、工具执行失败这些都要有明确的用户提示别让用户对着转圈界面干等。我一般设 10 秒超时超时后提示网络不太好请稍后再试同时把这次请求记下来下次联网时可以重试。7. 我对 ADK for Kotlin 的实际使用体会用了一段时间下来ADK for Kotlin 最让我满意的是它和 Kotlin 工程的无缝程度。KSP 生成代码、协程处理异步、Gradle 管理依赖这些都是 Kotlin 开发者本来就熟悉的东西学习成本主要花在 Agent 的概念上而不是工具链上。相比去折腾 Python 环境再想办法和 Android 通信这条路顺畅太多。当然它也有不成熟的地方。文档还在完善中有些 API 的命名和 Python 版本不一致社区案例也少。遇到问题很多时候得看源码。但考虑到它才刚起步这个状态可以理解。我的建议是如果你在做 Android 或者 Kotlin 后端想试水 AI AgentADK for Kotlin 是目前最自然的选择。先从简单的单工具 Agent 开始跑通了再逐步加记忆、加多工具、加自定义 Planner。别一上来就搞复杂架构容易劝退。最后分享一个小技巧调试 Agent 的时候把模型的原始输出打出来看。很多时候问题不在你的代码而在模型理解错了你的描述。看到原始输出你就能判断是该改描述还是改代码。这个习惯帮我省了大量排查时间。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进