降SpringAI阿里第1掌-亢龙有悔-识势选型
框架选错,后面十七掌都在还债
text
[读者] 已有 Spring 技术栈的后端与架构师
[痛点] 模型能调通,Agent 能力越加越乱
[现在读] 要在动手前把分层与选型边界定死
[读完] 能画出五层架构,说清何时用 SAA
text
[旧方案] Spring Boot 里用 HTTP 客户端直连模型 SDK
|
v
[新需求] 工具调用 / MCP / RAG / 多 Agent / 长运行状态
|
v
[冲突] 胶水代码没有运行时 没有状态 没有追踪
|
v
[后果] 每加一个能力 都要重写一遍会话与编排
我是老李,一个在深圳写了十多年 Java 的后端,最近两年主要在做对话与 Agent 类系统。我不太信「框架越多越好」,但我非常信一件事:选型阶段省下的半小时,会在后面三个月的排查里连本带利还回去。
这一掌要动的,是一个数字人项目。它的业务范围是固定的:注册登录、数字人项目 CRUD、标题主题色背景图、开场白结束语、Agent 模型与 System Prompt、Web 运行页,再加上 LiveKit 实时交互。整套内容会拆成十八掌,而这一掌只解决两件事:为什么学,以及什么时候用。
生产基线我们固定 alibaba/spring-ai-alibaba v1.1.2.2,环境是 JDK 21 LTS,练习仓库是 https://github.com/lifuchun522/springaialibabapractice 。基线这么定不是拍脑袋:按官方 Releases 页面(https://github.com/alibaba/spring-ai-alibaba/releases )的说明,v1.1.2.2 被标记为 Latest,v1.1.2.1 的说明里建议升级到 1.1.2.2,而 v2.0.0-M1.1 属于 Pre-release。也就是说,v2.0.0-M1.1 只作为未来 Spring AI 2.0 / Spring Boot 4 的观察线,不进生产;这三条在动手前请以当前 Releases 与 CHANGELOG(https://github.com/alibaba/spring-ai-alibaba/blob/main/CHANGELOG.md )页面为准,版本信息是会变的事实。
可问题是:模型能调通,为什么一加工具调用,会话状态就开始打架?
那你该直接上 Spring AI Alibaba,还是先把 Spring AI 本身用熟,甚至就用普通 Java 服务加 HTTP 调用?
如果选错了,代价是什么------是多写一点胶水,还是整条 Agent 主线都要推倒重来?
01、故事
项目开工的时候,现场是这样的:一个 Spring Boot 单体服务,注册登录和数字人 CRUD 已经跑通,运行页能加载开场白和结束语,LiveKit 的房间也能拉起来。任务是让运行页背后的数字人能「真的会聊」,并且能在后台配置模型和 System Prompt。
第一版做法很朴素:注入一个 HTTP 客户端,直接请求兼容模式接口,把历史消息放在一个 ConcurrentHashMap 里,key 是会话 ID,value 是一个 ArrayList。前端每发一句,后端拼一次 messages 数组,丢给模型,拿回文本,再塞回列表。上线演示没问题,写起来两个小时。
变化来得很快。产品说,希望数字人能查订单;于是加一段 if-else,命中关键词就调本地接口,把结果拼进 prompt。接着要接知识库,于是又加一段检索逻辑。接着要支持多个 Agent 分工,一个负责接待、一个负责问答,于是会话列表里开始出现 role 字段和路由逻辑。再接着要能中断、能恢复、能人工接管、能在后台看每一步跑了什么------这时候,那个 ConcurrentHashMap 已经不是会话存储了,它变成了一坨没人敢动的状态机。
冲突不在于「模型不会用」,而在于这套自研胶水里,模型调用、工具编排、状态保存、流程控制、可观测性全挤在一个类里,每加一个能力都要动同一段代码,改一次就要全量回归一次。
胶水非架构
能调不等通
先辨层与界
再动手写码
02、问题
旧方案失效的方式很典型:它在「单轮问答」这个场景下是正确的,但一旦需求变成「有状态、多步骤、可中断、可观测」,它的技术表现就暴露了三层问题。
第一层,业务影响。数字人项目 CRUD 里配置的 System Prompt 和模型,改完要重启服务才生效;运营改一次开场白,研发要发一次版本。多 Agent 上线后,路由规则写在代码里,运营完全不可见,出问题只能靠翻日志。
第二层,技术表现。会话状态与业务数据混存;工具调用没有统一契约,每个工具一个手写解析;流式输出和阻塞输出是两套代码路径;模型换了供应商,改的是调用层,而不是配置。最要命的是没有追踪:用户说「数字人乱回答」,我们只能看到最终文本,看不到中间用了哪个工具、检索到什么、哪一步走的哪个分支。
第三层,可验证的完成标准。这一掌结束时,我不要求系统变复杂,只要求做到三件事:其一,能在一张图上画出 Spring AI → Extensions → Agent Framework → Graph → Admin 的分层,并说清每层的输入输出;其二,能用一段话解释「什么时候选 Spring AI Alibaba,什么时候只用 Spring AI,什么时候连框架都不用」;其三,仓库里有一个能跑通的最小工程骨架,分支 chapter/01-value-selection 已经提交。
注意这里我没有写任何性能指标。因为这一掌没有压测、没有线上流量,任何 QPS、延迟、准确率数字都是编的。这一掌交付的是边界判断,不是性能结论。
需求先落地
标准可验证
无标不验收
有界才不慌
03、原理
要理解这套框架,先把五个名词摆正位置。
Spring AI 提供的是基础抽象:ChatClient / Model、Tool Calling、MCP、RAG / Vector Store、Observability。它向上定义了「怎么和模型说话、怎么把工具暴露给模型」,但不绑定具体厂商。Spring AI Alibaba Extensions 做的是厂商与生态适配:DashScope、MCP Registry / Nacos、Memory、RAG、Vector Store、Observation。Agent Framework 提供的是 Agent 层能力:ReactAgent、Hooks / Interceptors、HITL、Context Engineering、Skills、Flow Agents。Graph Runtime 是长运行、有状态 Agent 的底层运行时:State / Node / Edge、条件与并行分支、Checkpoint / Persistence、Interrupt、Streaming。Admin / Studio 负责的是可视化开发、追踪与评测。
反直觉判断第一条:Spring AI Alibaba 不是「一个更全的 SDK」,它是把 Agent 装配线分了层。分层带来的不是功能,而是成本------在你只需要单轮问答时,这套分层是纯粹的开销;只有当你需要长运行状态、可中断、多 Agent 协作时,它才开始回本。所以选型的第一问不是「它强不强」,而是「我的需求会不会跨过那条线」。
反直觉判断第二条:Graph 不是「拖拉拽的工作流画图工具」,而是有状态执行的 Runtime。这一点决定了它的适用边界------如果你要的是定时任务编排,用普通的工作流引擎更省事;只有当流程本身依赖 Agent 的中间结果、需要挂起与恢复时,Graph 的存在才有意义。
顺着这个原理往下推,什么时候选 SAA 就有了判据:已有 Spring 技术栈、需要 Agentic 或 Workflow 或 Graph、需要多 Agent 与 A2A / Nacos、并且落在阿里云生态里的项目,收益最大。反过来,如果你只是把模型当成一个文本补全接口,用 Spring AI 的基础抽象甚至普通 Java 服务就够了;如果你团队已经在 LangChain4j 或 AgentScope 上有大量沉淀,硬切过来只是换了个壳。
抽象非冗余
分层是代价
越早省事少
越晚还债多
04、架构
text
[输入] 运行页用户消息 / 运营在 Admin 的配置
|
v
[模块] ChatClient 与 Model 适配 或 Agent Framework 编排
|
v
[数据/状态] 会话记忆 检索结果 图状态 检查点
|
v
[处理] 工具调用 MCP 检索 条件分支 中断恢复
|
v
[输出] 流式文本 运行追踪 评测样本
这张图要按层读。最下面一层是模型与抽象层,Spring AI 定义契约,Extensions 把 DashScope 之类的供应商接进来;中间一层是 Agent 层,把「一次模型调用」升级成「带钩子、带上下文工程、可人工介入的执行单元」;再上一层是图运行时,负责把多个执行单元连成有状态、可中断、可持久化的流程;最上面一层是 Admin / Studio,把开发、追踪、评测变成可视化动作。
边界很清楚:越往下越稳定、越通用;越往上越贴近业务、越容易变。代价也清楚:每引入一层,你就多一个需要理解的配置面、多一个可能出错的版本对齐点。适用条件是------只有当业务需求真的跨过「单轮问答」这条线,向上加层才是划算的。
这也是为什么这一掌要先画图再写代码。图没画明白,代码写得越多,后面越难拆。
五层各守土
越层即埋雷
输入定契约
状态定生死
05、实战一次
目标很小:搭一个 Spring Boot 服务,通过 ChatClient 调通 DashScope 的对话模型,并留下分支与提交记录。这是本掌的 V1。
环境与版本:JDK 21 LTS,Maven 3.9+,Spring Boot 3.x(与 Spring AI Alibaba v1.1.2.2 的对应关系需按当前官方文档核验),spring-ai-alibaba v1.1.2.2。
先建分支,这也是本掌的产出物之一:
bash
git checkout main
git pull
git checkout -b chapter/01-value-selection
依赖部分。下面的坐标与版本管理方式需按当前官方文档核验,不同小版本的 artifactId 与 BOM 坐标可能调整:
xml
<properties>
<java.version>21</java.version>
<spring-ai-alibaba.version>1.1.2.2</spring-ai-alibaba.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-bom</artifactId>
<version>${spring-ai-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
</dependency>
</dependencies>
配置如下,密钥从环境变量注入,不要写进仓库:
yaml
spring:
ai:
dashscope:
api-key: ${DASHSCOPE_API_KEY}
chat:
options:
model: qwen-plus
server:
port: 8080
核心实现只做一件事:把 ChatClient 作为模型调用的唯一出口。这样后面换模型、加拦截器、加观测,都只改一处。
java
package com.example.digitalhuman;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ChatController {
private final ChatClient chatClient;
public ChatController(ChatClient.Builder builder) {
this.chatClient = builder
.defaultSystem("你是一个数字人助手,回答简短、口语化。")
.build();
}
@GetMapping("/api/chat")
public String chat(@RequestParam("q") String question) {
return chatClient.prompt()
.user(question)
.call()
.content();
}
}
启动与验证:
bash
export DASHSCOPE_API_KEY=sk-xxxx
mvn -q spring-boot:run
bash
curl -s "http://localhost:8080/api/chat?q=用一句话介绍你自己"
未在当前环境实测,以下为预期结果:
bash
我是一个数字人助手,可以陪你聊天、回答你的问题。
验证通过之后提交,提交信息沿用这一掌的约定:
bash
git add .
git commit -m "docs(ch01): define Spring AI Alibaba value and architecture"
V1 到这里就是通的:能启动、能请求、能返回。但它只证明了「链路能通」,没有证明「后面十七掌都撑得住」------会话没持久化、模型配置写死在 yml、没有任何追踪。这些正是后面章节要逐层补上的东西。
补充一句源码阅读入口。v1.1.2.2 的 Agent 与图运行时在主仓库里,本掌不深读,但建议先记住这两个目录:https://github.com/alibaba/spring-ai-alibaba/tree/v1.1.2.2/spring-ai-alibaba-agent-framework 和 https://github.com/alibaba/spring-ai-alibaba/tree/v1.1.2.2/spring-ai-alibaba-graph-core 。
先跑最小链
再谈大格局
一次通到底
胜过十次猜
06、排查
第一条诊断链,现象是服务启动失败,日志里出现 NoSuchMethodError 或 NoClassDefFoundError,类名指向 Spring AI 的某个核心类。
怀疑是版本不匹配。检查方式是在项目根目录跑依赖树:
bash
mvn -q dependency:tree -Dincludes=org.springframework.ai
证据是同一个 Spring AI 核心构件出现了两个不同的版本号,一个来自手工声明的依赖,一个来自传递依赖。根因是没有用 BOM 统一版本,导致模型抽象层与适配层对不上。修复是把版本收敛到 dependencyManagement,让适配层跟随同一套 Spring AI 版本。
错误尝试: 先想到的是在报错的那个依赖上直接 exclude 掉传递进来的 Spring AI 核心包,让编译先过。为什么错?因为被排除掉的正是适配层编译时依赖的契约,编译能过只是运气,运行到某个方法签名变了的地方就会炸,而且下次升级 Starter 时又会复现同一个问题------这是把版本对齐问题伪装成依赖裁剪问题。
第二条诊断链,现象是接口返回 401,报错信息指向 api-key 无效或未授权。
怀疑是密钥没有正确注入。检查方式是确认容器环境变量是否真的存在,而不是只在本地 shell 里 export 过:
bash
printenv | grep -i dashscope
证据是环境变量为空,或者值里带上了引号与首尾空格。根因有两种:一是进程启动环境与执行 export 的 shell 不是同一个;二是密钥在配置中心或 CI 变量里被复制时带入了不可见字符。修复是把密钥放到统一的配置来源,并在应用启动日志里只打印「是否已加载」而不打印值。
第三条诊断链,现象是调用返回 200,但内容是空字符串。怀疑是模型名或接口地址不对。检查方式是打开框架的请求日志,确认实际发出的模型名与地址。证据显示配置里的模型名在 DashScope 兼容模式下并不存在,请求被网关接受但无有效回复。根治办法是把模型名收敛为配置项并做启动期校验,而不是等到运行页被点开才发现。
三条链有一个共同点:现象都在表面,根因都在对齐。依赖要对齐,密钥来源要对齐,模型名要与网关对齐。
现象非根因
证据才开口
排除非修复
对齐方止损
07、优化
基于上一章的证据,V2 只做三件收敛,不引入新能力。
第一件事:统一版本来源。根因是版本分散声明,修改是把 Spring AI 与 Spring AI Alibaba 全部交给 BOM 管理,项目里不再出现手写的框架版本号。原因是让适配层与抽象层永远来自同一次发布。新行为是依赖树里同名构件只有一个版本,验证方式仍是 dependency:tree,观察同组构件版本号是否唯一。
第二件事:配置外置。根因是模型名与 System Prompt 写在代码与本地 yml 里,运营改一次要发一次版。修改是把模型、System Prompt 的默认值收敛到配置项,并由数字人项目 CRUD 的数据覆盖默认值。原因是让「配置」成为数据而不是代码。新行为是同一份代码可以服务多个数字人项目,验证方式是启动两个不同配置的项目实例,观察回答风格是否随配置变化。
第三件事:模型调用出口唯一化。根因是散落的调用点让后续加拦截器、加观测、加降级变得困难。修改是全部调用统一走 ChatClient,禁止在控制器里直接注入底层模型对象。原因是把变化点收敛到一个位置。新行为是后续加日志、加追踪、换模型,都只改一处。
这三件收敛都不产生性能数据,也不该产生。它们的效果只能通过「改一处、影响面是否可控」来验证,而不是通过某个百分比。
根因定改法
配置外其身
出口唯一处
后方好换件
08、演进
text
[同一输入]
|
+--[V1] 直连模型 + 内存会话 / 代价 每加能力改同一段代码
|
+--[V2] ChatClient 唯一出口 + 配置外置 + 版本对齐 / 代价 前期多一层理解成本
|
[Trade-off]
得到 可替换与可扩展 失去 两个小时的极简 适用边界 需求已跨过单轮问答
把两版放在一起比:正确性上,V1 在单轮问答场景没问题,V2 在配置变更与依赖升级场景更稳;稳定性上,V1 的失败集中在运行时方法签名与密钥,V2 把这两类问题前移到了启动期与依赖分析期;复杂度上,V2 明显更高,多了一层抽象和一套版本对齐规则;成本上,V2 前期多花时间,后期每次加能力少花时间;适用范围上,V1 适合验证与 demo,V2 适合要长期维护的数字人项目。
遗留问题也要写清楚:V2 仍然没有持久化会话,没有流式输出,没有工具调用,没有追踪。这些不是这一掌能解决的,也不该在这一掌解决。
同入不同路
得一必失一
取舍写明白
演进不留债
09、洞见
9.1 选型的本质是画边界,不是比功能
反直觉判断:功能列表越长,选型越难做对。因为功能是可以后加的,边界一旦画错,后面每加一个功能都在加固错误的结构。这一掌把五层摆清楚,真正的产出不是「我知道有这些模块」,而是「我知道哪些需求该落在哪一层」。当你能把「运营改配置」归到 Admin 与配置层,把「人工接管」归到 Agent Framework 的 HITL,把「挂起恢复」归到 Graph,你就不太可能在控制器里硬写状态机。
9.2 版本对齐是选型的一部分,不是运维细节
反直觉判断:很多 NoSuchMethodError 不是 bug,是选型决策的迟到账单。把 v1.1.2.2 定为生产基线、把 v2.0.0-M1.1 定为观察线,本质上是在回答一个问题:你愿意为「新特性」承担多大的不确定性。Pre-release 用在生产,等于把版本风险转嫁给业务方。这条判断在后面的源码、测试与上线章节会被反复验证。
9.3 什么时候不加框架
这一条最容易被忽略:如果你只需要把模型当文本补全接口用,Spring AI 的基础抽象就够了;如果你连多轮对话都不需要,普通 Java 服务加一次 HTTP 调用就是最优解。框架的价值只在需求跨过阈值时兑现,跨不过去时,它就是纯粹的负担。判断阈值的方法很简单:问自己三个问题------需不需要状态、需不需要工具、需不需要多步可控。三个都是「不需要」,就别上 Agent 那几层。
9.4 自研胶水最贵的部分不是代码量
反直觉判断:自研胶水的成本不在写它的时候,而在别人接手它的时候。一个 ConcurrentHashMap 会话存储,作者记得所有隐含约定;接手的人要读完全部调用点才知道状态什么时候被改。框架带来的最大好处不是少写代码,而是把隐含约定变成了显式的结构,让别人有地方可查、有路径可追。这也是后面 Observability 与评测章节存在的理由。
选型即边界
边界即成本
成本即架构
架构即取舍
10、系统落地
原来有什么:一个能跑通的数字人项目,注册登录、CRUD、配置项、运行页、LiveKit 房间,加上一段直连模型的胶水代码。
本篇新增什么:一份固定的分层认知,一张五层架构图,一个以 ChatClient 为唯一出口的最小工程骨架,一条 chapter/01-value-selection 分支和一次 docs(ch01) 提交,以及一条明确的版本基线------生产用 v1.1.2.2,v2.0.0-M1.1 只观察。
现在能做什么:能启动服务并完成一次对话;能在不改代码的前提下更换模型与 System Prompt;能在依赖层面保证抽象层与适配层版本一致;能对团队解释「为什么这个项目选 Spring AI Alibaba,而不是 Spring AI 单用或换别的生态」。
还缺什么:会话没有持久化,消息不能流式返回,没有工具调用与 MCP 接入,没有知识库检索,没有 Agent 编排与图运行时,没有追踪与评测。按六阶段划分,这一掌只完成了第一阶段的入口部分。
下一步如何演进:先把「藏忆流式」补上,让状态与输出这两条主线立住;再把工具与 MCP 接进来;然后才谈 Agent 与 Graph。顺序不能反------没有记忆与流式,后面的 Agent 编排只是在流沙上盖楼。数字人项目 CRUD 里的标题、主题色、背景图、开场白、结束语,会在配置外置这一步逐步从代码迁移到数据。
有底才加层
有层才谈多
有轨才可视
可视才敢上
11、小结
text
Q1 → 模型能调通但一加工具就打状态架,因为旧方案把调用 编排 状态挤在一处
Q2 → 先分层再选型:跨过单轮问答阈值选 SAA,只用补全能力用 Spring AI 或普通 Java 服务
Q3 → 选错的代价不是多写胶水,是结构固化后每加一个能力都要全量重构
状态 → 五层架构可画出 v1.1.2.2 最小工程可跑通 chapter/01-value-selection 已提交
这一掌没有让系统变强,但让它变得可解释。可解释是后面十七掌所有动作的前提:读源码要知道自己在读哪一层,配评测要知道自己在测哪一层,做 K8s 上线要知道自己在迁哪一层。
一问定选型
二问定分层
三问定边界
一图收全局
12、作业
12.1 理解题:Spring AI、Extensions、Agent Framework、Graph、Admin 各自解决什么问题?
参考答案:Spring AI 提供模型、工具、MCP、RAG 等基础抽象;Extensions 负责厂商与生态适配,把 DashScope、MCP Registry / Nacos、Memory、RAG、Vector Store、Observation 接进来;Agent Framework 提供 ReactAgent、Hooks、HITL、Context Engineering、Skills、Flow Agents 等 Agent 层能力;Graph 是长运行、有状态 Agent 的底层运行时,负责状态、节点、条件、检查点、中断与流式;Admin / Studio 负责可视化开发、追踪与评测。四层是递进关系,不是替代关系。
12.2 实战题:在本地跑通本掌的 V1,并画出你的分层图。
参考答案:建分支 chapter/01-value-selection,引入 v1.1.2.2 的 DashScope Starter(坐标需按当前官方文档核验),用 ChatClient 作为唯一调用出口,通过环境变量注入密钥,起服务后请求一次对话接口。画图时从输入开始:运行页消息与后台配置作为输入,ChatClient 与 Agent 编排作为模块,会话与图状态作为数据,工具与检索与分支作为处理,流式文本与追踪作为输出。图能画通,说明边界想清楚了。
12.3 排障题:启动报 NoSuchMethodError,你的排查顺序是什么?
参考答案:第一步跑依赖树,确认同一构件是否出现多个版本;第二步确认框架版本是否统一由 BOM 管理;第三步确认 JDK 版本与框架要求一致;第四步才是看具体的类与方法签名。不要一上来就 exclude 依赖,那只是让编译先过去,运行期会以更隐蔽的方式失败。
12.4 架构判断题:团队已有 Spring 技术栈,但只是想给表单加一个「智能润色」,该不该上 Agent Framework 与 Graph?
参考答案:不该。这类需求是单轮或短轮次的文本变换,落在 Spring AI 的基础抽象甚至一次直接调用即可。上 Agent Framework 会引入编排、钩子与上下文管理的理解成本,上 Graph 会引入状态与检查点的运维成本,而收益为零。判断标准回到三个问题:需不需要状态、需不需要工具、需不需要多步可控。都是否,就停在最下面两层。
题目非作业
动手才算学
答完再回看
边界更清晰
13、思考
这一掌的核心冲突,其实不是「用哪个框架」,而是「在需求还没长成之前,你能不能先想清楚它会往哪长」。模型能调通的那一刻是最危险的,因为它给了你一种「已经做完了」的错觉。真正的分水岭在需求跨过阈值的那一天:会话要持久化、工具要注册、流程要中断、运营要可视化。跨过去之前,框架是负担;跨过去之后,框架是省下来的重构时间。
把这条判断留下:选型不是选功能最全的那个,而是选与你未来十二个月的需求形状最匹配的那个。形状判断错了,功能越多,绑得越紧。
一念辨边界
一测定版本
一图见全局
一掌立根基