ADK for Kotlin 实战:Android 端侧智能体与多 Agent 编排
2026/9/19 6:49:15 网站建设 项目流程

1. 为什么值得花一个周末看 ADK for Kotlin

1.1 ADK 的真实定位:不是框架,是一套"骨架约定"

第一次看到 ADK 这个名字,我下意识以为是又一个"把 API 调用包一层"的壳子。真读完官方的示例工程之后,我的判断变了。ADK 全称 Agent Development Kit,是 Google 开源的一套智能体开发工具,最开始只有 Python 版本,后来补上了 Java 支持,再到 Kotlin 原生支持的落地,这条路径其实非常明确——它想把 Agent 从"一段 prompt 加几次模型调用"的脚本形态,升级成一个有明确生命周期、有工程边界的软件模块。

这件事的意义在哪?我举个自己踩过的例子。去年我做一个小助手功能,最初就是"用户输入 → 拼 prompt → 调模型 → 解析返回",两三百行代码搞定了。等到要加工具调用、要加多轮记忆、要加失败重试,代码迅速膨胀到两千行,而且每加一个功能都要动核心流程。问题不在于模型不好用,而在于没有骨架。ADK 想解决的就是这个:它规定了四个基本概念,Agent负责"谁来思考",Tool负责"能做什么",Session负责"记得什么",Event负责"发生了什么",然后用 Runner 把这四个东西串成一条可观测的事件流水线。

只要接受了这层约定,后面加记忆、加多智能体、加人工审核节点,都是往骨架里插模块,而不是推倒重来。Kotlin 版本的价值就在这里:它把同一套约定搬到了 JVM 生态,尤其是 Android 和 Spring Boot 这两个场景,不用再为了一点 Agent 能力硬塞一个 Python 服务进去做 IPC。

1.2 Kotlin 版和 Python 版到底差在哪

很多人第一反应是"Python 版文档全、示例多,我直接用 Python 不就行了"。如果你的产物是后端服务或者数据处理脚本,这个选择没问题。但如果你要在 Android 应用里做端侧能力,或者你现有的后端是 Kotlin/Java 技术栈,那 Python 版就是个尴尬的存在——要么服务端多维护一个进程,要么把模型调用全部改成 HTTP 转发,网络抖动、超时、错误码映射全得自己写一遍。

Kotlin 版本最实在的三个差异点,我按重要程度排一下。

第一是协程与异步模型的对齐。Agent 执行天然是流式的、可能长时间挂起的,Python 用 async/await,Kotlin 用 suspend 函数加 Flow。ADK 的 Kotlin 封装在事件流上返回的是 Flow,这跟 Android 里 Compose 的 collectAsState、ViewModel 的 viewModelScope 是同一套心智,不用做桥接。

第二是类型安全。工具函数的参数校验、返回结构定义,Kotlin 的 data class 加注解能直接在编译期发现问题,Python 只能靠运行时校验和文档约定。工具数量一多,这个差距非常明显。

第三是与 Android 生命周期和权限体系的天然衔接。工具要读通讯录、要调定位、要访问本地文件,这些都需要运行时权限,Kotlin 侧可以直接复用现有的权限封装,Python 侧根本摸不到这些系统能力。

注意:Kotlin 版本的发布节奏比 Python 版本慢,部分高级特性(比如某些第三方集成)可能还没覆盖。上生产之前一定先对着官方仓库的 release note 确认一遍,别照着 Python 文档直接抄。

1.3 三类人该上手,两类人可以先等等

我给个不那么政治正确的分类。

该上手的:一是 Android 开发者,手里已经有应用在跑,想加一个能查资料、能操作本地数据、能多轮对话的助手模块;二是 JVM 后端开发者,业务里有大量"接收自然语言指令 → 查多个内部系统 → 汇总输出"的流程;三是已经在用 Python 版 ADK 但团队主力是 Kotlin,想统一技术栈减少维护成本的团队。

