Google ADK Kotlin 智能体开发实战:工具、会话与多Agent编排
2026/9/19 6:49:47 网站建设 项目流程

1. 为什么 ADK 要专门支持 Kotlin:先搞清楚这东西到底解决什么问题

Kotlin 后端圈子里最近有件挺值得琢磨的事——Google 把自家的 Agent Development Kit(ADK)搬到了 JVM 生态上,而 Kotlin 因为和 Java 的无缝互操作,等于直接拿到了这套 AI Agent 框架的入场券。很多人第一反应是"又一个套壳 SDK",但我实际翻了一遍它的结构之后,发现它想解决的问题和之前的那些轻量封装完全不是一个量级。它不是在教你怎么调一次模型接口,而是在给"一个能自己规划、能调工具、能记住上下文、能多轮协作的智能体"提供一套标准骨架。这件事对写 Kotlin 的人来说意义不小,因为过去我们想在 JVM 上搭一套像样的 Agent 流程,基本只能自己拼装 HTTP 客户端、自己设计会话状态、自己写工具调用的解析逻辑,一通下来代码量惊人且难以维护。

ADK 出现的价值就在这儿:它把 AI Agent 拆成了几个明确的构件——模型、指令、工具、会话、记忆、运行器,然后给每个构件定义好接口和生命周期。你写 Kotlin 的时候,只需要关心"我的业务逻辑是什么",而不用再操心"这段对话的状态存在哪""工具调用的参数怎么校验""多轮之间怎么传递上下文"这些脏活。适合谁来学?我的判断是三类人:一是已经在做 Android 或者后端服务、想往 AI 能力上延伸的 Kotlin 开发者;二是手里有一堆内部 API,想把这些 API 变成模型能调用的工具的人;三是想搞清楚"Agent 到底是怎么跑起来的"、不想只停留在提示词层面的学习者。

1.1 ADK 和"手搓一个 Agent"的本质区别在哪

先说清楚一个概念,很多人会把"调用大模型 API"和"构建 Agent"混为一谈。调 API 是你发一段文字、它回一段文字,一次性的、无状态的;而 Agent 的核心在于它能自己决定下一步做什么。这个"决定"从哪来?来自模型看到当前上下文后,判断需要调用哪个工具、传什么参数、拿到结果后再决定是继续调工具还是给用户答复。这一整套循环,就是一个 Agent 的最小运行逻辑。

手搓这套循环最麻烦的地方在于工具调用的编排。你得把工具的描述转成模型能理解的格式(通常是 JSON Schema),拿到模型的 function call 响应后解析参数,调用本地函数,再把结果按特定格式回灌给模型,同时还要处理模型可能连续调用多个工具、可能调用失败、可能传错参数补全等一堆边界情况。ADK 把这些全部封装进了RunnerFunctionTool两层抽象里:工具只需要声明名字、描述和参数结构,运行器负责整条链路。我个人的体会是,这套抽象省下来的不是几十行代码,而是让你少踩一大堆状态管理上的坑。

1.2 Kotlin 在这套体系里的位置比你想的更靠前

有人会问,ADK 官方主打的是 Java,Kotlin 用起来会不会处处别扭?实测下来完全不会,原因有两个。第一,Kotlin 与 Java 是 100% 双向互操作,Java 的框架 Kotlin 可以直接消费,反过来也一样。第二,Kotlin 本身的几个特性恰好特别适合写 Agent 代码:data class天然适合定义工具的参数和返回值;协程(coroutine)处理流式响应比 Java 的响应式流写起来更顺手;扩展函数可以给框架的基类加一些符合业务习惯的语法糖。

我举个具体的感受——ADK 里模型输出是流式的,Java 侧用的是响应式流,Kotlin 里你可以用协程的Flow把它包一层,然后写起来就是runner.runAsync(...).asFlow()这种干净的样子。所以别被"官方是 Java 版"这句话劝退,Kotlin 在这儿不但能用,而且写起来更舒服。

