斗破Google ADK Java苍穹-第1境斗之气-聚气识势-ADK边界
斗之气:先画边界图,再写第一行 Agent 代码
选错框架的成本,不在 API 有多难,而在边界画错之后要拆多少代码。
[读者] 会 Spring Boot、能调通大模型 HTTP 接口的 Java 工程师
[痛点] 面对 ADK Java、LangChain4j、Spring AI 等一堆框架,先学 API 却说不清自己要交付什么
[现在读] 数字人项目刚立项,边界没画清就写代码,后面每加一项能力都要返工
[读完] 能用一张需求图划清 ADK Runtime 与业务层、媒体层的边界,并跑通本境验收
[旧方案] 一个 Controller + 一次 chat 调用,封装成 ChatService 就当"数字人"
|
v
[新需求] 要注册登录、项目 CRUD、Agent 配置、Web 运行、Tool、Session、Workflow、MCP/A2A、Live、Eval
|
v
[冲突] ChatService 里根本没有"会话状态、事件流、工具治理、可恢复执行"这些概念的位置
|
v
[后果] 每加一个能力就在 Controller 里打补丁,最终不可评测、不可恢复、不可复制
我是老李,在深圳一家做企业协同软件的公司带后端团队。上个月公司拍板做一个数字人平台:客户注册登录后能建项目、给自己的数字人配置人格与工具、在网页里和它连续对话、断线重连还能接着聊。
需求方给的能力清单有十一项,写在白板上:注册登录、项目 CRUD、Agent 配置、Web 运行、Tool、Session、Workflow、MCP/A2A、Live、Eval、Production。团队三个人,两个熟 Spring Boot,一个写过 Python 版 LangChain 的 demo。
版本基线当场钉死:google-adk 1.11.0、JDK 21、Maven 3.9+;代码从 Git 分支 chapter/01-positioning 开始,第一次提交的意图就写在 commit message 里——把 ADK Java 的运行时学习地图说清楚。
那么问题来了:如果一开始就把边界画错,代价是浪费两周,还是返工三个月?
在只允许以官方资料为准的前提下,我该用什么标准判断哪些能力必须落在 ADK Runtime,哪些必须留给业务和媒体层?
一个连一次 LLM 调用都要手写的数字人,它的第一境到底要交付什么,才不算白写?
01、故事
立项会那天,白板上的十一项能力被画成一排方框。有人提议:先写个 Demo 把模型调通,其余后面再说。
我们照这个思路做了第一版。Spring Boot 起一个 /chat 接口,里面一个 ChatService:拼 System Prompt、从 Redis 里取最近十条消息、用 RestTemplate 打模型 HTTP 接口、把回复写回 Redis。前端做一个输入框和一个渐显的打字机效果。
任务是:一个月内让客户在网页上跟自己的数字人说上话,而且这套东西后面要能长成白板上那十一项能力。
前三周反馈不错。第四周开始变味。运营说,客户问同一件事,数字人每次都像第一次见;研发说,想加一个"查项目接口清单"的工具,得在 ChatService 里加 if-else 分支;老板说,这套东西能不能私有化交付给三个客户——不能,因为 Prompt、模型名、工具逻辑、Redis Key 约定全糊在一个类里。
真正的冲突不是"模型回答得不够好",而是我们写的是一个聊天服务,需求方要的是一个数字人的执行环境。
需求未画清
代码先落地
边界若含糊
返工必成灾
02、问题
旧方案失效得很彻底。ChatService 能表达的只有一件事:一次请求进、一段文本出。它没有任何地方可以安放"这次执行属于谁、用到哪些工具、状态在哪一层、失败了能不能续"。
业务影响分三块。第一,续聊体验断裂:没有规范的会话与状态模型,多轮上下文只能靠 Redis Key 约定硬撑。第二,工具无法治理:工具调用散在业务分支里,没有注册、没有边界、没有审计。第三,不可交付:私有化客户要改 Prompt、换模型、关掉某个工具,我们都得改代码重新打包。
技术表现也很直白:模型调用没有事件流,前端拿不到过程态;重试没有幂等键,工具可能被调两次;Prompt 版本不可追溯,线上回答变差时无法归因。
于是本境的完成标准不是"把十一项做完",而是更窄、更硬的一条:把十一项能力全部归位。每一项属于哪一层、由谁负责、用什么接口暴露,必须写在图里;并且本境所有能力要能在同一个数字人项目中运行并有测试结果。
归位错了,就是返工;归位对了,后面十境才有地方长。
旧法能开口
新求要长存
一处撑不住
处处打补丁
03、原理
要以官方资料为准建立知识源,第一步是纠正一个默认假设:ADK Java 不是"Java 版 ChatClient"。
按 Google ADK 的官方定位,它是一个 code-first 的 Agent 开发工具包,围绕 Agent 定义、工具、运行时、会话与评测提供一整套抽象。Java 版的意义在于:让已有 Java 服务不必再为一个 Python Agent 服务做胶水层。具体到 1.11.0 已覆盖哪些能力,需按当前官方文档核验。
把它的七个核心词按职责摆开,边界立刻清楚:
- Agent:执行的"主体",定义人格、指令、模型与可用的工具集。
- Tool:主体可以调用的能力,必须可声明、可发现、可治理。
- Workflow:把多个 Agent 或步骤编排成确定性流程。
- Session:一次持续存在的对话与状态容器。
- Runner:执行入口,负责把一次请求变成一次 Invocation。
- Event:执行的产出流,是状态变化的事实记录,不只是文本。
- Runtime:以上所有东西共同的运行环境,也是所有横切关注点的落点。
反直觉判断:Agent 框架的价值不在于把模型调用变简单,而在于把"一次执行"变成可描述、可拦截、可恢复的对象。如果你的系统里连"一次执行"这个概念都无处安放,那么框架再强你也用不上。
第二个判断放在这里先说一半:能力清单越长,需要你亲手写的代码反而越少——因为你必须把越来越多的东西交还给 Runtime 去管,而不是继续写 if-else。
框架非桥接
乃执行之地
边界先认清
代码才生根
04、架构
[输入] 用户请求 + appName + userId + sessionId
|
v
[模块] Agent Definition(LlmAgent / Workflow Agent / 工具注册)
|
v
[数据/状态] InvocationContext + Session.State + Artifact
|
v
[处理] Runner 驱动一次 Invocation,Event 流持续外放,工具调用受治理
|
v
[输出] Event 流 → 业务层归约 → 落库 / 前端展示 / 审计
这张图里,横向是数据流,纵向其实是一把刀:它把所有能力切成五层,这就是本境要建立的 Harness 五层模型。
第一层 Agent Definition:谁在执行。LlmAgent、Workflow Agent、工具清单、指令与人格,全部属于这一层,归 ADK 管。
第二层 Execution Harness:怎么执行。Runner 装配、回调拦截、重试与幂等策略,属于这一层,一半归 ADK、一半归我们的装配代码。
第三层 Runtime Facts:执行过程中发生了什么。Event 流、Invocation 标识、工具调用记录,属于这一层,事实来自 Runtime,归约逻辑归业务。
第四层 State & Resources:执行之外还记得什么。Session、Memory、Artifact,属于这一层,存储实现可以换,语义由 ADK 定义。
第五层 Production Guard:什么情况下不许执行。鉴权、限流、审计、配额、内容边界,属于这一层,完全归业务,Runtime 只提供拦截点。
边界清楚了,收益也清楚了:注册登录、项目 CRUD、Agent 配置持久化属于业务层;Live 的音视频编解码、TTS/ASR 属于媒体层;剩下那七项能力,绝大多数要落在 ADK Runtime 上。
代价同样要说清:引入 Runtime 抽象意味着学习曲线变陡、调试链路变长、日志需要重新设计。如果你的场景只是一次性问答脚本,这套边界是负担,不是资产。
五层分明处
各守一方天
越界即隐患
守界即自由
05、实战一次
先跑通最小的 V1:环境、依赖、配置、实现、启动、请求、验证。
环境与版本:JDK 21、Maven 3.9+、google-adk 1.11.0。
pom.xml 只加一个依赖,把基线钉死:
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>digital-human</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<maven.compiler.release>21</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<adk.version>1.11.0</adk.version>
</properties>
<dependencies>
<dependency>
<groupId>com.google.adk</groupId>
<artifactId>google-adk</artifactId>
<version>${adk.version}</version>
</dependency>
</dependencies>
</project>
核心实现:一个 Agent、一个 Session、一次 Runner 驱动的执行,把 Event 打出来。
package com.example.digitalhuman.runtime;
import com.google.adk.agents.LlmAgent;
import com.google.adk.events.Event;
import com.google.adk.runners.Runner;
import com.google.adk.sessions.InMemorySessionService;
import com.google.genai.types.Content;
import com.google.genai.types.Part;
import io.reactivex.rxjava3.core.Flowable;
public final class PositioningDemo {
private static final String APP_NAME = "digital-human";
private static final String USER_ID = "u_1001";
public static void main(String[] args) {
LlmAgent agent = LlmAgent.builder()
.name("digital_human")
.model("gemini-2.0-flash")
.description("第一章:只负责确认 Runtime 边界")
.instruction("你是数字人的骨架,只回答与项目边界有关的问题。")
.build();
InMemorySessionService sessionService = new InMemorySessionService();
Runner runner = Runner.builder()
.agent(agent)
.appName(APP_NAME)
.sessionService(sessionService)
.build();
var session = sessionService
.create(APP_NAME, USER_ID, null)
.blockingGet();
Content userMsg = Content.fromParts(
Part.fromText("列出你必须负责的运行时能力。"));
Flowable<Event> events = runner.runAsync(USER_ID, session.id(), userMsg);
events.blockingForEach(event -> event.content().ifPresent(content ->
content.parts().ifPresent(parts -> parts.forEach(part ->
part.text().ifPresent(text ->
System.out.println("[event] " + text))))));
}
}
启动与请求:
# 模型凭证的配置方式以官方文档为准
export GOOGLE_API_KEY=<your-key>
mvn -q -DskipTests package
mvn -q exec:java -Dexec.mainClass=com.example.digitalhuman.runtime.PositioningDemo
未在当前环境实测,以下为预期结果。
示例输出:
[event] 我必须负责:会话状态、工具调用、事件外放、执行上下文、可恢复的中断。
验证点有三条,必须逐条确认,而不是"看着像跑通了":第一,同一 sessionId 再跑一次,上下文能否延续;第二,事件流是否出现多条而非单条,说明它确实是流不是字符串;第三,把这段执行封装成测试类后,断言是否稳定通过。
需要说明的是,LlmAgent、Runner、SessionService 的类名、构建器字段与返回类型,请以 ADK Java 1.11.0 官方 API Reference 与官方 Samples 为准;Java 版对 Live、Eval 等能力的覆盖范围也需按当前官方文档核验。本境先把边界立起来,具体 API 逐章补齐。
先跑一行码
再见全图景
能跑才算数
能验才敢信
06、排查
V1 跑通之后,问题立刻出现了两条。
第一条:事件流打印出一堆碎片,前端只收到半句话。
现象:控制台连续刷出多条 [event],内容前后拼不完整,前端展示时像被截断。
怀疑:模型输出被 token 上限截断。
检查:在消费端打印每个 Event 的标识、作者、是否是增量片段、是否是本轮终态,对照官方对 Event 语义的说明。
证据:一次 Invocation 产生的是一个 Event 序列,其中既有增量片段,也有表示本轮结束的终态事件,业务层却把它们当成了一条条独立消息往库里写。
根因:把 Event 流当成了"消息列表",而不是"一次执行的状态变化流"。
修复:业务层只对终态事件落库与展示,增量事件仅用于打字机效果,两个通道分开处理。
错误尝试: 一开始我把请求超时调大、把输出上限调高,试图让模型"一次吐完整"。为什么错:问题不在模型侧,而在消费侧对 Event 语义的误判。调参只会掩盖现象,等并发上来后碎片更多,问题会更难查。
第二条:一个工具在一次对话里被调用了两次,写了两条库记录。
现象:日志里同一 userId 的工具调用出现两次,业务表多出一条重复数据。
怀疑:模型不稳定,重复发起工具调用。
检查:在工具实现里打印调用来源与 Invocation 上下文,对比两次调用之间的差异。
证据:第一次调用来自 Agent 的 function call,第二次来自业务层"顺手"再调一次,为了给前端补一份返回值。
根因:工具定义在业务层,Runtime 完全不知道它的存在,也就无法保证它的执行语义唯一。
修复:工具只注册到 Agent Definition,业务层只消费 Event,不再重复调用;写操作补幂等键。
这两条诊断链指向同一个结论:绝大多数"看起来像模型的问题",其实是边界问题。
现象多迷惑
证据定根因
调参非良药
归因是正途
07、优化
基于第 06 章的证据,V2 不改模型、不改 Prompt,只改结构:把代码目录按 Harness 五层切开。
src/main/java/com/example/digitalhuman/
agent/ # L1 Agent Definition:LlmAgent、Workflow Agent、工具注册
harness/ # L2 Execution Harness:Runner 装配、回调、重试与幂等
facts/ # L3 Runtime Facts:Event 归约、执行事实、审计事件
state/ # L4 State & Resources:Session、Memory、Artifact
guard/ # L5 Production Guard:鉴权、限流、配额、内容边界
api/ # 业务层:注册登录、项目 CRUD、Agent 配置
media/ # 媒体层:Live 音视频接入
修改一是把工具全部收敛到 agent/ 下统一注册。原因:第 06 章第二条诊断链已经证明,工具定义一旦外溢,Runtime 就无法保证执行语义。新行为:任何一次工具调用都能从 Event 流里被观察到,业务层不再有"顺手再调一次"的路径。验证方式:构造一次对话,断言工具调用次数等于事件中记录的工具调用次数。
修改二是引入执行事实归约层。原因:第 06 章第一条诊断链证明,增量事件与终态事件必须分通道。新行为:facts/ 负责把 Event 流归约成对业务有意义的执行事实,api/ 只依赖归约结果。验证方式:用同一段事件序列做输入,断言归约输出稳定且可重复。
修改三是把鉴权、限流、配额挪进 guard/,只在 Harness 的回调点上挂载。原因:这些判断散落在 Controller 里时,Agent 配置一换就得重审一遍。新行为:治理策略与 Agent 定义解耦,换 Agent 不动治理。验证方式:切换两套 Agent 配置,断言治理规则命中情况一致。
V2 并没有让功能变多,它只是让"谁负责什么"第一次变得可读。
根因既已明
结构随之心
包界即人界
一改见真章
08、演进
[同一输入] 用户在网页里说:帮我查一下这个项目的接口清单
|
+--[V1] Controller 直调模型,手工拼 Prompt,Redis 存最近十条
| 代价:无法续聊、无法审计、无法评测、无法私有化交付
|
+--[V2] Runner 驱动 Agent,Event 流回业务层,Session 与工具进 Runtime
| 代价:学习曲线变陡、调试链路变长、日志体系要重做
|
[Trade-off]
得到:可描述、可拦截、可恢复的执行边界
失去:一部分"随手写"的灵活性与短期速度
适用边界:能力清单 ≥ 5 项、需要多轮状态或工具治理时成立;一次性问答脚本不必上
正确性上,V2 把工具调用与状态变更收进了 Runtime 的事实流,重复写库这类问题从"靠人盯"变成"结构上不成立"。
稳定性上,V2 的故障面更集中:出问题优先看 Event 流与 Session 状态,而不是在 Controller 里逐个断点。
复杂度上,V2 明显更高。多出来的不是代码行数,而是概念数量——Invocation、Event、Session、Tool 注册,每一个都要团队理解一遍。这是必须付的学费。
成本上,短期开发速度会下降,尤其在第二、三章把工具和会话真正落地的时候;长期看,私有化交付、换模型、加工具这三件事的成本会显著下降。
适用范围上,只要你的系统满足"多轮 + 有工具 + 要交付多份"中的任意两条,V2 的结构就是划算的。
遗留问题也很明确:Live 与 Eval 在 ADK Java 1.11.0 中的具体支持程度,需按当前官方文档核验;MCP 与 A2A 的接入方式要等到后续章节才能验证;本境的工程基线还缺一套稳定的测试骨架。
同问不同答
旧新各有价
取舍写明白
演进才可续
09、洞见
9.1 边界不是代码分层,是权力划分
五层模型最容易被误读成"包结构规范"。它真正规定的是权力:谁能改变执行结果,谁只能观察,谁只能拒绝。Agent Definition 能改变执行结果;Runtime Facts 只能记录;Production Guard 只能拒绝。写代码之前先把这三种权力分清楚,包结构是结果,不是原因。
9.2 反直觉判断:框架越强,你写的业务判断越少,但你要想清楚的事越多
很多人以为引入 ADK 是为了少写代码。前半句对,后半句更重要——Runtime 接管了执行之后,你必须提前回答"什么算一次执行、什么算终态、什么算可恢复"。这些问题以前被 if-else 掩盖着,现在被摆到台面上。少写的是胶水,多写的是判断。
9.3 反直觉判断:第一版要交付的不是功能,而是归位表
十一项能力如果第一版就动手做,一个月后你得到的是十一个补丁。第 02 章定的完成标准之所以故意收窄到"归位",正是因为归位错了,后面每一境都要为它买单。第一境的产出物可以是一张图、一份包结构、一次能跑的 Demo,但不能是一堆没有归属的功能。
9.4 选型应该比抽象边界,而不是比功能表
AgentScope Java、LangChain4j、Spring AI Alibaba 与 ADK Java 放在一起,最容易做的是功能对照表,最没用也是这个。更有用的问法是:它把你的一次执行描述成什么对象?它的状态和资源由谁定义?它的治理点在哪里?本轮只比较抽象边界,不做大而全评测,也不给谁下结论——具体能力请以各自官方文档为准。
洞见非玄谈
皆由证据生
反直觉之处
往往是真相
10、系统落地
原来有什么:一个 Spring Boot 工程,一个 ChatService,一个 Redis Key 约定,没有测试骨架,没有可交付的配置边界。
本篇新增了什么:一张 Harness 五层边界图、一份按层切分的包结构、一个由 Runner 驱动并外放 Event 的最小可运行 Agent、一份把十一项能力逐条归位的清单。同时把版本基线固定在 google-adk 1.11.0、JDK 21、Maven 3.9+,代码起点放在 chapter/01-positioning 分支,第一次提交的意图是"把 ADK Java 的运行时学习地图写清楚"。
现在能做什么:能说清每一项能力该由谁负责;能让数字人第一次由 Runtime 驱动而不是由 Controller 驱动;能在出问题时先看 Event 流再看业务代码。
还缺什么:缺工具的真实注册与治理(下一境)、缺会话与记忆的持久化语义、缺统一的生产防护层、缺评测与可观测手段、缺对 Live 与 MCP/A2A 的验证。
下一步如何演进:先把 Agent 从"能说话"推进到"有身份、有工具边界",让工具调用第一次进入受治理的轨道。本境的验收必须守住——所有能力都要在同一个数字人项目中运行并有测试结果,不能靠散落的 Demo 拼凑。
落地非堆砌
一步一交付
缺什么写明
下一步有据
11、小结
Q1 → 先错边界不是浪费两周,而是每加一项能力返工一次,最终交付不出去
Q2 → 只认官方文档、API Reference 与 Samples,用"一次执行由谁描述、由谁治理、由谁记录"来判定归属
Q3 → 第一境交付归位表、包结构与一个 Runner 驱动的最小可运行 Agent,功能留给后续境界
状态 → ADK 边界已画清,数字人第一次由 Runtime 驱动,工程基线可继承
本境完成后,数字人仍然不会查数据库、不会记住用户、不会暂停等审批,但它终于知道自己站在哪一层。
三问皆有答
一境终可验
能力已入账
根基始成形
12、作业
12.1 理解题:为什么说 ADK Java 不是"Java 版 ChatClient"?
参考答案:ChatClient 只描述一次请求与一次响应;ADK 描述的是 Agent、Tool、Workflow、Session、Runner、Event、Runtime 构成的一次执行环境。差别不在调用方式,而在这套抽象能否承载状态、工具治理、事件流与恢复。判断方法很简单:如果你的系统里没有地方安放"一次执行",那无论用哪个库,你都还停在 ChatClient。
12.2 实战题:把本境的十一项能力逐条归位到 Harness 五层。
参考答案:Agent 配置归 L1;Runner 装配与回调归 L2;Event 归约与工具调用记录归 L3;Session、Memory、Artifact 归 L4;鉴权、限流、审计、配额归 L5。注册登录、项目 CRUD 属业务层;Live 的音视频编解码属媒体层。工作流编排横跨 L1 与 L2,MCP/A2A 主要落在 L1 与 L2 的工具与外联部分。归位不必唯一正确,但必须能说清理由,并能在同一个项目里跑通验证。
12.3 排障题:事件流打印正常,但前端只显示半句话,先查什么?
参考答案:先查消费端对 Event 语义的假设,而不是先调模型参数。打印每个事件的标识、作者、是否为增量片段、是否为本轮终态,确认业务层是否把增量片段当成了独立消息。若确认是消费侧误判,就分通道处理:增量只做打字机效果,终态才落库与展示。同时补一条断言,防止后续再退化。
12.4 架构判断题:团队只有三个人、两个月交期,是否应该直接采用本境的结构?
参考答案:看两条。若能力清单 ≥ 5 项且需要多轮状态或工具治理,结构划算,短期慢一点、长期少返工;若只是内部一次性问答工具,或明确不打算做工具与续聊,则不划算,Controller 加一个薄封装更合适。关键不是"框架好不好",而是"你的系统里是否存在必须被描述的一次执行"。
作业非负担
乃自证之路
答完四道题
斗气方入体
13、思考
回到本篇的核心冲突:一个只会调用一次 LLM 的数字人,面对十一项能力清单时,第一件该做的事不是选框架,也不是写代码,而是把边界画在需求图上。
因为框架的 API 是可以查的,边界的错位只能靠返工偿还。先把"哪些能力属于 Runtime、哪些属于业务与媒体层"写清楚,再让第一行代码落下去,后面的境界才有地方生长。
这就是斗之气:吸纳斗气、淬炼筋骨之前,先要知道自己要练的是什么,以及力气应该往哪里使。
一念辨边界
一证定根因
一改知取舍
一役见真章

浙公网安备 33010602011771号