可以先等等的:一是只做单次 prompt 调用、没有工具和多轮需求的场景,用不用 ADK 差别不大,反而多一层学习成本;二是对延迟极其敏感的实时交互场景,Agent 的多轮工具调用链路本身会带来秒级开销,这个后面我会细讲怎么压。

我个人的建议是,先花半天把官方那个最简示例跑通,感受一下 Event 流长什么样。跑通了再决定要不要深入,比读十篇介绍文章都管用。

2. 动手前的三件事:依赖、密钥、心智模型

2.1 Gradle 依赖与版本选择

先说工程结构。如果你是在 Android 项目里试水,我强烈建议先单独开一个纯 Kotlin/JVM 模块,或者干脆新建一个空工程,别一上来就塞进主 App 的 app 模块。原因很实际:Agent 依赖会引入一堆网络库、序列化库,跟你现有的 OkHttp、Gson、Retrofit 版本打架的概率不低,混在主模块里排查依赖冲突能耗掉你一整天。

依赖配置大致长这样,具体版本号以官方仓库当前发布的为准:

// build.gradle.kts dependencies { // 核心 ADK 运行时 implementation("com.google.adk:google-adk:0.1.0") // Kotlin 侧的协程与 Flow 封装 implementation("com.google.adk:google-adk-kotlin:0.1.0") // 底层用到的响应式流,ADK 的事件流建立在这之上 implementation("io.reactivex.rxjava3:rxjava:3.1.8") // 网络与 JSON implementation("com.squareup.okhttp3:okhttp:4.12.0") implementation("com.google.code.gson:gson:2.11.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0") testImplementation(kotlin("test")) }

选择上有个取舍要说清楚。ADK 的 Java 版本底层用的是 RxJava 的 Flowable 做事件流,Kotlin 封装把它转成了 Flow。这意味着你的工程里同时存在两套异步模型。我的做法是:在 Agent 边界之内用 RxJava 的语义(比如 blockingGet 拿 Session),出了边界立刻转成 Flow,业务层完全感知不到 RxJava。

另外,minSdkVersion如果你要在 Android 上跑,建议至少 24,低版本上协程调度和部分网络能力会有兼容性坑。别为了覆盖几个老设备把整个工程拖下水。

2.2 模型接入:直连还是走托管,先想清楚

ADK 调模型需要凭证。常见有两条路:一条是通过开发者平台的 API Key 直连,适合本地开发、个人项目、原型验证;另一条是走云上的托管服务,用服务账号鉴权,适合有合规要求、需要配额管理和审计日志的生产环境。

本地开发我一般这么配,把 Key 放在环境变量里,别硬编码进代码再提交:

export GOOGLE_API_KEY="你的开发用密钥" export GOOGLE_GENAI_USE_VERTEXAI="false"

然后代码里读:

val apiKey = System.getenv("GOOGLE_API_KEY") ?: error("没有配置 GOOGLE_API_KEY,先把它写进运行配置里")

这里有个很多人第一次会栽的坑:Android 应用里直接用 API Key 是不安全的。APK 是可以被反编译的,字符串常量扒出来是分分钟的事。正确的做法是让客户端走你自己的后端,后端持有密钥并做限流和鉴权,客户端只跟后端通信。我在本地调试时图省事直接在 App 里读环境变量,上线前一定要换掉,这一步千万别偷懒。

提示:密钥管理这件事,我踩过一次亏。曾经把测试 Key 写进了一个 demo 仓库并设成了公开,几小时内配额就被跑光了。从那之后我的习惯是本地用.envgitignore,CI 里用平台的 Secret 变量,从来不写进源码。

2.3 先分清 Agent、Tool、Session、Event 四个词

在写第一行代码之前,把这四个词的含义对清楚,能省掉后面大量的困惑。

Agent是"决策者"。它持有模型名、系统指令、可用工具清单。你可以把它理解成一个有职责边界的员工,有的 Agent 只负责收集信息,有的只负责写报告。