1.3 这套东西的能力边界,提前说清楚免得踩空

在动手之前得有个清醒的预期。ADK 是"框架"不是"模型",它本身不提供任何推理能力,你得接一个模型后端,官方默认对接的是 Gemini 系列,同时通过适配层也能桥接其他兼容接口的模型。它也不负责帮你做数据存储,会话状态的持久化、记忆的落盘,框架给的是接口和几种现成实现,真上生产还是得你自己替换成合适的外部存储。还有就是工具的安全性——框架会老老实实按模型的意图去调用你注册的工具,如果工具本身能删数据、能发请求,那出问题的责任在你自己这边。把这三条记在心里,后面看代码就不会有落差。

2. 环境准备与项目骨架:版本选择背后的坑

动手搭环境这一步,看似简单,其实是新手翻车最多的地方。ADK 是跑在 JVM 上的框架,对 JDK 版本、构建工具、依赖坐标都有要求,而 Kotlin 项目通常又叠加了 Gradle 的 Kotlin DSL,几个版本之间只要有一个对不齐,编译就会报一堆看不懂的错。我把自己搭环境和后面帮别人排错的经历整理一遍,尽量让你少走弯路。

2.1 JDK 和 Kotlin 版本怎么选才不折腾

ADK 的 Java 实现基于较新的语言特性,稳妥的起点是 JDK 17 及以上。我建议直接用 JDK 21,它是长期支持版本,和 Kotlin 的兼容性也最好。Kotlin 版本这边,建议用 1.9 之后的稳定版,因为它对 Java 的互操作支持在持续打磨,尤其是涉及泛型和函数式接口的地方,版本太老会遇到类型推断的奇怪问题。

构建工具我强烈建议用 Gradle 配合 Kotlin DSL,原因很实在:ADK 的依赖树里带着响应式流、JSON 解析、HTTP 客户端等好几层传递依赖,Gradle 的依赖解析和冲突处理比 Maven 更透明,出问题时dependencies任务一跑,冲突一目了然。用 Maven 也不是不行,但排查依赖冲突的时候你会想念 Gradle。

提示:不要一上来就去追最新版本号。框架和它的传递依赖之间耦合比较紧,最新的未必和你的 Kotlin 版本对得上。先用官方文档里写的推荐版本跑通,确认能跑再考虑升级。

2.2 依赖引入与"第一个能跑的 Agent"

项目初始化完之后,核心就是加依赖。ADK 的 Maven 坐标大致长这样(具体版本号以官方仓库当前最新为准):

// build.gradle.kts dependencies { implementation("com.google.adk:google-adk:0.2.0") implementation("io.reactivex.rxjava3:rxjava:3.1.8") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-rx3:1.8.0") }

那个kotlinx-coroutines-rx3是干嘛的?就是前面提到的桥接层,它让响应式流和协程能互相转换。加上它之后,你写流式处理就不用再和Flowable的订阅回调打交道了。

第一个能跑的 Agent 我建议从最简单的纯对话开始,先不挂任何工具,就验证模型能通、会话能建、输出能拿到。核心代码大致是这样:

val agent = LlmAgent.builder() .name("hello_agent") .model("gemini-2.0-flash") .description("一个最基础的对话智能体") .instruction("你是一个说话简洁的助手,回答控制在三句话以内。") .build() val runner = InMemoryRunner(agent) val session = runner.sessionService() .createSession("demo_app", "user_001") .blockingGet() runner.runAsync("user_001", session.id(), Content.fromParts(Part.fromText("你好"))) .asFlow() .collect { event -> event.content()?.parts()?.forEach { part -> part.text()?.let { print(it) } } }

这段代码里几个点值得单独说。LlmAgent.builder()是链式构建器模式,这是 Java 框架的典型写法,Kotlin 用起来也不别扭。instruction相当于系统提示词,是给模型定人设和行为规范的地方,这个字段的写法直接决定了 Agent 的输出质量,后面我会专门展开。InMemoryRunner是内存版运行器,会话状态存在进程内存里,重启就没了,只适合开发调试。

