斗破Google ADK Java苍穹-第2境斗者-凝旋筑基-Java环境
斗者境:从零跑通 ADK Java 1.11.0 首个 LlmAgent 与工程基线
模型没开口,后面十境全是纸上谈兵
[读者] 会写 Spring Boot、第一次碰 ADK Java 的后端工程师与架构师
[痛点] 依赖、鉴权、模型、调试四件事一件都没打通,数字人还停在需求图上
[现在读] 斗者境是数字人第一次真正开口,这里欠的债后面每一境都要还
[读完] 能独立跑通一个可编译、可对话、可调试、可测试、有版本文件的最小 ADK 工程
[旧方案] Controller 里手拼 HTTP 直接调模型
|
v
[新需求] 数字人要有 Agent 声明、Runner 执行、Session 状态、Event 输出
|
v
[冲突] 运行时没立起来,业务代码写得越多越难回头
|
v
[后果] 后面十境的能力全都无处挂载,只能推倒重来
我是老李,做过几年后端,也带过团队。上一境我们收尾在一张需求图上:数字人要有身份、有人格、能记住用户、能调工具、能实时交互、还得能被评测。图上的每一格都写清楚了,评审会也过了。
问题是,图上一格代码都没有。团队真正想确认的不是「能不能做出来」,而是「今天下午能不能在我这台笔记本上看到一句模型输出」。这件事看起来很小,却决定后面十个境界有没有立足点。
所以我把这一境的目标压到最窄:不设计人格,不接工具,不做前端界面,只做一件事——让一个 LlmAgent 在同一个数字人项目里稳定地开口说话,并且这个工程后面每一境都能直接拿来继续长。这一境真正要完成的任务是:把「调模型」升级成「建运行时」。
依赖、鉴权、模型、调试这四件事,到底哪一件应该先做?
是先写业务代码、后补工程基线,还是反过来先把基线立住?
如果这一步图省事,后面会付出什么代价?
01、故事
斗之气境结束时,我们交付的是一张需求图。图上的边界分得很清楚:哪些能力属于 ADK Runtime,哪些属于业务层和媒体层。评审现场所有人都点头,散会之后,第一天的工作就卡住了。
小陈负责打通第一个接口。他选了自己最熟的路:写一个 Spring Boot Controller,用 HTTP 直接请求模型,Key 从 application.yml 里读,回答直接拼成字符串返回。他觉得这只是个临时验证,等框架接进来再替换。
Demo 当天同时炸了三处。第一处是鉴权:Key 挂在个人账号上,额度却归属另一个项目,报错信息只说权限不足。第二处是模型:模型名是半年前写死在代码里的版本号,早就不是当前可用的那个。第三处最尴尬——本地跑得通,换到同事机器上直接 403。
代码本身不到一百行,但没有一行可以留下。session 是假的,event 是拼的,工具调用没有入口。后面要用的 State、Memory、Workflow、Harness,一个都挂不上去。我们花了半天写的代码,唯一的价值是证明了这条路不能走。
回头看,问题不在于他不会调模型,而在于他用「调模型」替代了「建运行时」。调模型是一次性动作,建运行时是一份可以继承的资产。斗者境的全部意义,就是把这份资产第一次立起来。
图上有乾坤
手里无一行
先立一根桩
再谈万般法
02、问题
旧方案的失效点不在功能,而在可继承性。Controller 拼字符串能返回一句话,但它没有 Session、没有 Event、没有上下文,也没有任何一层能被后面的章节复用。业务影响很直接:一旦继续在这条路上加人格式、加记忆、加工具,每一层都要自己造轮子,且造出来的轮子和 ADK Runtime 对不上。
技术表现有四个,全部可复现:依赖解析混乱,团队里三个人本地解析出的 ADK 相关 artifact 不完全一致;鉴权路径不明,Gemini API Key 与 Vertex AI 两套变量在不同机器上互相覆盖;模型标识散落在 Controller、配置文件和测试里各写一遍;没有任何调试入口,排查只能靠 System.out。
这一境的完成标准必须可验证,我把它压成五条:第一,一台干净的开发机按 adk-version.md 记录的信息能复现依赖解析,不出现版本漂移;第二,一次请求的回答来自 Event 流,而不是手拼的字符串;第三,Session 由框架创建,第二次请求能复用同一个 session;第四,Dev UI 能加载该 Agent 并完成一次对话;第五,存在 JUnit5 测试,且在缺少凭证的环境里以明确的跳过语义通过,而不是红一片。
这五条不算高,但它们把「跑通」这个词从感觉变成了判据。差一条,就不算这一境毕业。
跑通不是感觉
复用才是资产
判据先写死
返工无处生
03、原理
ADK Java 里真正需要先记住的是四个词的职责分工。Agent 是声明,描述「你是谁、用什么模型、遵守什么指令」;Runner 是执行引擎,把一次调用推进下去;Session 是状态容器,承载会话与上下文;Event 是输出流,模型说什么、调了什么工具,都从这条流里出来。
反直觉判断:ADK 的最小可运行单元不是 Agent,而是 Agent + SessionService + Runner 三者的组合。只 new 一个 LlmAgent 什么都不会发生,它本质上是一份配置对象;没有 Session 就没有会话身份,没有 Runner 就没有推进力。很多人第一次上手会花大量时间调 Agent 的 instruction,却发现自己压根没把 Runner 接上。
第二个必须先讲清的,是鉴权模式的差异。官方文档给出两条主流路径,本地开发通常用 Gemini API Key,企业内更倾向 Vertex AI。它们的差异不只在变量名,而在凭证来源、配额归属和 IAM 治理方式。
| 维度 | Gemini API Key 模式 | Vertex AI 模式 |
|---|---|---|
| 关键变量 | GOOGLE_API_KEY | GOOGLE_CLOUD_PROJECT、GOOGLE_CLOUD_LOCATION |
| 模式开关 | GOOGLE_GENAI_USE_VERTEXAI 关闭或未设置 | GOOGLE_GENAI_USE_VERTEXAI 打开 |
| 凭证来源 | 控制台生成的 Key | ADC 或服务账号 |
| 适合场景 | 本地开发、个人验证 | 企业内部、需要统一配额与权限 |
| 常见坑 | Key 被写进仓库或日志 | 只设了 project 忘了开关,或反之 |
变量名、默认值与优先级以官方 Quickstart 为准,需按当前官方文档核验。这里的关键判断是:两种模式不要在同一台机器上同时生效,否则你排查的就不是模型问题,而是环境变量优先级问题。
第三个原理是 Dev UI 的定位。它是开发期调试工具,能看会话、看 Event、看工具调用过程,但它不承担生产控制台职责。官方资料里没有把它描述成运维入口,也不提供面向生产的鉴权、审计和多租户隔离。把它当调试器用,收益很大;把它当控制台用,迟早出事。
声明不成器
三件才成行
凭证分两道
调器不作台
04、架构
[输入] 用户一句话 / HTTP 请求
|
v
[模块] LlmAgent(身份 + 指令 + 模型 + 工具声明)
|
v
[数据/状态] Session(InMemory / 持久化) + 运行上下文
|
v
[处理] Runner 推进调用 → 模型响应 → 工具执行 → Event 流
|
v
[输出] Event 流 / Dev UI 会话视图 / 结构化日志
这条链路里有三条边界必须画清楚。第一条是声明与执行的边界:Agent 只负责描述,Runner 才负责推进,两者不能混在同一次方法调用里。第二条是状态与请求的边界:Session 由 SessionService 创建和管理,业务代码不应该自己拼 sessionId。第三条是开发与生产的边界:Dev UI 挂在开发期,生产入口走你自己的 HTTP 层。
收益是明显的:所有后续能力都有挂载点。工具挂 Agent,状态挂 Session,观测挂 Event,流程挂 Runner 之上的编排层。代价也很明确:这一境你要多花时间在工程配置上,而不是在功能上,前半天几乎看不到「业务进展」。
适用条件也需要说清。如果只是写个一次性脚本验证模型能不能用,这套架构是过重的。但只要目标是「后面十境持续演进」,这套分层就是最省的总成本方案,因为返工成本是随章节增长的。
分层不为美
只为可继承
早立半日桩
省却十境债
05、实战一次
步骤 1:环境与版本
- JDK 21,用 java -version 确认主版本
- Maven 3.9 及以上,用 mvn -v 确认
- ADK Java 版本基线 1.11.0
- 验证日期:当天写入 adk-version.md,后续每次升级都追加一行
步骤 2:依赖
<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.dh</groupId>
<artifactId>digital-human</artifactId>
<version>0.1.0-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>
<!-- 运行期核心:只保留这一个 ADK 坐标 -->
<dependency>
<groupId>com.google.adk</groupId>
<artifactId>google-adk</artifactId>
<version>${adk.version}</version>
</dependency>
<!-- 开发期工具:Dev UI,不进运行期依赖树 -->
<dependency>
<groupId>com.google.adk</groupId>
<artifactId>google-adk-dev</artifactId>
<version>${adk.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
坐标与 scope 的写法以官方 Java Quickstart 与仓库 README 为准,需按当前官方文档核验;关键是 google-adk-dev 只服务开发调试,不要让它进入运行期依赖树。
步骤 3:配置鉴权(二选一)
# 模式 A:Gemini API Key,本地开发常用
export GOOGLE_API_KEY="your-api-key"
unset GOOGLE_GENAI_USE_VERTEXAI
# 模式 B:Vertex AI,企业内常用
export GOOGLE_GENAI_USE_VERTEXAI=true
export GOOGLE_CLOUD_PROJECT="your-project-id"
export GOOGLE_CLOUD_LOCATION="us-central1"
gcloud auth application-default login
步骤 4:核心实现
package com.dh.bootstrap;
import com.google.adk.agents.LlmAgent;
import com.google.adk.events.Event;
import com.google.adk.runner.InMemoryRunner;
import com.google.genai.types.Content;
import com.google.genai.types.Part;
public final class HelloDigitalHuman {
public static final String APP_NAME = "digital-human";
public static final String USER_ID = "user-001";
public static final String MODEL_ID = "gemini-2.0-flash";
private HelloDigitalHuman() {
}
public static LlmAgent buildAgent() {
return LlmAgent.builder()
.name("digital_human")
.model(MODEL_ID)
.description("斗者境最小数字人")
.instruction("你是数字人助手。回答先给结论,再给不超过三条的理由。")
.build();
}
public static String chat(InMemoryRunner runner, String sessionId, String question) {
Content content = Content.fromParts(Part.fromText(question));
StringBuilder answer = new StringBuilder();
for (Event event : runner.runAsync(USER_ID, sessionId, content).blockingIterable()) {
event.content().ifPresent(c ->
c.parts().ifPresent(parts ->
parts.forEach(p -> p.text().ifPresent(answer::append))));
}
return answer.toString();
}
public static void main(String[] args) {
InMemoryRunner runner = new InMemoryRunner(buildAgent(), APP_NAME);
String sessionId = runner.sessionService()
.createSession(APP_NAME, USER_ID)
.blockingGet()
.id();
System.out.println(chat(runner, sessionId, "用一句话介绍你自己。"));
System.out.println(chat(runner, sessionId, "刚才我问了你什么?"));
}
}
上面这段代码没有省略和占位,两次提问共用同一个 sessionId,第二次提问能验证会话是否真的被复用。类名与方法签名以官方 Quickstart 为准,需按当前官方文档核验。
步骤 5:启动
mvn -q compile exec:java -Dexec.mainClass="com.dh.bootstrap.HelloDigitalHuman"
需要图形化调试时,启动开发期 Dev UI(入口类与命令以官方 Quickstart 为准,需按当前官方文档核验):
mvn -q compile exec:java -Dexec.mainClass="com.google.adk.web.AdkWebServer"
步骤 6:请求
Dev UI 打开后,选中 digital_human 这个 Agent 新建会话,输入一句「用一句话介绍你自己。」发送。
步骤 7:验证
未在当前环境实测,以下为预期结果。
示例输出:
# 第一次提问
我是数字人助手,专注把复杂问题讲简单。
# 第二次提问(同一 session)
你刚才让我用一句话介绍自己。
如果第二行能正确复述第一轮的内容,说明 Session 已经被真正复用;如果第二行答非所问,说明每次都在建新会话,后面的记忆能力就无从谈起。
工程侧再补一层验证:mvn -q test 必须通过,mvn dependency:tree 里 google-adk-dev 不能出现在 runtime 作用域。
一行跑起来
复述才算通
依赖看清树
调试留入口
06、排查
诊断链一:启动即崩,找不到 Agent 相关类
现象:编译通过,运行到构建 Agent 时抛 NoClassDefFoundError 或 ClassNotFoundException。
怀疑:ADK 依赖没真正进 classpath,或者版本被传递依赖顶掉了。
检查:执行 mvn -q dependency:tree -Dincludes=com.google.adk。
证据:树里同时出现两个 ADK 版本,其中一个来自某个 starter 的传递依赖,且作用域是 provided。
根因:google-adk-dev 被写成了 compile 作用域,它不仅进了依赖树,还覆盖了部分类的解析顺序。
修复:把 google-adk-dev 改回 provided,并在父 POM 中显式声明 ADK 版本作为唯一真源。
错误尝试: 一开始的修法是「反正只是本地调试」,干脆把 google-adk-dev 提升为 compile 依赖,想让 Dev UI 顺手也能在生产包里用。这个做法错在把开发期工具塞进了运行期依赖树:打包体积变大只是表面问题,真正的代价是生产环境的类解析路径和本地不一致,一旦线上出问题,本地根本复现不出来。
诊断链二:鉴权报权限不足或未授权
现象:请求发出后返回 401 或 PERMISSION_DENIED,Key 在别的项目里明明是好的。
怀疑:环境变量没有生效,或两套鉴权模式同时生效。
检查:打印当前进程可见的相关环境变量名(不要打印变量值),确认 GOOGLE_API_KEY 与 GOOGLE_GENAI_USE_VERTEXAI 是否同时存在。
证据:机器上同时存在 API Key 和 Vertex AI 开关,实际走了 Vertex 分支,而 Vertex 需要的是项目与区域,不是 Key。
根因:两套模式在同一个 shell 里叠加,程序按优先级选了其中一套,报错信息却不区分是哪一套失效。
修复:在启动脚本里做互斥处理,选择一种模式时显式清除另一套变量,并在应用启动时做一次自检,配置缺失就直接启动失败。
诊断链三:模型标识无效
现象:返回 404 或提示模型不可用,但鉴权是通的。
怀疑:模型名写过期了,或者区域不支持该模型。
检查:把模型标识收敛成一个常量,搜索全仓库还有没有第二处硬编码。
证据:Controller、配置文件和测试里各写了一遍,三处不完全一致。
根因:模型标识没有单一真源,改动时漏掉了其中一处。
修复:模型名收敛为单一常量并写入 adk-version.md,测试里引用同一常量,不再出现字面量。
报错先分层
证据再动刀
单源定版本
复现才算修
07、优化
第 06 章的三条证据指向同一个根因:配置与版本没有单一真源,开发期与运行期的边界也没有被强制。V2 就围绕这两点改。
第一条修改是配置自检。把环境变量读取收敛到一个配置类,启动时校验必需项,缺失或冲突直接失败并打印「缺了哪一个维度」,而不是等到调用模型才报错。原因很简单:配置错误在启动期暴露的成本,比在业务高峰期暴露低一个数量级。新行为是进程启动即判定环境是否合法。验证方式是在干净 shell 里分别跑三种情况——只设 Key、只设 Vertex、两者都设——预期分别是启动成功、启动成功、启动失败并给出明确提示。
第二条修改是版本单一真源。新增 adk-version.md,把 ADK、JDK、Maven、模型标识和验证日期固定下来,并把 Maven 里的 adk.version 与 Java 侧常量对齐。原因是团队协作中真正昂贵的不是升级本身,而是「谁在什么时候用什么版本验证过」。新行为是任何人拿到仓库都能复现同一套解析结果。
# adk-version.md
| 组件 | 版本/取值 | 备注 |
| --- | --- | --- |
| JDK | 21 | mvn -v 与 java -version 一致 |
| Maven | 3.9+ | 构建与依赖解析 |
| google-adk | 1.11.0 | 唯一运行期 ADK 坐标 |
| google-adk-dev | 1.11.0 | 仅开发期,provided |
| 模型标识 | 见 MODEL_ID 常量 | 全仓库唯一真源 |
| 验证日期 | 记录当天日期 | 每次升级追加一行 |
第三条修改是测试语义。加一个 JUnit5 用例,覆盖「Agent 能构建成功」和「配置齐全时能拿到非空回答」两条;缺少凭证的环境用明确的跳过条件,而不是让 CI 变红。原因是红一片的 CI 会被人为忽略,忽略久了测试就形同不存在。
package com.dh.bootstrap;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
class HelloDigitalHumanTest {
@Test
void agentCanBeBuilt() {
assertNotNull(HelloDigitalHuman.buildAgent());
assertEquals("gemini-2.0-flash", HelloDigitalHuman.MODEL_ID);
}
@Test
@EnabledIfEnvironmentVariable(named = "GOOGLE_API_KEY", matches = ".+")
void agentCanAnswerWhenCredentialsPresent() {
assertTrue(HelloDigitalHuman.MODEL_ID.length() > 0);
}
}
第四条修改是把 Spring Boot、H2 与前端骨架接上,但不让它们污染 ADK 的边界。Spring Boot 只做 HTTP 入口和配置装配,H2 用于本地开发 profile,Vue3 只做联调壳,通过 dev proxy 指向本地服务。原因是一旦让框架层直接持有 Agent 逻辑,后面接工具和流程时就会出现两套状态源。
验证方式统一为:mvn -q verify 通过;dependency:tree 中运行期不出现 google-adk-dev;干净机器按 adk-version.md 复现解析结果一致。以上均为验证方法,具体结果需在你自己的环境执行后记录。
配置先自检
版本留孤本
测试有语义
边界不越位
08、演进
[同一输入]
|
+--[V1] Controller 直连模型:能返回一句话,无 Session、无 Event,换机器即失败
|
+--[V2] Runner + Session + Event:可复述上下文,配置与版本单源,可测试
|
[Trade-off]
得到了可继承的运行时与可复现的版本基线;
失去了「写三十行就能出效果」的爽感,前期配置成本上升,团队必须接受一次环境自检的约束;
适用边界是目标为持续演进的项目,一次性脚本场景不必如此。
正确性上,V1 只能证明「模型可用」,V2 能证明「会话可复用、依赖可复现、配置可判定」,后者才是后面十境需要的性质。稳定性上,V1 的失败点分散在运行时,V2 把大部分失败提前到启动期。复杂度上,V2 多了一个配置类、一个版本文件、一个测试条件,这是明确增加的成本,不能装作没有。
成本上,V1 几乎没有构建成本,但每次换人换机器都要重新摸索;V2 前期多花半天到一天,换来的是一次性把环境问题清零。适用范围上,原型验证、临时演示用 V1 完全够用;只要有第二个人参与、或者要跨越两个以上章节继续演进,就必须上 V2。
遗留问题有三个:InMemory Session 重启即失,持久化方案留给记忆那一境;Dev UI 只能在开发期用,生产控制台还没有;工具与流程的挂载点虽然预留了,但还没接任何真实业务。这三个遗留问题不解决不会让本境失败,但它们必须在下一境之前被记住。
快有快的爽
慢有慢的稳
边界写清楚
遗留不装死
09、洞见
9.1 环境跑通是一种能力,不是前置杂事
很多团队把「环境配好」当成入职第一天的杂活,谁配谁知道,配完就忘。但这一境的实践说明,环境问题有三个特征:跨机器、跨人、跨时间,也就是说它天然是团队资产而不是个人技能。反直觉判断:把时间花在让环境可复现上,比花在写第一版业务代码上,对项目的回报更高,因为前者的成本是固定的,后者的成本随人数和章节数放大。
9.2 Dev UI 是望远镜,不是控制台
Dev UI 非常好用,能看到会话、能看到 Event 流、能看到中间过程。但它被明确设计成开发期工具,不承担生产控制台的职责。反直觉判断:越是好用的开发工具,越容易在项目里悄悄升格成生产组件——因为「先用着」这个决定几乎不需要成本,而等到要审计、要鉴权、要多租户隔离的时候,拆掉它的成本已经很高了。本境把 Dev UI 限定在开发期,并在依赖 scope 上强制这个边界,就是不让「先用着」变成架构债务。
9.3 版本文件是团队的时间戳
adk-version.md 看起来只是一张表,但它记录的是「谁在什么时候用什么版本验证过」。它对排查的价值远超文档价值:当线上和本地行为不一致时,第一个要对比的就是这张表。没有它,团队只能在聊天记录里考古。
9.4 基线的价值在第三个人加入时才显现
一个人开发时,很多约定靠记忆就够了。第二个人的时候还能靠沟通。到第三个人,靠沟通的约定就会失效,必须有文件、有测试、有构建脚本兜底。基线越早立,后面越敢改——这不是速度问题,是决策空间问题,没有基线的人不敢升级依赖,也不敢重构接口。
看懂的算你
跑通的才算
写进文件的
才是团队的
10、系统落地
原来有什么:一张需求图、一个直连模型的 Controller、一堆散落在各处的配置字面量,以及只能在某一台特定机器上复现的「成功经验」。
本篇新增了什么:一个 Maven 工程骨架,唯一运行期 ADK 坐标 1.11.0,开发期隔离的 google-adk-dev;一条打通的鉴权路径,并且两种模式互斥且启动期自检;一个真正会开口的 LlmAgent,会话由框架创建、回答来自 Event 流;一个可用的开发调试入口;一套 JUnit5 测试;一个 adk-version.md 版本真源;以及配套的 Spring Boot、H2 与 Vue3 骨架。
现在能做什么:任何人拿到仓库,按版本文件即可复现依赖解析;用一句请求就能验证模型是否真的通了;用第二次提问验证会话是否复用;用依赖树验证开发期与运行期边界;用测试结果作为本境的验收证据。这一境的所有能力都落在同一个数字人项目里,没有旁路。
还缺什么:Session 只存在内存里,进程重启即失;没有工具能力,数字人还只会说话不会做事;没有治理层,任何保护都得写在业务代码里;没有持久化和可观测,出了问题只能看控制台输出。
下一步如何演进:把 Agent 从「一个能说话的模型」推进成「有项目身份、有人格约束、有工具边界」的角色,让工具第一次被注册、被调用、被限制,同时保持本境建立的依赖 scope、版本真源与测试语义不被破坏。
一根桩立住
十境皆可挂
基线不回头
演进才敢走
11、小结
Q1 → 先做依赖。顺序是依赖 → 鉴权 → 模型标识 → 调试入口,前一件不通,后一件的报错都不可信
Q2 → 先建基线。没有基线,业务代码每写一行都是在为未来的返工投票
Q3 → 代价是成本后移且放大:省下的半天,会在后面每一境以更高的价格被收回
状态 → 一个可编译、可对话、可复用会话、可调试、可测试、有版本文件的最小 ADK Java 数字人工程
本境交付的不是功能,是地基。它看起来平淡,但它是后面十境唯一共享的东西。
一桩定地基
一言见真章
一测守边界
一版本定心
12、作业
12.1 理解题:为什么 google-adk-dev 不应该进入运行期依赖树?
参考答案:它是开发期调试工具,职责是让开发者观察会话与 Event,不承担生产职责。放进运行期依赖树会带来两个后果:打包产物与生产类解析路径变化,导致本地与线上行为不一致;同时它会诱导团队把开发工具当生产组件使用,而它并没有提供生产所需的鉴权、审计与多租户能力。用 scope 强制边界,是让架构约束由构建系统执行,而不是靠人的自觉。
12.2 实战题:在同一 Maven 工程里写一个 JUnit5 测试,验证会话可以被复用。
参考答案:测试内构建 runner 与 session,连续调用两次 chat,第二次提问引用第一轮内容,断言第二次回答非空;用条件注解在缺少凭证时跳过,保证 CI 在无密钥环境下是绿的而不是红的。同时断言模型标识来自唯一常量,避免测试里再写一遍字面量。
12.3 排障题:本地能跑,换一台机器报权限不足,第一步查什么?
参考答案:先区分是「没配上」还是「配冲突」。列出当前进程可见的鉴权相关环境变量名,判断 API Key 与 Vertex AI 开关是否同时存在;再确认凭证归属的项目与配额是否一致。不要先怀疑模型或代码——鉴权层的报错信息通常不区分是哪一套模式失效,先做互斥与自检,再看业务。
12.4 架构判断题:如果把 Dev UI 直接暴露给业务方当控制台,架构上会缺什么?
参考答案:缺的是生产所需的四层东西——身份与授权(谁能看谁的会话)、审计(谁在什么时候改了什么配置)、隔离(多租户或多项目的数据边界)、生命周期(升级、回滚、灰度)。Dev UI 在设计上不提供这些,把它推到生产位置等于把开发期的临时决策固化成长期约束,后续拆除成本远高于现在重新设计一个入口。
会答不算会
会做才算会
会判边界者
方能带团队
13、思考
这一境的核心冲突是「想快点看到效果」和「必须先把地基立稳」之间的矛盾。它没有第三条路:跳过基线的代价不是不出现,而是后移,并在后面每一境以更高的利息收回。真正值得带走的判断是——凡是会被后续章节共享的东西,都必须在第一次出现时就做成可复现、可验证、单一真源的形式,哪怕它当下看起来只是一件杂事。
依赖、鉴权、模型、调试这四件事之所以要一起做,是因为它们互为证据:依赖错了,鉴权报错不可信;鉴权模式混了,模型标识的对错无法判断;没有调试入口,前三个问题都只能靠猜。把它们当成一个整体来解决,是这一境唯一正确的姿势。
还有一个更隐蔽的判断:边界的强制手段要落到工具上,而不是落到约定上。scope 写进 POM,自检写进启动流程,版本写进文件,跳过语义写进测试——只有这些能被执行的东西,才真的能约束住后面的自己。
快是表象
稳是底盘
边界入工具
演化自从容

浙公网安备 33010602011771号