Tool是"能力"。它是一个普通的 Kotlin 函数加一段描述,模型看到描述后自己决定要不要调、怎么传参。注意这里的主动权在模型,不在你。这一点极其关键,后面排查"工具为什么没被调用"时全靠这个认知。

Session是"会话容器"。它绑定了用户 ID 和会话 ID,内部维护着一份对话历史和一份可变的状态字典。同一个 Session 内的多轮对话是连贯的。

Event是"流水线上的包裹"。Agent 每做一步——收到用户输入、决定调用工具、工具返回结果、生成最终回答——都会产出一个 Event。你的 UI 层消费的就是这条 Event 流。

我习惯用生活化的方式记:Agent 是厨师,Tool 是厨房里的锅碗瓢盆和各种食材,Session 是这次订单的完整记录,Event 是后厨传菜口不断递出来的每一道半成品和成品。你作为前厅,只看传菜口,不用管后厨怎么切的菜。

3. 第一个能跑起来的 Agent:逐行拆解

3.1 最小可运行单元长什么样

先看一个能跑通的最小例子。我刻意写得简单,就是为了让你把注意力放在结构上,而不是业务逻辑。

import com.google.adk.agents.LlmAgent import com.google.adk.runners.InMemoryRunner fun main() { // 1. 定义 Agent val agent = LlmAgent.builder() .name("assistant") .model("gemini-2.5-flash") .instruction( """ 你是一个简洁的助手。 回答控制在三句话以内,不要输出无关的寒暄。 """.trimIndent() ) .build() // 2. 创建 Runner,绑定 Agent val runner = InMemoryRunner(agent = agent, appName = "demo") // 3. 建一个会话 val session = runner.sessionService() .createSession("demo", "user_001") .blockingGet() // 4. 发一条消息,消费事件流 runner.runAsync( userId = "user_001", sessionId = session.id(), newMessage = userText("你好,用一句话介绍一下你自己") ).blockingForEach { event -> val text = event.content()?.parts() ?.joinToString("") { it.text() ?: "" } .orEmpty() if (text.isNotBlank()) println(text) } }

四个步骤,对应上一节说的四个概念,一点不多。新手最容易犯的错是跳过 Runner 直接调 Agent。ADK 里 Agent 本身不负责编排执行,Runner 才是那个调度器,它管会话读写、管事件分发、管工具执行。理解这一点,后面看到SequentialAgent之类的编排器就不会懵。

instruction这个字段值得多说一句。它就是系统提示词,但 ADK 会把它和当前会话的上下文一起组装。写的时候有几条经验:明确角色边界、明确输出格式、明确禁止项。我见过太多人在生产环境里把 instruction 写成一大段模糊的"你要尽力帮助用户",结果模型输出格式每天都不一样,前端解析直接崩。

3.2 Runner 与 Event 流:为什么它不是"调一次返回一次"

很多从普通 API 调用转过来的人,会本能地期望run方法返回一个字符串。ADK 返回的是一串事件。这个设计的理由很实在:Agent 执行过程中可能调三次工具、可能切换到子 Agent、可能需要你中途插进去做个人工确认。如果把整条链路压成一次返回,中间过程全丢了,出问题根本没法查。

事件流的每一帧大概包含这些东西:是谁产生的(author)、内容是什么(content)、是不是最终回答(isFinalResponse)、有没有工具调用或工具结果。

消费的时候,我一般分三路处理:

runner.runAsync(userId, sessionId, newMessage).collect { event -> when { event.isFinalResponse() -> { // 最终回答,推给 UI 收尾 renderFinal(event.text()) } event.hasToolCall() -> { // 工具调用中,给个 loading 提示 showStatus("正在调用 ${event.toolName()}...") } else -> { // 中间过程,开发期打日志 logDebug("中间事件:${event.author()}") } } }

分三路之后,UI 体验会好很多。用户在等工具返回的时候能看到"正在查询天气",而不是对着一个转圈发呆。这点在移动端尤其重要,超过两秒没有任何反馈,用户就会以为卡死了。

注意:runAsync返回的流一定要在合适的协程作用域里消费,并且在页面销毁时取消。我第一版忘了在onCleared里 cancel,结果页面退出后还在后台跑,日志刷得飞起,流量也白白消耗。

3.3 把输出接到 Android UI 上

如果你是在 Android 里用,推荐的结构是 ViewModel 持 Runner,UI 通过 StateFlow 订阅。

class ChatViewModel( private val runner: InMemoryRunner, private val sessionId: String ) : ViewModel() { private val _state = MutableStateFlow(ChatUiState()) val state: StateFlow<ChatUiState> = _state.asStateFlow() private var job: Job? = null fun send(input: String) { job?.cancel() job = viewModelScope.launch { _state.update { it.copy(sending = true, streamingText = "") } runner.runAsync( userId = "user_001", sessionId = sessionId, newMessage = userText(input) ).collect { event -> when { event.isFinalResponse() -> { _state.update { it.copy(sending = false, messages = it.messages + event.text()) } } event.hasToolCall() -> { _state.update { it.copy(statusText = "正在处理...") } } } } } } override fun onCleared() { job?.cancel() super.onCleared() } }

这里有几个实践点:一是发送新消息前取消上一个任务,否则用户连点发送会产生多条并发流,UI 状态直接乱掉;二是状态用不可变 data class,方便 Compose 做 diff;三是工具调用阶段单独暴露一个状态字段,别和正文混在一起。

实测下来,这套结构在中等复杂度的对话场景里非常稳。真正需要额外设计的是流式输出的逐字渲染,那个要看你用的模型是否返回增量 token,Kotlin 版本目前对这种场景的支持在不同版本间有差异,用之前先确认一下。

4. 工具系统:让 Agent 真的能"做事"

4.1 三种写工具的方式与各自适用场景

只聊天的 Agent 价值有限,工具才是分水岭。ADK 里写工具大致三条路。

第一条,普通函数加注解。适合绝大多数业务逻辑,比如查数据库、调内部接口、做计算。

class WeatherTool { @Schema(description = "查询指定城市在指定日期的天气情况") fun getWeather( @Schema(description = "城市名,例如 北京、上海") city: String, @Schema(description = "日期,格式 YYYY-MM-DD,不填表示今天") date: String = "" ): Map<String, Any> { // 真实项目里换成网络请求 return mapOf( "city" to city, "date" to date.ifBlank { "today" }, "tempC" to 24, "condition" to "晴", "windLevel" to 3 ) } }

然后用FunctionTool.create(WeatherTool(), "getWeather")把它包装成工具注册给 Agent。注意工具类本身是普通类,不需要继承什么基类,这是非常好的设计,意味着你现有的 Service 层几乎可以原样复用。

第二条,内置工具。ADK 提供了一些开箱即用的能力,比如联网检索、代码执行这类。适合原型阶段快速验证,但生产环境要评估合规性和结果可控性。

第三条,Agent 作为工具。这个稍微进阶一点,把一个完整的子 Agent 当成另一个 Agent 的工具来用。适合"主流程需要调用一个专注做某件事的专家"的场景。

三条路的选择标准很简单:逻辑是你自己写的,就用函数工具;逻辑想让平台兜底,就用内置工具;逻辑复杂到需要独立一整套 prompt 和工具集,就封装成 Agent 工具。

4.2 参数 Schema 写不好,模型就不会调用

这是我在实际项目里花时间最多的地方,也是新手最容易忽略的地方。模型决定要不要调用工具、怎么填参数,完全依赖你写的描述文本。描述写糊了,模型要么不调用,要么填错参数。

几条实打实的经验:

第一,描述里写清楚单位和格式date这个参数,如果不写"格式 YYYY-MM-DD",模型可能传"明天""下周三""2025年3月5日",你的解析代码就要写一堆兼容。我现在的习惯是把格式写死在描述里,再补一句示例。

第二,参数名用英文,描述用中文。参数名是代码层面的,用英文避免编码问题;描述是给模型看的,用中文表达更精确。

第三,枚举值一定要列出来。比如状态查询工具,描述里写"状态只能是 pending、running、done 三者之一",比写"传入状态"效果好一个量级。

第四,能不给可选参数就不给。可选参数越多,模型越容易乱填。我一般先做成必填,跑一段时间观察模型的行为,确实高频缺失的再放开默认值。

提示:我做过一个反直觉的测试——同一份工具描述,只改描述的措辞,工具调用成功率从六成出头提升到九成以上。所以如果你遇到"工具时灵时不灵",先别怀疑模型,回去改描述。

4.3 工具里抛异常会发生什么

这个问题必须提前想清楚。工具执行过程中抛异常,ADK 会把异常信息作为工具结果回传给模型,模型看到之后可能会尝试换个参数重试,也可能直接跟用户说"我遇到问题了"。

这个默认行为在开发期很方便,但生产环境有两个隐患。

一是敏感信息泄露。异常堆栈里可能包含数据库连接串、内部路径、表名。如果这段内容被原样喂给模型再输出给用户,就是实打实的信息泄露。我的做法是工具内部先 catch,把异常转成一个结构化的错误对象:

@Schema(description = "根据订单号查询订单状态") fun queryOrder( @Schema(description = "订单号,纯数字,长度 12 位") orderId: String ): Map<String, Any> { return try { val order = orderRepo.find(orderId) ?: return mapOf("ok" to false, "reason" to "订单不存在") mapOf("ok" to true, "status" to order.status, "updatedAt" to order.updatedAt) } catch (e: Exception) { // 只回传安全信息,堆栈写日志 log.error("查询订单失败 orderId={}", orderId, e) mapOf("ok" to false, "reason" to "系统繁忙,请稍后重试") } }

二是无限重试。模型可能因为同样的错误反复调用同一个工具。控制手段是在 instruction 里明确写"同一个工具连续失败两次就停止重试并告知用户",或者用 LoopAgent 的退出条件来兜底。我在没有约束的情况下见过模型连续调同一个工具六次,把配额烧掉一大截。

5. 多 Agent 编排与状态管理

5.1 顺序、并行、循环:三种编排怎么选

单 Agent 搞不定的复杂流程,就要上编排。ADK 给了三种基本形态。

SequentialAgent(顺序)适合有明确前后依赖的流水线。比如"先收集用户需求 → 再查资料 → 最后生成报告",每一步都依赖上一步的输出。

val pipeline = SequentialAgent.builder() .name("report_pipeline") .subAgents(listOf(collectorAgent, researcherAgent, writerAgent)) .build()

ParallelAgent(并行)适合互相独立的子任务。比如"同时查天气、查航班、查酒店",三个都查完了再汇总。用并行的收益很直接:原本串行 6 秒的活儿,并行之后两秒多。

LoopAgent(循环)适合需要迭代打磨的任务,比如"写草稿 → 评审 → 修改"循环到评审通过为止。这里必须设最大轮次,否则容易死循环。

val refineLoop = LoopAgent.builder() .name("refine_loop") .maxIterations(3) .subAgents(listOf(draftAgent, reviewAgent)) .build()

选择标准:有依赖用顺序,无依赖用并行,需要收敛用循环。实际项目里三种往往是嵌套的,外层顺序、中间某一步并行、某一步内部循环,这都很正常。我的建议是先全用顺序跑通,再针对明显的性能瓶颈改并行,别一上来就设计复杂拓扑。

5.2 Session、State、Memory 的分层

这三层的区别,是我见过最多人绕晕的地方。

Session是一次会话的容器,绑定了 userId 和 sessionId。它的生命周期通常就是用户这次对话的时长。

State是挂在 Session 上的一份可变字典,用来在多个 Agent、多轮对话之间传递数据。比如收集 Agent 把"用户想要的目的地"写进 state,后面的 Agent 从 state 里读。写法上一般是通过context.state()读写。

Memory是更长期的东西,跨 Session 存在。用户上次聊天说喜欢安静的地方,下次打开应用时 Agent 还能记得。

// 子 Agent 里写入状态 context.state().put("destination", "杭州") context.state().put("days", 3) // 后续 Agent 里读取 val destination = context.state().get<String>("destination")

这里有个坑我必须提醒:State 的 value 要能序列化。我最早图方便往里塞了一个自定义对象,本地内存模式下一切正常,换成持久化存储之后直接报序列化错误。后来统一约定:State 里只放 String、Number、Boolean 和它们的集合,复杂对象转成 JSON 字符串存。

Memory 的使用要克制。不是所有信息都值得长期记住,无差别地存会让上下文越来越长,成本和延迟同步上升。我的做法是只存显式的用户偏好和关键事实,并且设一个上限,比如只保留最近 20 条。

5.3 Callback:不改 prompt 也能改行为

Callback 是 ADK 里我认为最被低估的能力。它让你在不修改 prompt、不修改工具代码的前提下,在关键节点插入自己的逻辑。

常用的是四个位置:模型调用前、模型调用后、工具调用前、工具调用后。

我实际用过几个场景。一个是权限校验,在工具执行前检查当前用户有没有权限调这个工具,没有就直接拦掉并返回拒绝信息:

val guardedAgent = LlmAgent.builder() .name("guarded") .model("gemini-2.5-flash") .instruction("...") .beforeToolCallback { tool, args, context -> val allowed = permissionService.canUse(context.userId(), tool.name()) if (!allowed) { // 返回非 null 表示短路,不再执行原工具 mapOf("ok" to false, "reason" to "当前账号没有该操作的权限") } else { null // 返回 null 表示继续走原流程 } } .build()

另一个场景是审计日志。在 afterModelCallback 里把模型输入输出记录下来,用于事后复盘。这个对排查"模型为什么给出奇怪答案"特别有用,比事后猜强一百倍。

还有一个不太常见但很实用的用法是成本监控。在 afterModelCallback 里累加 token 用量,超过阈值就熔断当前会话。上线初期我就是靠这个把一个失控的循环给拦下来的。

6. 实操避坑与排查速查表

6.1 最常见的几类报错与定位顺序

我把实际碰到的问题整理成了一张表,按出现频率排序。

现象大概率原因定位手段
启动就报缺少凭证环境变量没配或名字写错打印 System.getenv 确认
模型返回 400模型名写错或当前账号不可用对着官方模型列表逐个核对
事件流卡住不结束工具里死循环或阻塞调用给工具加超时,打点日志
工具一直不触发描述不清晰或参数类型不匹配单独测试工具函数 + 改描述
多轮对话丢失上下文sessionId 每次新建导致换会话确认 sessionId 复用
序列化异常State 里塞了不可序列化对象统一转 JSON 字符串
依赖冲突报 NoSuchMethod网络库或 RxJava 版本不一致用依赖树命令排查
内存持续上涨事件流没取消,Session 没释放检查协程作用域与 onCleared

定位顺序我一般遵循三步:先看日志里最后一个成功的 Event 是什么,能确定卡在哪一步;再单独测试工具函数,排除业务代码问题;最后才怀疑模型和框架。实践中八成问题出在前两步。

6.2 工具不触发的五个排查点

这个现象太常见了,值得单独说。

第一个点,工具有没有真的注册到 Agent 上。我犯过这个错,工具类写完了忘了加进.tools(...),排查了半小时。

第二个点,描述里有没有明确的触发词。如果工具描述写的是"处理数据",模型根本不知道什么时候该用。改成"查询指定用户在指定日期范围内的订单列表",触发率立刻不一样。

第三个点,参数类型是不是模型难以生成的形式。比如要求传一个复杂的嵌套对象,模型很容易填错。能拆成扁平的基本类型就拆。

第四个点,instruction 里有没有反向引导。我见过一个案例,instruction 里写了"尽量直接用你的知识回答,不要调用外部工具",结果工具永远不触发。这两处要保持一致。

第五个点,模型能力差异。同样的工具和描述,不同模型的表现差别可能很大。测试阶段如果一直不触发,换个模型对照一下,能快速判断是描述问题还是模型问题。

提示:我一般会准备一个"工具冒烟测试"的脚本,不经过 Agent,直接把工具函数用几个典型参数调一遍。业务代码正确的前提下,问题必然在描述侧。

6.3 延迟与成本的实际控制手段

Agent 的延迟和成本,本质上是两个变量的函数:模型调用次数上下文长度

调用次数怎么压?一是减少不必要的工具。工具列表越长,模型每次决策的负担越重,也越容易乱调。我一般控制在单 Agent 五个工具以内,多了就拆分。二是避免多层嵌套。三层嵌套的 Agent 链路意味着至少三轮模型调用,能用一层解决就别套三层。三是用并行替代串行,前面说过了。

上下文长度怎么压?一是历史消息做窗口截断,只保留最近 N 轮,更早的总结成一段摘要塞进 instruction。二是工具返回值精简。我见过一个工具返回了完整的一条数据库记录、四十多个字段,而模型只需要其中三个。后来改成只返回必要字段,单次 token 消耗直接降了七成。三是State 里别堆垃圾,用完的中间变量及时清掉。

成本方面还有一个容易被忽略的点:同样的请求重复调用。用户在输入框里改了两个字重新发送,如果每次都全量重跑一遍,浪费很可观。可以做一层简单的结果缓存,key 用规范化之后的输入加会话状态摘要。

7. 我实际想过的几个落地方向

7.1 端侧个人助手

这是 Kotlin 版本最自然的场景。Android 应用里有大量本地数据——日程、备忘、照片元信息、应用使用记录——以前要么做成一堆固定入口的页面,要么做成关键词搜索。有了 Agent 之后,用户可以用一句话完成复合操作,比如"把我这周拍的照片里带猫的整理到一个相册"。

实现路径上,工具就是几个本地能力的封装:读相册、读日程、写文件、发通知。Agent 负责把自然语言拆成工具调用序列。这套东西的好处是数据不出端,隐私层面的顾虑小很多,前提是你用的是端侧能力或者只把必要的文本摘要发出去。

难点在权限和兜底。用户拒绝相册权限时,Agent 要能优雅降级,而不是直接抛异常。我的做法是在工具里统一返回结构化结果,把权限不足当成一种正常的业务分支返回给模型,让模型去跟用户解释。

7.2 服务端流程自动化

服务端的想象空间更大。典型的比如客服工单分派:收到一段描述 → 判断类别 → 查历史相似工单 → 查相关订单信息 → 生成处理建议 → 写入工单系统。这整条链路用 SequentialAgent 加几个函数工具就能搭出来,而且每一步都可观测、可回放。

另一个方向是内部工具的自然语言入口。以前运维同学要查个服务状态,得记住七八个内部平台的入口和参数格式。套一层 Agent 之后,直接说"看一下订单服务昨天的错误率",Agent 去调监控接口、调日志接口,把结果汇总成一段话。

这类场景我特别看重的两点:每一步都要有审计日志,出了问题能还原;高风险操作必须有人工确认节点,别让 Agent 有直接删数据、改配置的权限。这个边界一定要划清楚。


我在实际用 ADK for Kotlin 的这段时间里,最大的体会是:它的价值不在模型那一层,模型换来换去其实差别没有想象中那么大,真正的价值在于它逼着你把 Agent 当成一个有生命周期的软件模块来设计。什么时候用工具、状态存在哪、失败怎么恢复、成本怎么控制,这些问题在骨架搭起来之后都会自然浮现,而如果一开始就是一堆散乱的 API 调用,这些问题会一直藏在水面下,直到上线之后集中爆发。

最后再分享一个我自己的小习惯:每加一个新工具,我都会先写三个"反例测试"——分别是参数缺失、参数格式错误、工具内部异常,看 Agent 的反应是否符合预期。这三个测试加起来花不了十分钟,但能提前挡掉绝大多数线上问题。

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

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

立即咨询