2.3 目录结构上的一个约定

ADK 对目录结构没有硬性要求,但我建议把三块东西分开:agents放 Agent 的定义,tools放各种工具类,runner或者service放启动和会话管理逻辑。这么分不是为了好看,而是因为 Agent 项目一旦长大,工具数量会快速膨胀,混在一起之后你很难分清哪些工具属于哪个 Agent。我见过一个项目把二十多个工具全塞在一个文件里,后来要改一个工具的说明都找不到地方,非常痛苦。

另外配置项别硬编码在代码里。API 密钥、模型名称、区域配置这些全部走环境变量或者外部配置文件,Kotlin 里读环境变量就是System.getenv("XXX")。把密钥写进源码然后提交,这个失误的后果不用我多说。

3. 核心概念拆解:Agent、Tool、Session、Memory 四件套

掌握了环境搭建,接下来得把 ADK 的几个核心概念吃透。这四样东西构成了一个 Agent 的骨架,理解它们之间的关系,比死记 API 重要得多。我会用生活化的类比来解释,再落到 Kotlin 代码上,尽量让你既知道是什么,也知道为什么要这么设计。

3.1 LlmAgent:把"提示词 + 模型 + 工具"打包成一个对象

LlmAgent是整个框架的核心类,你可以把它理解成一个"员工档案":它规定了这个人是谁(name)、用什么大脑思考(model)、该遵守什么工作规范(instruction)、有哪些权限可以调用的工具(tools)、以及把这个员工交给谁管理(subAgents,多 Agent 场景用)。

这里我想重点说instruction,因为它是新手最容易写坏的地方。很多人的指令写得像产品需求文档,又长又啰嗦,结果模型反而抓不住重点。我实践下来的经验是:指令要短、要有明确的角色、要有输出格式约定、要有边界。比如一个客服 Agent 的指令,好的写法是"你是XX产品的客服助手。只回答和产品使用相关的问题,其他问题礼貌拒答并建议联系人工。回答不超过 200 字。"——角色、边界、格式三样齐了。而坏的写法是堆一大段公司介绍和客套话,模型反而不知道该干嘛。

model字段填的是模型标识,不同后端标识不同。这里有个细节:模型选择不是越强越好,而要看"任务复杂度 vs 成本 vs 延迟"的三角平衡。简单意图识别用轻量模型就够,复杂推理再用强模型,这个思路后面性能章节我还会展开。

3.2 工具的定义:一个 Kotlin 函数怎么变成模型能调的能力

工具(Tool)是 Agent 真正"干活"的手。在 ADK 里定义工具,本质上是把你的函数"描述"给模型看,让模型知道这个函数叫什么、能干什么、要传什么参数,然后由框架负责在合适的时机调用它。

ADK 提供了函数式工具的方式,大致是通过注解把方法暴露出去。一个查天气的工具写起来像这样:

