写在前面:先说一件可能让你松口气的事------你不需要转 Python。
我是十年 Java 后端出身,前两篇写的都是 Python 生态(LangGraph、CrewAI、AutoGen 那一套)。评论区和私信里问得最多的一句话是:"我们团队全是 Java,Spring Boot 的老项目,AI Agent 是不是必须重开一个 Python 服务?"
不必须。而且在生产环境里,Java 反而有它的优势------这篇会讲清楚优势在哪、劣势在哪、什么时候该老实回 Python。
但有一件事必须先泼冷水:Spring AI 现在是 2.0.0,只支持 Spring Boot 4.0.x / 4.1.x。 你现在能搜到的中文教程、以及 AI 帮你生成的代码,几乎 100% 是 1.x 的写法,照抄过去大概率编译不过。这篇会把破坏性变更逐条列出来。
这篇的核心不是教你调 API,而是给你一张"翻译表":Agent 世界里的每一个新名词,都能对应到你已经用了十年的 Spring 概念。看完你会发现,你要学的新东西比想象中少得多。
关于验证方式,先声明清楚 :前两篇的代码我都在本地实跑过。这一篇不同------文中所有版本号、类名、方法签名、注解属性,都是我对照 2026 年 8 月的官方文档逐条核对 的,但受限于我当前环境的网络策略(Maven 中央仓库不可达),没有做本地编译验证 。文末我附了一个可直接
mvn compile的验证工程结构,你可以自己跑一遍再决定信不信。我宁可把这句话写在开头,也不想让你在生产项目里翻车。
目录
- 一、先给结论:Java 做 Agent,缺什么、不缺什么
- 二、版本地基(2026.08 核对)
- 三、核心翻译表:Agent 概念 ↔ 你已经会的 Spring
- 四、逐个拆解:为什么这么映射
- 五、多角色协作:Java 侧目前的真实水位
- 六、Spring AI 2.0 破坏性变更清单
- 七、什么时候老实回 Python
- 八、Java 人最容易踩的 11 个坑
- 附:可自行验证的工程结构
- 结语
一、先给结论:Java 做 Agent,缺什么、不缺什么
先把话说透,省得你看完一万字才发现方向错了。
1.1 一个很多人没意识到的事实
做一个能上线的 Agent,模型调用只占工作量的 20%。剩下 80% 是什么?
- 连你的业务库、你的 RPC、你的权限体系
- 事务边界、连接池、超时、重试、熔断
- 多租户隔离、审计日志、灰度发布
- 监控告警、链路追踪、容量规划
这 80% 恰好是 Java 生态过去二十年最擅长的事。 Python 那边现在还在为"怎么把 Agent 塞进一个能扛住流量的服务"发明轮子,而你的 Spring Boot 项目里这些东西早就是标配了。
1.2 那 Java 缺什么
诚实地说,缺两样,而且都很关键:
| 缺的能力 | Python 侧的对应物 | Java 侧现状 |
|---|---|---|
| 检查点 / 断点续跑 / 时间旅行 | LangGraph 的 Checkpointer |
没有对等物。ChatMemory 只是"存消息",不是"存执行状态" |
| 成熟的多智能体编排 | LangGraph、AutoGen、CrewAI | Spring AI 没有一等公民 ;LangChain4j 有 langchain4j-agentic,但官方标注 experimental |
如果你的场景强依赖这两样,别硬扛,老实用 Python 写编排层,Java 那边做业务网关------这是很务实的架构,第七节会展开。
1.3 一句话判断
模型调用围绕你的业务系统转 → 用 Java。业务系统围绕模型编排转 → 用 Python。
大部分企业内部场景(智能客服、工单分派、报表问答、内部知识库、审批助手)都属于前者。
二、版本地基(2026.08 核对)
版本锁定是第一生产力,这是上一篇就说过的。先把地基钉死:
| 组件 | 版本 | 关键约束 |
|---|---|---|
| Spring AI | 2.0.0 |
只支持 Spring Boot 4.0.x / 4.1.x |
| Spring AI(旧线) | 1.1.8 / 1.0.9 |
还在维护,Boot 3.x 项目走这条线 |
| LangChain4j | 1.19.0 |
最低 JDK 17 |
| LangChain4j 反应式模块 | langchain4j-reactor 1.19.0-beta29 |
想要 Flux<String> 才需要 |
Spring AI 2.0 的 Maven 配置:
xml
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>2.0.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
Starter 命名规范是 spring-ai-starter-model-{provider},比如 spring-ai-starter-model-openai。接国产模型(DeepSeek / 通义 / Kimi / 智谱)走 OpenAI 兼容协议即可,Spring AI 也有独立的 deepseek 模块。
LangChain4j 的 Maven 配置:
xml
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j</artifactId> <!-- 高层 AiServices API -->
<version>1.19.0</version>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai</artifactId> <!-- 具体模型集成 -->
<version>1.19.0</version>
</dependency>
第一个决策点 :你的项目还在 Spring Boot 3.x 上?那 Spring AI 2.0 你升不上去 。要么走 Spring AI 1.1.x 老线,要么用 LangChain4j(它对 Boot 版本没这么强的绑定)。这个约束往往直接决定选型,建议先查你的
spring-boot-starter-parent版本再往下看。
三、核心翻译表:Agent 概念 ↔ 你已经会的 Spring
这一节是全文的价值所在,建议单独截图存下来。
| Agent 世界的名词 | 你已经用了十年的东西 | 说明 |
|---|---|---|
ChatClient 流式 API |
RestClient / WebClient |
官方文档明说是照着这两个设计的,.prompt().user().call().content() 就是熟悉的味道 |
Advisor / AdvisorChain |
HandlerInterceptor 链 / Servlet Filter 链 |
一模一样的责任链模型 |
advisorChain.nextCall(req) |
filterChain.doFilter(req, res) |
不调它,链就断在这儿 |
Advisor 实现 Ordered |
@Order / Ordered |
值小的先执行,栈式结构:先处理请求的最后处理响应 |
BaseAdvisor.before/after |
@Before / @AfterReturning |
AOP 的前置/后置通知 |
@Tool 注解的方法 |
@RequestMapping 之于 HTTP |
都是"把一个方法暴露出去",区别只是调用方从浏览器换成了模型 |
@ToolParam(required=false) |
@RequestParam(required=false) |
连语义都一样 |
ToolContext |
RequestContextHolder / ThreadLocal |
传 tenantId 这种隐式参数,且永远不会发给模型 |
ToolExecutionExceptionProcessor |
@ControllerAdvice + @ExceptionHandler |
统一异常处理,决定"抛出去"还是"转成消息" |
returnDirect = true |
提前 return,不走后续链路 |
工具结果直接返回调用方,不再回灌给模型 |
ChatMemory + conversationId |
HttpSession + sessionId |
会话状态,靠 ID 隔离不同用户 |
JdbcChatMemoryRepository |
你的 DAO 层 | 就是把会话落库,表结构官方给了 |
.entity(Person.class) |
Jackson 反序列化响应体 | 把模型输出直接映射成 POJO / record |
VectorStore |
一个"按语义查"的 Repository | 心智模型就是 findBySimilarity(...) |
ToolCallback |
HandlerMethod(可反射调用的方法句柄) |
Spring MVC 内部也是这么描述一个 handler 的 |
| MCP Server / Client | 面向模型的 RPC(≈ Dubbo / Feign) | 只不过服务发现和调用决策交给了 LLM |
| Spring AI Observability | Micrometer / Actuator | 直接出 metrics 和 trace,不用另接一套 |
LangChain4j AiServices |
Spring Data JPA Repository / Feign Client / MyBatis Mapper |
官方原话:"very similar to Spring Data JPA or Retrofit" |
LangChain4j @MemoryId |
@SessionAttribute / 分片键 |
按 ID 路由到不同的会话上下文 |
看完这张表你应该有个感觉:没有一个概念是全新的。 全都是你写业务代码时天天在用的模式,只是换了个名字,服务对象从"人/系统"变成了"模型"。
四、逐个拆解:为什么这么映射
4.1 ChatClient ≈ RestClient
这个不用多讲,看一眼就懂:
java
@RestController
class MyController {
private final ChatClient chatClient;
MyController(ChatClient.Builder builder) { // 自动配置好的 prototype Bean
this.chatClient = builder.build();
}
@GetMapping("/ai")
String ask(String q) {
return chatClient.prompt()
.user(q)
.call()
.content();
}
}
构造器注入、Builder、流式链式调用------这段代码你不用学任何 AI 知识就能看懂,这就是 Spring AI 的设计目标。
call() 之后能拿到什么,也和你熟悉的 RestClient 一一对应:
| 方法 | 类比 |
|---|---|
.content() |
String 响应体 |
.chatResponse() |
完整的 ResponseEntity(带元数据) |
.entity(Xxx.class) |
getForObject(url, Xxx.class) |
.entity(new ParameterizedTypeReference<List<X>>(){}) |
泛型集合反序列化,连 API 都是同一个类 |
.stream().content() |
返回 Flux<String>,就是 WebFlux |
2.0 还给 entity() 加了两个很实用的选项:
java
ActorFilms r = chatClient.prompt().user("...")
.call()
.entity(ActorFilms.class, spec -> spec
.useProviderStructuredOutput() // 用模型原生的结构化输出能力
.validateSchema()); // 校验不过自动重试
validateSchema() 这个在生产上很值------它把"模型偶尔返回一段不合法 JSON"这个经典问题在框架层解决了,不用你自己写 try-catch-retry。
4.2 Advisor ≈ 拦截器链(本文最重要的一节)
如果只让我留一个映射,就是这个。
Spring AI 的 Advisor 就是拦截器链,源码级别的相似:
java
public interface CallAdvisor extends Advisor {
ChatClientResponse adviseCall(ChatClientRequest request,
CallAdvisorChain chain);
}
public interface Advisor extends Ordered {
String getName();
}
看到 extends Ordered 了吗?和你写 Filter 时用的 @Order 是同一个东西。值越小越先执行,链条是栈式的:第一个处理请求的,最后一个处理响应。
一个日志 Advisor,和你写过的日志 Filter 长得几乎一样:
java
public class SimpleLoggerAdvisor implements CallAdvisor, StreamAdvisor {
@Override public String getName() { return getClass().getSimpleName(); }
@Override public int getOrder() { return 0; }
@Override
public ChatClientResponse adviseCall(ChatClientRequest request,
CallAdvisorChain chain) {
logRequest(request); // ≈ 前置处理
ChatClientResponse response = chain.nextCall(request); // ≈ filterChain.doFilter()
logResponse(response); // ≈ 后置处理
return response;
}
}
chain.nextCall(request) 就是 filterChain.doFilter(req, res)。不调它,链就断在这里------这也意味着你可以直接短路返回,做熔断、做缓存、做敏感词拦截,全是你熟悉的套路。
如果你只想写前置/后置逻辑,用 BaseAdvisor 更省事,它把 before / after 拆开了,对应 AOP 的 @Before / @AfterReturning:
java
public class MyAdvisor implements BaseAdvisor {
@Override public ChatClientRequest before(ChatClientRequest req, AdvisorChain chain) { ... }
@Override public ChatClientResponse after(ChatClientResponse res, AdvisorChain chain) { ... }
@Override public int getOrder() { return 0; }
}
框架自带的 Advisor,本质就是"官方写好的拦截器":
| Advisor | 干什么 | 类比 |
|---|---|---|
MessageChatMemoryAdvisor |
把历史消息塞进请求 | 从 Session 里读上下文的拦截器 |
VectorStoreChatMemoryAdvisor |
从向量库取记忆 | 从 Redis 取会话的拦截器 |
QuestionAnswerAdvisor |
朴素 RAG | 请求前查一次库再拼参数 |
RetrievalAugmentationAdvisor |
完整模块化 RAG | 上面那个的加强版 |
ToolCallingAdvisor |
处理工具调用循环 | 2.0 起自动注册,见踩坑 ② |
SafeGuardAdvisor |
内容安全兜底 | 敏感词过滤器 |
SimpleLoggerAdvisor |
打日志 | 日志 Filter |
注册方式也是熟悉的 Builder:
java
ChatClient.builder(chatModel)
.defaultAdvisors(
MessageChatMemoryAdvisor.builder(chatMemory).build(),
QuestionAnswerAdvisor.builder(vectorStore).build())
.build();
理解到这里,Spring AI 的"高级功能"对你就不神秘了------RAG、记忆、安全、日志,全部是往拦截器链上挂东西。你想加个"每次调用扣配额"的逻辑?写个 Advisor 就行,和你当年写限流 Filter 一模一样。
4.3 @Tool ≈ 把 Service 方法暴露给模型
java
class DateTimeTools {
@Tool(description = "获取用户当前时区的日期和时间")
String getCurrentDateTime() {
return LocalDateTime.now()
.atZone(LocaleContextHolder.getTimeZone().toZoneId())
.toString();
}
@Tool(description = "设置闹钟", returnDirect = true)
void setAlarm(@ToolParam(description = "ISO-8601 格式的时间") String time) {
// ...
}
}
心智模型:@RequestMapping 是把方法暴露给 HTTP 调用方,@Tool 是把方法暴露给模型。
一个重要区别,也是 Java 人最容易忽略的:description 不是注释,它是"接口文档",而且是模型唯一能看到的信息。 你写 @RequestMapping 时可以靠路径名让前端猜出用途,但模型只能读 description。写得含糊,模型就选错工具。
注册方式:
java
// 单次请求
chatClient.prompt("明天几号?").tools(new DateTimeTools()).call().content();
// 全局默认
ChatClient.builder(chatModel).defaultTools(new DateTimeTools()).build();
注意 :单次请求的
tools()会完全覆盖defaultTools(),不是追加。这个语义和你以为的可能不一样。
4.4 ToolContext ≈ ThreadLocal(多租户的关键)
这个设计我很喜欢,做过 SaaS 的人一看就懂:
java
class CustomerTools {
@Tool(description = "查询客户信息")
Customer getCustomerInfo(Long id, ToolContext toolContext) {
String tenantId = (String) toolContext.getContext().get("tenantId");
return customerRepository.findById(id, tenantId);
}
}
chatClient.prompt("查一下 42 号客户")
.tools(new CustomerTools())
.toolContext(Map.of("tenantId", "acme")) // 挂上下文
.call().content();
关键点:ToolContext 里的数据永远不会发给模型。
这解决了一个真实的安全问题------租户 ID、用户 ID、内部 token 这类东西,你既需要它在工具里可用,又绝对不能让模型看见 (模型看见了就可能在回答里吐出来,或者被提示词注入骗着换一个租户去查)。这就是 RequestContextHolder 的角色,只不过边界更硬。
4.5 工具异常 ≈ @ControllerAdvice
工具报错了怎么办?Spring AI 的答案很 Spring:
java
@FunctionalInterface
public interface ToolExecutionExceptionProcessor {
String process(ToolExecutionException exception);
}
默认行为(DefaultToolExecutionExceptionProcessor):
RuntimeException的 message → 转成文本发回给模型,让它自己调整参数重试- 受检异常(
IOException等)→ 直接抛出 Error(如OutOfMemoryError)→ 直接抛出
一行配置切换全局策略:
properties
spring.ai.tools.throw-exception-on-error=false # false=转成消息给模型;true=抛给调用方
这个设计比 Python 那边优雅。LangChain 系要么让你自己写中间件,要么工具报错后模型开始疯狂重试同一个参数。这里直接给了框架级的策略开关。
4.6 ChatMemory ≈ HttpSession
java
chatClient.prompt()
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, "user-123-session-456"))
.user(userText)
.call().content();
conversationId 就是 sessionId。存储实现有内存版、JDBC 版、向量库版,对应你的 Session 存内存 / 存 MySQL / 存 Redis。
Spring AI 2.0 在这里做了一个我认为很正确的破坏性变更 :删掉了 ChatMemory.DEFAULT_CONVERSATION_ID,也删掉了在 Builder 上设 conversationId 的能力,强制你每次调用显式传。
为什么正确?因为 1.x 那个默认值就是个定时炸弹------你本地单人测试一切正常,一上线多用户共用同一个 default 会话,所有人的对话串在一起。这个坑我见过不止一次。2.0 用编译期/运行期强制的方式让你没法犯这个错。
4.7 LangChain4j 的 AiServices ≈ Spring Data JPA
现在换 LangChain4j。它的高层 API 叫 AiServices,官方文档原话是:"This approach is very similar to Spring Data JPA or Retrofit"------你定义接口,框架用动态代理给你实现。
java
interface Assistant {
@SystemMessage("你是一个礼貌的客服助手。")
String chat(@MemoryId String userId, @UserMessage String message);
}
Assistant assistant = AiServices.builder(Assistant.class)
.chatModel(model)
.chatMemoryProvider(id -> MessageWindowChatMemory.withMaxMessages(10))
.tools(new OrderTools())
.contentRetriever(contentRetriever) // RAG
.build();
如果你写过 MyBatis 的 Mapper、Feign 的 Client、或者 Spring Data 的 Repository,这个模式对你是零学习成本的。 在 Spring Boot 里有 starter,直接 @Autowired 注入就行,连 AiServices.create() 都不用写。
返回类型的设计也很 Java:
java
interface SentimentAnalyzer {
@UserMessage("下面这段文本情绪是正面的吗?{{it}}")
boolean isPositive(String text); // 返回 boolean!
}
interface PriorityAnalyzer {
@UserMessage("分析这个工单的优先级:{{it}}")
Priority analyzePriority(String issue); // 返回枚举!
}
返回 boolean 和 enum 这一手非常关键 ------它意味着你可以把 LLM 直接嵌进普通的 if / switch 里,作为一个"有点智能的判断函数",而不是非要把整个应用改造成 Agent。这是很多团队落地的最佳切入点:先用 LLM 替换掉一两个规则难写的判断,而不是一上来重构架构。
想拿元数据就包一层 Result<T>:
java
Result<List<String>> result = assistant.generateOutlineFor("Java");
List<String> outline = result.content();
TokenUsage usage = result.tokenUsage(); // 算成本用
List<Content> sources = result.sources(); // RAG 命中了哪些片段
List<ToolExecution> tx = result.toolExecutions(); // 调了哪些工具
tokenUsage() 和 toolExecutions() 直接暴露出来,做成本核算和审计非常方便,这点比 Spring AI 顺手。
五、多角色协作:Java 侧目前的真实水位
上一篇整篇在讲多智能体,这里必须给 Java 人一个诚实的现状。
5.1 Spring AI:没有一等公民
Spring AI 官方文档有一页叫《Building Effective Agents》,覆盖了 Anthropic 提出的五种模式:
| 模式 | 官方给的类 |
|---|---|
| Chain Workflow(链式) | ChainWorkflow |
| Parallelization(并行) | ParallelizationWorkflow |
| Routing(路由) | RoutingWorkflow |
| Orchestrator-Workers(编排者-工人) | OrchestratorWorkersWorkflow |
| Evaluator-Optimizer(评估-优化循环) | EvaluatorOptimizerWorkflow |
但请注意:这些不是框架里的 API,而是示例仓库(spring-ai-examples/agentic-patterns)里的手写类。 你看一眼 ChainWorkflow 的实现就明白了:
java
public String chain(String userInput) {
String response = userInput;
for (String prompt : systemPrompts) {
response = chatClient.prompt(String.format("{%s}\n{%s}", prompt, response))
.call().content();
}
return response;
}
一个 for 循环。这不是贬低------恰恰相反,它诚实地告诉你:所谓"链式多 Agent",本质就是一个 for 循环。 你完全有能力自己写,而且写出来的可控性比黑盒框架更好。
官方那页的"Future Work"里明确写着模式组合、MCP 集成、高级记忆管理还在路上。所以现状是:Spring AI 给你打好了单 Agent 的地基,多角色编排要你自己搭。
5.2 LangChain4j:有 langchain4j-agentic,但标注实验性
LangChain4j 走得更远一些,有一个专门的 langchain4j-agentic 模块:
java
CreativeWriter writer = AgenticServices.agentBuilder(CreativeWriter.class)
.chatModel(BASE_MODEL)
.outputKey("story")
.build();
UntypedAgent novelCreator = AgenticServices.sequenceBuilder()
.subAgents(writer, audienceEditor, styleEditor)
.outputKey("story")
.build();
String story = (String) novelCreator.invoke(Map.of(
"topic", "dragons and wizards",
"style", "fantasy",
"audience", "young adults"));
提供的构建器有 sequenceBuilder(顺序)、parallelBuilder(并行)、loopBuilder(循环)、conditionalBuilder(条件)、parallelMapperBuilder,另外还有 SupervisorAgent(主管制)和一套注解(@Agent、@ChatModelSupplier、@PlannerSupplier、@SupervisorRequest)。
角色之间靠 AgenticScope 共享变量 传值------用 outputKey 写入,用 @V("story") 读出。这个心智模型和上一篇里 Google ADK 的 output_key + {key} 插值几乎一样。
但官方文档开篇就写着:"the whole module has to be considered experimental and is subject to change in future releases." 翻译成人话:随时可能改 API。
5.3 结论
| 你的需求 | 建议 |
|---|---|
| 单 Agent + 工具 + RAG + 记忆 | Java 完全够用,Spring AI 2.0 首选 |
| 固定流程的多步编排 | Java 够用,自己写 for 循环 / 状态机,比黑盒框架可控 |
| 动态多角色、断点续跑、人工审批、时间旅行 | 老实用 Python(LangGraph),Java 侧还没有对等物 |
六、Spring AI 2.0 破坏性变更清单
这一节可能是全文对你最实用的部分。 网上的教程、以及 AI 生成的代码,绝大多数还是 1.x 写法。以下是升级到 2.0 时会让你编译不过或行为变化的地方,逐条对照:
6.1 工具注册方式变了(影响最大)
@Bean + @Description 注册 Function 的方式被移除 了(SpringBeanToolCallbackResolver 和 toolNames() 都没了):
java
// ❌ 1.x 写法,2.0 不再工作
@Bean
@Description("Get the weather in location")
Function<WeatherRequest, WeatherResponse> currentWeather() {
return weatherService::getWeather;
}
// ✅ 2.0 写法
@Bean
ToolCallback currentWeather() {
return FunctionToolCallback.builder("currentWeather", weatherService::getWeather)
.description("Get the weather in location")
.inputType(WeatherRequest.class)
.build();
}
6.2 ToolCallingAdvisor 现在自动注册
java
// ❌ 2.0 里再手动注册会产生重复
chatClient.prompt("...").tools(weatherTool)
.advisors(ToolCallingAdvisor.builder().build())
.call().content();
// ✅ 直接用就行
chatClient.prompt("...").tools(weatherTool).call().content();
6.3 conversationId 必须显式传
见 4.6。ChatMemory.DEFAULT_CONVERSATION_ID 常量和 .conversationId() Builder 方法都被删了。另外 PromptChatMemoryAdvisor 被移除 ,统一用 MessageChatMemoryAdvisor。
6.4 options() 现在要传 Builder,不是构建好的对象
java
// ❌ 1.x
ChatOptions opts = AnthropicChatOptions.builder().maxTokens(100).build();
chatClient.prompt("...").options(opts).call().content();
// ✅ 2.0:不要 .build()
chatClient.prompt("...")
.options(AnthropicChatOptions.builder().maxTokens(100).temperature(0.7))
.call().content();
6.5 配置属性拍平了,去掉 .options 前缀
properties
# ❌ 1.x
spring.ai.openai.embedding.options.model=text-embedding-3-small
# ✅ 2.0
spring.ai.openai.embedding.model=text-embedding-3-small
对应的 Java 代码 properties.getOptions().getModel() → properties.getModel()。
6.6 Options 变成不可变
copy() 和 fromOptions() 被移除,改用 mutate();getter 返回的集合改动会抛 UnsupportedOperationException:
java
// ❌ OllamaChatOptions o = original.copy(); o.setFoo("...");
// ✅
OllamaChatOptions o = original.mutate().foo("...").build();
6.7 默认 temperature 0.7 取消了
2.0 不再统一给 0.7,用各家 provider 的原生默认值。如果你的提示词是按 0.7 调优的,升级后输出会变------需要显式配置:
properties
spring.ai.openai.chat.temperature=0.7
6.8 Anthropic 模块换成官方 SDK
底层从自研 RestClient 换成了 com.anthropic:anthropic-java,AnthropicApi 等类被移除,构造方式改为 Builder。并且默认 maxTokens 从 500 变成了 4096------如果你之前依赖那个 500 的默认值来控成本,升级后账单会涨。
6.9 JDBC ChatMemory 表结构变了
新增 sequence_id BIGINT 列用于确定性排序,需要手动执行迁移 SQL(官方给了 PostgreSQL 版本):
sql
ALTER TABLE SPRING_AI_CHAT_MEMORY ADD COLUMN sequence_id BIGINT;
-- 回填 + NOT NULL + 建索引,详见官方 Upgrade Notes
6.10 MCP 注解全部换包
java
// ❌ org.springaicommunity.mcp.annotation.McpTool
// ✅ org.springframework.ai.mcp.annotation.McpTool
MCP 传输模块的 groupId 也从 io.modelcontextprotocol.sdk 改成了 org.springframework.ai。
6.11 好消息:官方提供 OpenRewrite 自动迁移
不用全手改:
bash
mvn org.openrewrite.maven:rewrite-maven-plugin:6.32.0:run \
-Drewrite.configLocation=https://raw.githubusercontent.com/spring-projects/spring-ai/refs/heads/main/src/rewrite/migrate-to-2-0-0-M3.yaml \
-Drewrite.activeRecipes=org.springframework.ai.migration.M3MigrateMcpAnnotations \
-Dmaven.compiler.failOnError=false
七、什么时候老实回 Python
这一节我尽量不带立场。
7.1 该用 Java 的信号
- Agent 需要大量调用你现有的业务系统(数据库、RPC、内部 API)
- 需要事务、权限、多租户、审计这些企业级能力
- 团队是 Java 团队,运维体系、监控、发布流程都是围绕 JVM 建的
- 场景是"给现有系统加一层智能入口",而不是"从零做一个 AI 产品"
7.2 该用 Python 的信号
- 需要检查点、断点续跑、时间旅行 (LangGraph 的
Checkpointer,Java 侧没有对等物) - 需要成熟的动态多角色编排(见第五节)
- 需要人工审批(HITL)中断再恢复------这依赖检查点能力
- 需要跟上最新的研究型能力(各种 retriever、评测框架、新范式),Python 生态永远快半年到一年
7.3 我实际会推荐的架构
大多数企业其实不用二选一:
[ Spring Boot 业务系统 ] ← 权限 / 事务 / 业务数据 / 现有 RPC
│
│ MCP(面向模型的 RPC)
↓
[ Python 编排层(LangGraph 等) ] ← 只做多角色编排、状态机、检查点
用 MCP 做两边的边界:Java 侧把业务能力包装成 MCP Server(Spring AI 2.0 的 MCP 支持相当完整),Python 侧做编排逻辑当 MCP Client。
这样各自发挥优势:Java 管"能做什么、谁能做、做了要留痕",Python 管"什么时候做、按什么顺序做"。而且哪天 Java 侧的编排能力成熟了,你把 Python 那层换掉就行,业务能力层一行不用动。
八、Java 人最容易踩的 11 个坑
- Spring AI 2.0 只支持 Spring Boot 4.0.x / 4.1.x。Boot 3.x 项目升不上去,走 1.1.x 老线或改用 LangChain4j
@Bean + @Description注册工具的方式在 2.0 被移除 ,改用FunctionToolCallback.builder(...)ToolCallingAdvisor2.0 起自动注册,再手动注册会重复conversationId必须每次显式传 ,DEFAULT_CONVERSATION_ID已删除(这其实救了你,见 4.6).options()要传 Builder 而不是.build()后的对象 ,写惯 1.x 的手会自动多打一个.build()- 默认 temperature 0.7 取消了,升级后模型输出风格会变,别以为是提示词的问题
- Anthropic 默认
maxTokens从 500 变成 4096,成本会涨 - 单次
tools()会完全覆盖defaultTools(),不是追加 @Tool的description是给模型看的接口文档,不是注释。写含糊模型就选错工具- LangChain4j:同一个
@MemoryId不能并发调用 。官方明确说了框架不做并发保护,会污染ChatMemory------Java 人写惯了线程安全的 Service,这里特别容易翻车。建议按 memoryId 加分布式锁或串行化队列 - LangChain4j:没开
-parameters编译选项时@V注解必填 。Spring Boot 项目默认开了,纯 Maven 项目要自己在maven-compiler-plugin里加
附:可自行验证的工程结构
前面说过这篇没做本地编译验证,所以给你一个能自己验证的最小工程。建议先跑通再决定要不要在项目里推:
java-agent-demo/
├── pom.xml # spring-ai-bom 2.0.0 + Spring Boot 4.x
└── src/main/java/com/example/
├── DemoApplication.java
├── config/ChatClientConfig.java # defaultAdvisors / defaultTools
├── advisor/BudgetGuardAdvisor.java # 自己写的"配额熔断"拦截器
├── tools/OrderTools.java # @Tool + ToolContext 多租户
└── controller/AskController.java
bash
mvn -q compile # 只要能编译过,说明本文所有 API 签名都对得上
mvn spring-boot:run
BudgetGuardAdvisor 是我推荐每个人第一个动手写的 Advisor------它只有二十几行,但写完你就彻底理解拦截器链了,而且是生产上真正需要的东西(防止某个会话把 token 烧穿):
java
public class BudgetGuardAdvisor implements CallAdvisor {
private final int maxTokens;
private final AtomicInteger used = new AtomicInteger();
@Override public String getName() { return "budgetGuard"; }
@Override public int getOrder() { return Ordered.HIGHEST_PRECEDENCE; }
@Override
public ChatClientResponse adviseCall(ChatClientRequest req, CallAdvisorChain chain) {
if (used.get() > maxTokens) {
throw new IllegalStateException("预算熔断:已消耗 " + used.get() + " tokens");
}
ChatClientResponse res = chain.nextCall(req);
// 从 res 的 usage 元数据里累加,具体字段以你使用的 provider 为准
return res;
}
}
结语
如果只让我留一句话给正在观望的 Java 工程师:
你不缺学 Agent 的能力,你缺的只是一张翻译表。
Advisor 就是拦截器,@Tool 就是把 Service 方法暴露出去,ChatMemory 就是 Session,AiServices 就是 Mapper。这些模式你已经用了十年,只是这次调用方从人变成了模型。
真正需要重新学的只有三件事,而且都不是 Java 知识:
description是写给模型看的接口文档,含糊了模型就选错------这是产品思维,不是编程思维- 模型的输出是不确定的 ,所以要有
validateSchema()、要有重试、要有护栏------这是你写幂等接口时的思维,只是强度要拉满 - token 是会烧钱的,每一次多余的上下文都在花钱------这是你做性能优化时的思维,换了个计量单位
最后提醒一次:先去查你项目的 Spring Boot 版本。 如果还在 3.x,Spring AI 2.0 这条路今天走不通,别浪费一周才发现。
下一篇我打算写**《用 Spring AI 把老项目包装成 MCP Server》**------怎么让你那套写了五年的业务接口,一行业务代码不改,直接变成模型能调用的能力。这是我认为 Java 团队接入 AI 最实际、ROI 最高的一条路。
觉得有用的话点个赞 + 收藏。评论区聊聊你们团队现在卡在哪一步------是 Boot 版本升不上去,还是没想清楚该从哪个场景切入。