class WeatherTool : FunctionTool() { override fun name() = "get_weather" override fun description() = "查询指定城市的实时天气。当用户询问天气、气温、是否下雨时调用。" @Schema(description = "城市名称,使用中文,例如:杭州") fun getWeather(city: String): Map<String, Any> { // 真实场景这里去调用你的气象数据源 return mapOf( "city" to city, "temperature" to 24, "condition" to "多云转晴", "humidity" to 62 ) } }

这里有几个关键细节,都是踩过坑才知道的。第一,description不是写给人看的,是写给模型看的,它决定了模型什么时候会想到调用这个工具,所以描述里要写清楚触发场景,比如上面我专门加了"当用户询问天气时调用"。第二,参数说明同样重要,@Schema里的描述会影响模型传参的准确率,参数含义模糊的时候模型经常传错。第三,返回值我用了Map而不是自定义对象,因为这个结果会被序列化后交给模型阅读,结构越简单模型理解得越准。

注意:工具的描述文字会占用上下文长度。如果你的工具很多,每个都写一大段描述,token 消耗会非常可观,模型的注意力也会被稀释。描述要精准但不冗长。

再补充一个设计上的原则:一个工具只做一件事,不要写"万能工具"。我见过有人把增删改查塞进一个工具里靠参数区分,结果模型经常搞不清该传哪个操作值,调用错误率很高。拆成独立的工具,虽然数量多了,但每个的成功率高得多。

3.3 Session 和 State:多轮对话的状态到底存在哪

这是很多人第一次用 Agent 框架会困惑的地方。大模型 API 本身是无状态的,每次请求都得把完整历史带上;而Session就是帮你管理"这次对话的完整上下文"的对象。它包含两样东西:一个是事件历史(用户说了什么、模型回了什么、调了哪些工具),一个是状态字典(你可以往里塞任何想跨轮次共享的数据)。

SessionService负责会话的创建、读取和持久化。框架自带内存实现,适合开发;生产环境下你得换成基于数据库或者缓存服务的实现,因为多实例部署的时候内存会话根本没法共享。这是我强烈建议早点规划的一点——很多项目最早图省事用内存会话,等要上线了才发现多副本之间状态对不上,那时候改起来牵动的地方特别多。

State的用法很灵活。比如你可以在状态里存一个"用户等级",然后每次对话开始前读出来,动态调整模型的行为。它的作用是把那些不该让模型自己记、但需要跨轮次保持一致的信息固定下来。

session.state()["user_tier"] = "premium" session.state()["preferred_language"] = "zh-CN"

这里要提醒一个概念上的区分:State是给"当前这次会话"用的,不是长期记忆。会话结束它就没了。如果你要的是"这个用户上次聊过什么、有什么偏好"这种跨会话来记住的信息,那要用的是下一节的 Memory。

3.4 Memory 与回调:长期记忆和流程钩子

Memory解决的是跨会话的记忆问题。它把历史对话提炼成可检索的内容,下次用户来的时候可以按相似度召回相关片段塞进上下文。这个机制的实现细节框架给了接口,具体用向量库还是关键词检索,取决于你接的后端。我的经验是,记忆功能不要一上来就开,先把无记忆的闭环跑通,再逐步引入,否则一旦回答出问题,你很难判断是模型的问题、工具的问题还是召回内容的问题。

回调(Callback)是另一个实用工具。它允许你在 Agent 执行流程的特定节点插入自己的逻辑,比如每次模型调用前打日志、每次工具调用后做审计、出错时做兜底。这几个钩子在生产环境里特别重要,因为 Agent 的行为天然带有不确定性,你得有地方能观测它、约束它。框架提供了模型前、模型后、工具前、工具后等几个回调点,用起来就是把函数注册进去,具体签名以官方文档为准。

我个人特别看重工具调用前的回调,因为你可以在这里做参数校验和权限判断——模型传的参数不一定靠谱,在真正执行前拦一道,能避免很多意外。

4. 完整实操:从零写一个能查天气、能记账的 Kotlin Agent

纸上谈兵到这儿够了,我们从头搭一个稍微像样点的东西:一个个人助理 Agent,能查天气、能记账、能按日期查账。这个例子虽小,但把工具定义、多工具路由、会话状态、流式输出全串起来了,你可以直接拿来改。下面我按"需求拆解 → 工具实现 → 组装 → 运行"的顺序走一遍。

4.1 需求拆解与工具划分

先想清楚要哪些能力。查天气是一个工具,记一笔账是另一个工具,按日期查询账目是第三个。这里有个设计细节值得讲:记账的时候要记日期、金额、类别、备注,查询的时候按日期范围查。要不要把"查询"和"统计"合成一个工具?我倾向于分开,因为统计逻辑复杂、参数不同,混在一起模型的传参准确率会下降。

工具清单如下:

工具名作用关键参数触发场景
get_weather查实时天气city用户问天气、气温
add_expense记录一笔支出amount, category, note用户说"记一笔""花了多少"
query_expense按日期查支出startDate, endDate用户问"某天花了多少"

4.2 工具函数的 Kotlin 实现

记账工具我用内存列表模拟存储,实际项目里换成数据库即可。这里重点是感受一下参数定义的写法。

class ExpenseRepository { private val records = mutableListOf<Expense>() fun add(e: Expense) { records.add(e) } fun query(start: String, end: String): List<Expense> = records.filter { it.date >= start && it.date <= end } } data class Expense( val date: String, val amount: Double, val category: String, val note: String ) class AddExpenseTool(private val repo: ExpenseRepository) : FunctionTool() { override fun name() = "add_expense" override fun description() = "记录一笔支出。当用户说记一笔、花了多少钱、消费了时调用。" @Schema(description = "金额,单位元,正数") fun addExpense( @Schema(description = "支出类别,如餐饮、交通、购物") category: String, @Schema(description = "金额,单位元") amount: Double, @Schema(description = "备注说明") note: String, @Schema(description = "日期,格式 yyyy-MM-dd,默认今天") date: String ): Map<String, Any> { if (amount <= 0) return mapOf("success" to false, "msg" to "金额必须为正数") repo.add(Expense(date, amount, category, note)) return mapOf("success" to true, "msg" to "已记录:$category $amount 元") } }

这里我特意在函数里加了一个金额校验。为什么?因为模型传参不一定永远正确,与其让它把一条错误数据写进库里,不如在工具层面挡掉,返回一个"失败"的结果让模型去跟用户澄清。这个"工具层做防御"的思路,是我做 Agent 项目时反复强调的一点——永远不要假设模型的输入是对的。

4.3 组装 Agent 与 Runner

工具齐了之后,把它们挂到 Agent 上,再接上运行器。

val expenseRepo = ExpenseRepository() val assistant = LlmAgent.builder() .name("personal_assistant") .model("gemini-2.0-flash") .description("个人生活助理,能查天气和管理日常支出") .instruction( """ 你是一个个人助理。根据用户意图选择合适的工具。 规则: 1. 记录支出前,如果用户没说类别,先追问类别,不要瞎猜。 2. 查询支出时若用户没给日期范围,默认查询今天。 3. 涉及金额一律用阿拉伯数字,保留两位小数。 4. 回答简洁,不要重复用户的话。 """.trimIndent() ) .tools( WeatherTool(), AddExpenseTool(expenseRepo), QueryExpenseTool(expenseRepo) ) .build() val runner = InMemoryRunner(assistant)

注意instruction里那几条规则。这里的第二条"没说日期默认查今天"和第一条"没说类别要追问",其实是在用自然语言给 Agent 定行为边界。这类规则的写法很讲究:能明确说清的就说清,模糊的交给模型判断。我试过把每条规则都写得无比细致,结果模型反而变得死板,遇到稍微变化的说法就不知道怎么处理了。指令的粒度需要反复调,没有标准答案。

4.4 流式输出与多轮交互

最后是驱动它跑起来。流式输出对体验影响很大,尤其是回答较长的时候,等待整段生成完再显示会让人觉得很卡。

suspend fun chat(runner: InMemoryRunner, userId: String, sessionId: String, text: String) { runner.runAsync(userId, sessionId, Content.fromParts(Part.fromText(text))) .asFlow() .collect { event -> event.content()?.parts()?.forEach { part -> part.text()?.let { print(it) } } } println() }

多轮交互的关键在于复用同一个sessionId。每次调用都传同一个会话 ID,框架就会自动把这一轮追加到历史里,模型下一轮就能看到之前说过什么。如果每轮都新建会话,那每次对话都是失忆的,这个坑我踩过,当时排查了半天才发现是会话 ID 没复用。

实操心得:调试的时候把每轮收到的完整 event 打出来,包括模型的思考过程、工具调用的参数和返回值。很多"模型答非所问"的问题,看一眼工具实际收到的参数就明白了,比反复改指令有效得多。

5. 多 Agent 协作与工作流编排:把复杂任务拆开

单个 Agent 能应付的场景有限。任务一复杂,一个 Agent 挂十几个工具,模型的判断准确率就会明显下滑——工具越多,选错的概率越大。这时候正确的做法是拆成多个专职 Agent,让它们各管一摊,再通过编排组合起来。ADK 在这方面提供了几种现成的范式,理解它们的适用场景比会用更重要。

5.1 顺序、并行、循环三种基础编排

顺序执行的意思很直白:前一个 Agent 的输出作为后一个的输入,像流水线一样。典型场景是"信息收集 → 分析 → 生成报告"这种有明确先后依赖的流程。ADK 里通过SequentialAgent之类的组合器把子 Agent 串起来,每个子 Agent 通过共享的会话状态传递数据。

并行执行是几个 Agent 同时干活,最后汇总结果。适合互相独立的子任务,比如同时查天气、查日历、查交通,然后合成一个出行建议。它的好处是省时间,坏处是得处理结果汇总的逻辑,不能让几个 Agent 的输出互相打架。

循环执行是让一个 Agent 反复跑直到满足某个条件,或者跑固定的轮数。适合"审稿-修改-再审"这类需要迭代打磨的任务。这里要特别注意设置最大轮数上限,否则一旦条件永远不满足,就会陷入死循环烧掉大量额度。我给所有循环都设一个硬上限,宁可提前退出也不放任它转。

5.2 LLM 驱动的动态路由

比固定编排更灵活的是让模型来决定走哪条路。做法是设一个"调度 Agent",把可选的子 Agent 作为它的下级,用户请求进来后由调度 Agent 判断该交给谁。举个例子,一个客服系统下有"售前咨询""售后维修""账单查询"三个子 Agent,用户问的问题由调度层判断归谁管。

这种方式的优点是能处理开放式输入,用户不用按固定格式提问;缺点是调度判断本身也会出错,而且多了一层模型调用,延迟和成本都会增加。我的经验是,子 Agent 的description在这个场景里格外关键,调度 Agent 主要靠这些描述来做判断,所以每个子 Agent 的描述要写得互斥、清晰,别出现两个 Agent 的描述都很像的情况。

编排方式适用场景主要风险
顺序有明确先后依赖的流水线中间环节失败会卡住整条链
并行子任务互相独立结果汇总逻辑复杂
循环需要迭代打磨的任务不设上限会死循环
LLM 路由开放式输入、多种意图调度误判、额外延迟成本

5.3 人工确认环节:什么时候必须让人介入

有些操作不能全交给模型自动执行,比如涉及付款、删除数据、发送对外消息。ADK 支持在工具调用前插入人工确认的环节,让流程暂停等人拍板。实现方式通常是在工具执行前抛出一个"需要确认"的信号,由外层应用捕获后展示给用户,用户确认了再继续。

这个机制看着简单,但在实际业务里非常关键。我参与过一个自动化审批的项目,起初所有操作全自动,结果模型把一批本来该人工复核的申请直接通过了。后来加了确认环节,高风险操作必须人工点一下,问题就解决了。判断标准我一般这么做:只读操作全自动,写操作按风险分级,涉及资金和对外的必须确认。

6. 常见问题与排查技巧实录

这部分是纯实战经验,都是我自己和身边人踩过的坑。Agent 项目的报错往往不那么直观,因为出错的可能不是代码,而是模型的行为。下面按问题类型整理,附上排查路径。

6.1 依赖冲突与版本对不齐

最常见的就是编译期或运行期的类找不到、方法签名不匹配。这类问题九成是依赖版本冲突。排查方法:先跑./gradlew dependencies,找到 ADK 及其传递依赖的版本,看有没有同一个库出现多个版本。如果有,用resolutionStrategy强制统一。

还有一个隐蔽的坑是 Kotlin 版本和 Java 目标版本不匹配。症状是编译能过但运行时报NoSuchMethodError。解决办法是明确设置jvmTarget和 Java 兼容版本,别让它们各自为政。

6.2 工具调用失败的排查路径

模型该调工具却没调,或者调了但参数不对,这是最高频的问题。我总结的排查顺序是这样的:第一,看工具的description是不是太模糊,模型没意识到该调用它;第二,看参数名和描述是否清晰,模型传参错多半是这里的问题;第三,看是不是工具太多导致模型选择困难,试着拆分成多个 Agent;第四,把模型返回的原始响应打出来,看它到底有没有生成工具调用请求。

避坑技巧:给工具名起名要有区分度。我见过两个工具叫querysearch,模型经常搞混。改成query_expense_by_datesearch_weather_by_city之后,准确率立刻上去了。

6.3 上下文超限与 token 控制

多轮对话跑久了会撞上模型的上下文长度上限。症状是对话到一定轮次后突然报错或者模型开始"忘事"。应对方法有几个层次:最直接的是精简历史,只保留最近 N 轮;更进一步是对早期对话做摘要,把摘要而不是原文放进上下文;再进一步是接入记忆检索,只召回和当前问题相关的片段。我一般先用最简单的截断,如果业务确实需要长期记忆,再上摘要和检索。

要提醒的是,工具的定义和描述本身也占上下文。工具一多,光描述就吃掉一大块额度。所以工具设计上要克制,不常用的工具别一股脑全挂上。

6.4 常见问题速查表

现象可能原因处理方向
编译报错找不到类依赖版本冲突检查依赖树,统一版本
运行时报方法不存在Kotlin 与 Java 目标版本不匹配对齐 jvmTarget 和兼容版本
模型不调用工具工具描述模糊或工具太多优化描述,拆分 Agent
工具参数传错参数说明不清晰完善参数描述,加类型约束
对话久了报错上下文超限截断历史或做摘要
多实例状态不一致用了内存会话换成外部持久化会话
循环停不下来没设最大轮数加硬上限
回答风格飘忽指令不够明确补充角色和格式约束

6.5 几条花钱买来的经验

最后说几条不太好归类但很值钱的经验。第一,永远先在开发环境用小额度跑通完整闭环,再去连真实业务系统,因为 Agent 的不确定性意味着它可能以你意想不到的方式调用工具。第二,给所有会写数据的工具加校验和幂等,模型重复调用同一个工具的情况比你想的常见。第三,日志要打全,尤其是每次工具调用的入参和出参,出了问题这些日志就是唯一的线索。第四,成本和延迟要尽早监控,Agent 的多轮循环很容易在你不注意的时候把额度吃掉。

7. 部署与工程化的几句实话

把 Demo 跑通和上线是两码事。部署这块我踩过的坑主要集中三点。会话存储必须外部化,前面反复提过,内存会话在多实例下必挂。可观测性必须跟上,Agent 的行为不像传统程序那样确定,你得能看到它每一步在干什么,日志、链路追踪、每次模型调用和工具调用的记录,一个都不能少。成本控制要有手段,设置超时、限制最大轮数、对高频操作做缓存,这些都是常规操作。

关于模型选择,我的建议是分级使用。简单的意图识别用轻量模型,复杂的推理和生成用强模型,这样能明显压成本。ADK 支持在 Agent 层面指定模型,也可以在多 Agent 场景下给不同的子 Agent 配不同模型,这个灵活度要用起来。

至于从哪个方向继续深入,我个人的路径是先把单 Agent 加工具这块彻底吃透,包括错误处理、状态管理、安全边界;再往多 Agent 编排走;最后才是记忆和检索这些进阶能力。贪多嚼不烂,Agent 这东西每加一层复杂度,排查难度都是翻倍的。

我在实际使用中的体会是,ADK 这类框架的价值不在于它帮你省了多少行代码,而在于它把 Agent 的各个组成部分规范化了,让不同人写出来的东西有共同的骨架可循。真正决定项目成败的,还是你对业务的理解、对边界的把控,以及对"模型可能会犯错"这件事的敬畏。工具调用前的校验多做一道,生产环境里就能少一次事故。

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

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

立即咨询