最近一次代码评审,我终于把维护了一整年的Python大模型中转服务下线了。上一轮项目刚开始接大模型的时候,公司技术栈全是Java,团队里没有任何人有Python实战经验,最后只能临时找外包搭了一个Flask服务,专门负责把群里的模型API请求包装成Java能调的REST接口。这个服务在线上苟延残喘了将近一年,期间经历了conda环境崩溃、pip依赖冲突、SSE流式连接被网关掐断、模型升级后参数不兼容等一堆问题。每次一出事,Java这边看不懂Python日志,外包又离职了,简直成了整个项目组的梦魇。直到我把Spring AI 2.0真正用进项目,才彻底从这个泥潭里爬出来。这篇文章就围绕Spring AI 2.0的实战接入过程,把从依赖引入到企业级能力落地的完整路径拆给你看,重点聊聊Java程序员如何不碰一行Python就完成大模型集成。
1. 先回答一个问题:为什么Spring AI 2.0值得Java程序员专门学一次
1.1 过去的日子:接一个大模型需要“三层套娃”
如果你的项目在Spring AI出现之前就接入过大模型,大概率经历过下面三种方案之一。
第一种是纯Java手写HTTP调用。用RestTemplate或者OkHttp去请求模型API,自己拼Prompt、自己解析JSON、自己处理鉴权。这种做法在模型只有一个、接口只用一两个的时候还能抗住,一旦涉及流式输出、多轮记忆、结构化返回,代码复杂度会飞速膨胀。我记得当时光一个SSE流式解析器就写了两百多行,还得自己处理连接中断重连,维护成本相当高。
第二种是当前最常见的“套娃Z”架构:Java业务服务调Python中转服务,Python再去调模型API。这种做法的好处是能用上Python生态的各种成熟库,坏处是团队必须同时养两套技术栈。部署上的问题尤其致命:Java服务能用现成的镜像和K8s编排一键发布,Python服务却往往要从装conda、配pip源开始,每次发版都像拆盲盒。更麻烦的是两边数据结构经常对不上,Java这边定义了一个OrderVO,Python那边返回的是dict,联调阶段出现JSON key大小写不一致这种低级问题的频率高得让人崩溃。
第三种是硬着头皮直接用LangChain4j这类Java侧的开源库。这个方向本身没问题,但早期版本的东西要么API不够稳定,要么和Spring Boot的自动装配体系结合得不好,总有一种拾人牙慧的感觉。很多Java团队的方案是“等一等观望一下”。
这些问题的根源其实不在这三种方案本身,而在于整个Java生态缺少一个真正由Spring官方背书、能与Spring Boot深度集成的AI开发框架。模型API是标准的,但工程化的接入路径一直是散装的。
1.2 从“能连”到“可复用”:Spring AI 2.0的核心设计
Spring AI 2.0做的事情,如果只记住一句话,就是:把大模型集成变成了Spring Boot里一个普通的自动配置模块。
我习惯用一个类比来理解它:Spring AI在AI开发中的角色,类似于JDBC在数据库开发中的角色。在没有JDBC的年代,Java连MySQL要写一堆厂商相关的代码;有了JDBC,换数据库只需要换Driver和连接串。Spring AI也是同样的思路,它把“对话模型”“嵌入模型”“向量存储”“结构化输出”“函数调用”这些AI开发中的常用能力全部抽象成统一接口,再通过starter机制完成自动配置。
具体到2.0这个版本,和更早期的AI框架雏形相比,最大变化是抽象层已经非常清晰。你引入一个starter之后,Spring容器里会自动装配好ChatModel、EmbeddingModel、VectorStore等核心Bean,业务代码里直接注入使用即可。不再需要自己写累赘的工厂类或者动态代理去适配不同厂商的模型API。
另一个核心设计是它对业界标准的拥抱。2.0的模型调用统一走OpenAI兼容协议(这也是目前几乎所有主流模型服务商都在用的标准),对MCP协议的原生支持更是让模型可以无缝调用外部工具服务。Spring AI Alibaba这类分支项目也把国内模型的对接链路做了完整封装。这些标准化的结果就是:你的业务代码只依赖Spring AI的抽象接口,模型厂商是谁、部署在哪里、本地还是云端,全都从代码层面解耦。
1.3 和LangChain4j相比,我为什么最终选了Spring AI
LangChain4j作为Java生态里最早的LangChain移植项目,确实也做得不错。它在Agent编排、记忆管理等方面的API设计很成熟,人群中评价也很高。但我在对比后还是选了Spring AI,原因主要是三个。
第一是血缘关系。Spring AI由Spring官方团队维护,天然和Spring Boot的配置体系、Actuator监控体系、Bean生命周期管理是一套东西。你在Spring AI里写的代码,未来能无缝吃到Spring生态的升级红利。比如Spring Boot升级后,AI模块的自动配置也能跟着调整,这让我这种老Spring用户很安心。
第二是标准协议的支持深度。Spring AI 2.0在MCP和结构化输出方面做得非常彻底。MCP客户端的自动发现与注册机制很轻量,结构化输出对Java泛型类型的处理也很顺滑,后面的实战章节我会展开讲。
第三是模型接入的覆盖面。Spring AI官方提供了OpenAI、Ollama、Azure、Bedrock等多个接入模块,社区又有Spring AI Alibaba补全了国内模型阵容,你基本不会遇到“某个模型供应商没有适配”的情况。LangChain4j当然也支持很多模型,但遇到冷门供应商时,常常需要自己写适配器,这不是不行,但多一事不如少一事。
当然这不是捧一踩一,工程选型本来就是结合团队情况的权衡。但如果你本身就是Spring技术栈,我倾向于认为Spring AI是投入产出比更高的选择。
2. 从零到能对话:一个纯Java项目只需要动三个文件
2.1 环境检查清单:先确认版本再动手
Spring AI 2.0对Java版本有明确要求,这一点很多人会忽略。我建议至少使用JDK 17,如果条件允许直接上JDK 21。JDK 8在这个框架面前是彻底无能为力的,原因是Spring AI底层依赖了Spring Boot 3.x,而Spring Boot 3.x从Java 17起步。如果你所在的公司还在用JDK 8,先把基础版本升级这件事当成前置条件,否则后续每一步都会遇到编译错误。
Spring Boot的版本最好和Spring AI官方兼容矩阵保持一致。以我手头的项目为例,用的是Spring Boot 3.4.x搭配Spring AI 2.0.x。你在创建新项目时,最好去Spring Initializr上直接勾选Spring AI相关依赖,这样它会自动帮你匹配兼容版本,比自己对着文档查省心太多。
构建工具方面,Maven 3.6+或Gradle 7.5+都可以。国内团队用Maven居多,下面的示例默认用Maven。另外建议在IDE里安装Lombok插件并且确认注解处理已经开启,因为后面写实体类的时候大概率要用的,而Lombok和新版JDK之间时不时的兼容性小坑也是真实存在的,提前装好能省一道麻烦。
2.2 依赖与配置文件:这一份可以直接抄
我们用一个最简单的Spring Boot Web项目来演示。这个项目要做的事就一件:接收HTTP请求,调用大模型,返回回答。
pom.xml里需要引入Spring AI的starter。这里用OpenAI兼容协议接入,因为国内很多模型服务商(比如DeepSeek、通义百炼的兼容模式等)都提供OpenAI风格的API端点,用一个starter就能覆盖多家。
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-openai</artifactId> <version>2.0.0</version> </dependency> </dependencies>注意这个starter的名字是“model-openai”,它表示的是协议兼容,不是只允许连OpenAI官方。只要目标服务的API风格是OpenAI兼容的,都能通过配置接入。
application.yml里最核心的配置就四个字段:模型服务地址、API Key、模型名称、可选温度参数。
spring: application: name: spring-ai-demo ai: openai: base-url: ${AI_BASE_URL:https://api.deepseek.com} api-key: ${AI_API_KEY:sk-xxxxxxxx} chat: options: model: ${AI_MODEL_NAME:deepseek-chat} temperature: 0.7环境变量AI_BASE_URL、AI_API_KEY、AI_MODEL_NAME的存在是为了避免把密钥硬编码进配置文件。这点很重要,尤其是项目代码要提交到Git仓库的场景。之前见过不少人把api-key直接写死在yml里,结果代码一泄露,key马上被人盗刷,教训很深刻。
2.3 模型API选型:开发和线上怎么搭配更省钱
模型API的选择影响开发调试效率和线上成本,我的建议是分环境用不同模型。
开发阶段可以用本地Ollama部署的小参数模型,比如qwen2.5:7b、llama3.1:8b这类。好处是零成本、不限流、不依赖外网,改Prompt和调参数可以快速迭代。缺点是生成质量和速度不如云端商用模型,但这对于功能联调来说完全够用。
线上环境再切换到云端商用模型。国内可选范围很大:DeepSeek的API、阿里云百炼上的通义千问系列,都是OpenAI兼容协议,配置方式几乎不用改。前文示例里用的就是这种方案。
还有一个折中做法是开发环境直接接云端模型的免费试用额度,比如新用户赠送的token额度,或者一些开放平台的开发者免费档位。如果只是写Demo、做技术验证,这会比本地模型效果更好。唯一的风险是免费额度有效期短,不适合长期开发。下面这张表是我实际对比过的选型思路,供参考。
| 使用阶段 | 首选方案 | 优点 | 缺点 |
|---|---|---|---|
| 本地开发调试 | Ollama + qwen2.5:7b | 免费、离线、稳定 | 小参数模型效果有限 |
| 功能验证阶段 | 云端模型免费额度 | 效果接近线上 | 额度有限,过期后需付费 |
| 生产环境 | DeepSeek/通义等商用API | 效果好、SLA有保障 | 按token计费 |
配置层面,本地Ollama只需要把base-url改成http://localhost:11434,模型名改成本地模型名,其他代码完全不用动。这就是Spring AI抽象层带来的实际好处。
3. 10分钟跑通第一个对话:用ChatClient写一个可用的AI接口
3.1 最简可运行代码:Controller + ChatClient
现在进入正题。假设你已经建好了一个Spring Boot Web项目,并且把前面的依赖和配置都配好了,接下来的代码量少得会让你惊讶。
首先写一个Controller:
@RestController @RequestMapping("/api/ai") public class ChatController { private final ChatClient chatClient; public ChatController(ChatClient.Builder builder) { this.chatClient = builder.build(); } @GetMapping("/chat") public String chat(@RequestParam(defaultValue = "介绍一下你自己") String message) { return this.chatClient.prompt() .user(message) .call() .content(); } }就这么点代码。ChatClient由Spring AI自动配置的Builder建造,构造器注入即可用。prompt()开启一次对话,user(message)设定用户消息,call()发起同步调用并等待模型返回,content()取出文本内容。启动应用,浏览器打开http://localhost:8080/api/ai/chat?message=你好,几秒种后就能看到模型回话。
这个接口已经能完成最基本的单轮对话。如果你只想验证“Spring AI是否能跑通”,到这里就够了。从创建项目到跑通,花费的时间确实不会超过10分钟,这也是我为什么在标题里敢用“10分钟集成”。
但要注意一个小细节:ChatClient实例本身不是无状态的工具类,它在内部可以携带一些默认配置,比如系统提示词、默认模型参数、可能注册的函数工具。所以同一个Spring Boot应用里可以同时创建多个不同配置的ChatClient Bean,分别服务不同的业务场景。这种设计我们在后面会用到。
3.2 理解ChatModel、ChatClient和Prompt三者之间的关系
如果你刚接触Spring AI,可能会被ChatModel、ChatClient、Prompt这几个概念搞晕。我用自己的理解给你捋一下。
ChatModel是最底层的模型抽象,负责直接对接具体的模型API。OpenAI模型、Ollama模型、通义模型都能用对应的ChatModel实现来替换。它有点像DataSource,定义了数据库访问的底层能力,但你平时不会直接拿DataSource去拼SQL执行。
ChatClient则是面向业务开发者的高层API。它把Prompt构建、模型调用、返回解析、工具注册这些繁琐细节都封装好了,类似JdbcTemplate对DataSource的那一层封装。你的业务代码里只需要和ChatClient打交道。
Prompt是每一次请求的输入封装。它不仅包含用户说的话,还包含系统提示词、消息历史、模型参数(温度、最大Token数等)等完整上下文。ChatClient的prompt().user(...).call()就是一个构建Prompt并执行的过程。
用数据库开发来类比,ChatModel是JDBC Driver层面的东西,ChatClient是Spring JDBC,Prompt则是你传入的一条SQL加参数列表。这个类比可能不是百分百精确,但足够帮助你建立初始的理解框架。用顺手之后,你会发现最频繁打交道的其实是ChatClient。
3.3 流式输出:让AI回答一行一行“长”出来
接手过真实项目后你会意识到,同步返回对用户体验并不友好。尤其模型的生成时间往往有数秒甚至十几秒,让用户一直干等页面转圈,非常煎熬。流式输出(Streaming)能把模型的生成过程实时推给前端,呈现“打字机”效果,体验会好一大截。
Spring AI对SSE流式输出的支持同样很简洁:
@GetMapping(value = "/chat/stream", produces = "text/event-stream") public Flux<String> chatStream(@RequestParam String message) { return this.chatClient.prompt() .user(message) .stream() .content(); }区别就两个地方:返回值类型变成了Flux<String>,终止操作从call()换成了stream()。call()是同步阻塞等待全部结果,stream()则返回一个响应式流,内容会分块推送。WebFlux的Flux类型天然支持SSE协议,前端用EventSource或者fetch流式读取都能对接。
这里要提醒一个常见的坑:把响应式重返值写到Controller时,如果你的项目只引入了spring-boot-starter-web(Spring MVC),而没有引入spring-boot-starter-webflux,Flux可能没法直接使用。原因在于spring-webmvc本身不包含Reactive Streams的类型。解决方案有两个:简单一点,在pom里额外引入webflux依赖(虽然有点重);讲究一点,直接用WebFlux构建整个服务。根据我自己的经验,对于一个纯AI后端服务,直接上WebFlux往往更契合流式输出的场景。
4. 企业级标配:结构化输出与多模型切换
4.1 让大模型直接返回Java对象,而不是手工解析JSON
Python中转服务时代最折磨人的一件事就是JSON对账。模型返回一段JSON,Python那边先解析成dict,再转换成Java团队约定的DTO结构,一旦key大小写不一致或者嵌套层级变了,两边就要反复联调。Spring AI 2.0的结构化输出能力,直接把这个痛点从根上解决了:你让模型直接返回一个Java对象。
看这个例子。我希望模型帮我分析一段用户评论,提取出摘要、关键词和情感倾向。
public record ReviewAnalysis( String summary, List<String> keywords, boolean positive) {} @GetMapping("/analyze") public ReviewAnalysis analyze(@RequestParam String content) { return chatClient.prompt() .user("请分析下面这条用户评论: " + content) .call() .entity(ReviewAnalysis.class); }核心是末尾的entity(ReviewAnalysis.class)。Spring AI会为这个Java类型生成一个JSON Schema描述,把它放进Prompt里,要求模型严格按照该结构返回,最后再把模型输出的JSON自动反序列化成ReviewAnalysis对象。整个过程你不需要手写一行JSON解析代码。
如果返回的是泛型类型,比如List<ReviewAnalysis>,就用ParameterizedTypeReference:
List<ReviewAnalysis> list = chatClient.prompt() .user("请分析这批评论: " + contents) .call() .entity(new ParameterizedTypeReference<List<ReviewAnalysis>>() {});这种写法在处理批量数据、报表统计、信息抽取等场景里非常实用。我之前用一个接口从几百份合同文档里提取“合同编号、甲方、乙方、金额、有效期”,返回一个List<ContractInfo>,然后直接批量写入数据库。整个过程只用了不到50行Java代码,这在以前至少需要一整个Python服务才能做到。
顺带说一句,结构化输出对模型的JSON生成能力有一定要求。经过实测,主流的商用大模型问题都不大,但一些本地的小参数模型偶尔会格式翻车。遇到这种情况,可以在Prompt里再强调一下“只输出JSON对象”,或者考虑把模型换成能力更强的版本。
4.2 一套代码接入多家模型:配置决定一切
上面4.1的代码示例里,你从头到尾没有见过“DeepSeek”“通义”“Ollama”任何一家厂商的专属API。这是因为Spring AI把所有模型厂商都收编到了同一个ChatModel接口后面。所以“多模型切换”在代码层面基本是零改动,变的只有配置。
在实际项目里,我是这样利用这个特性的。
项目里定义一个ChatClient,然后定义三个Spring Profile:dev、test、prod。dev环境的配置指向本地Ollama,test环境指向云端模型免费测试Key,prod环境指向商用API。同一个Java进程的心理上没区别,部署到不同环境时,通过环境变量或配置中心切换Profile,整个AI能力就从本地小模型平滑切换到了云端大模型。应用代码中关于AI的这个部分不用动任何一个字节。
具体配置分两种情况:
接Ollama本地模型,引入:
<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-ollama</artifactId> </dependency>配置:
spring: ai: ollama: base-url: http://localhost:11434 chat: options: model: qwen2.5:7b接国内云端模型,以OpenAI兼容协议为例(这也是最常见的形态,绝大多数服务商都支持),配置几乎和章节2.2里一样。如果你想用阿里云Spring AI Alibaba,官方也提供了封装更好的starter,用法异曲同工。
这种“代码不变、只改配置”的多模型切换能力,对运维和架构的友好程度怎么高估都不过分。设想一下,当某个模型服务商涨价或者出故障时,你只需要改配置切换,而不是改Java代码重新发版,这对一个生产服务来说意味着什么,做过多模型接入的人心里都清楚。
4.3 MCP:Spring AI 2.0最容易被低估的扩展机制
MCP,全称Model Context Protocol,是这两年大模型应用领域最热门的协议之一。一句话描述它的作用:把“模型调用外部工具”这件事标准化。你可以把MCP理解成AI领域的USB接口。USB让不同类型的设备通过统一接口连接电脑,MCP让不同模型可以通过统一接口连接外部数据和工具服务。
Spring AI 2.0从很早起就原生支持MCP客户端。这意味着你只需要在配置里声明某个MCP Server的地址,Spring AI就能自动发现它暴露的工具,并把这些工具注册给ChatClient,让模型在回答时自动决定是否需要调用它们。
举个实际例子。假设公司内部已经有人搭建了一个订单查询MCP Server,暴露了一个queryOrder工具。你的业务服务只要在配置里加上MCP服务地址,然后在ChatClient构建时开启MCP工具支持,就能让模型具备“查询订单状态”的能力。
spring: ai: mcp: client: enabled: true connections: order-service: type: sse url: http://order-mcp-server:8080/sseJava代码里大致这样使用:
@Bean ChatClient chatClient(ChatClient.Builder builder) { return builder .defaultTools("mcp:queryOrder") .build(); }当用户问“我的订单OD20241201现在什么状态”,模型会把这条问题转成一次对queryOrder的调用,拿到返回结果后再组织成自然语言回答给用户。这个过程中,你没有写一行“调用订单服务的HTTP代码”,模型却自动完成了工具调用。
需要提醒的是,MCP不是银弹。它带来便利的同时也增加了链路复杂度,特别是MCP Server的可用性和权限管理是必须关注的。我的经验是:优先把成熟稳定的内部系统封装成MCP服务,让团队内多个AI应用复用;但对于一次性使用的小功能,直接用函数调用(第5章会讲)反而更轻量。
5. 实战进阶:RAG和函数调用,让大模型真正进入业务系统
5.1 二选一之前,先搞清楚RAG和微调的边界
把大模型接进业务系统时,几乎所有人都会遇到同一个问题:模型不懂我们公司的内部知识,比如员工手册、产品文档、私有业务数据。解决方案主要有两个:微调和RAG。
微调是拿一批标注数据去训练模型权重,让模型“学会”某种特定知识或表现风格。它的成本比较高,需要整理数据、准备微调环境、甚至可能需要GPU资源,而且模型每次升级后微调结果可能还要重新做。普通业务团队不建议一上来就微调。
RAG,即检索增强生成,是完全不同的思路。它不改变模型本身,而是在模型回答问题之前,先从你的私有知识库中检索相关内容,把相关内容作为参考上下文塞进Prompt里,让模型基于这些资料组织回答。这就像给模型发了一张“开卷考试的小抄”,模型不需要背下你的全部知识,只需要会读小抄。
对大多数企业知识问答、制度查询、产品咨询类场景,RAG是性价比更高的选择。Spring AI 2.0对RAG提供了从文档解析、切分、向量化、存储到检索增强的完整链路支持,接下来用一个最小可运行的示例带你走通这条链路。
5.2 从文档到问答:一个你能跑通的RAG最小实例
一个完整的RAG流程,其实就分两大步:第一步把知识文档“灌”进向量库,第二步在提问时做相似度检索并交给模型回答。
第一步的建设代码示例如下:
@Service public class KnowledgeIndexService { private final VectorStore vectorStore; public KnowledgeIndexService(VectorStore vectorStore) { this.vectorStore = vectorStore; } public void indexDocument(String filePath) { var reader = new TikaDocumentReader(new FileSystemResource(filePath)); var splitter = new TokenTextSplitter(); var documents = splitter.apply(reader.get()); vectorStore.add(documents); } }TikaDocumentReader负责从docx、pdf等格式的文档中抽取文本,TokenTextSplitter会把大段文本拆成固定大小的chunk,VectorStore负责把每个chunk向量化并存储。三步走下来,你的私有知识就进向量库了。
第二步的问答接口:
@GetMapping("/ask") public String ask(@RequestParam String question) { var searchRequest = SearchRequest.builder() .query(question) .topK(4) .build(); var similarDocs = vectorStore.similaritySearch(searchRequest); String context = similarDocs.stream() .map(Document::getText) .collect(Collectors.joining("\n---\n")); return chatClient.prompt() .system("你是一个企业内部知识助手,请基于提供的参考资料回答问题。") .user("参考资料:\n" + context + "\n问题:" + question) .call() .content(); }这个接口做的事情很直观:先用question在向量库里检索最相关的4个文档片段,把它们拼成上下文,再交给模型生成回答。
你可以直接把员工手册、产品说明书这类文档塞进知识库,然后问“年假怎么休”“退款流程是什么”这种内部问题,效果比直接裸问模型好得多。我在测试环境用一套公司自己的制度文档跑这个流程,回答的准确率从裸模型的30%左右直接升到80%以上,差距非常明显。
需要特别注意的是向量库选型。Spring AI支持Redis、PGVector、Milvus等主流向量存储。如果你的公司运维能力强,PGVector可以直接复用现有PostgreSQL,不用额外引入新组件;如果数据量很大、并发要求高,Milvus更合适。先在本地用Redis和docker跑通Demo,是最快的路径。
5.3 函数调用:把自己写的Java Service暴露给大模型
RAG解决的是“模型不知道”的问题,函数调用解决的是“模型做不了”的问题。两者结合,才能让AI应用从“聊天机器”进化成“数字员工”。
Spring AI 2.0的函数调用非常方便,核心就是@Tool注解。看下面这个例子:
@Service public class OrderTools { private final OrderMapper orderMapper; public OrderTools(OrderMapper orderMapper) { this.orderMapper = orderMapper; } @Tool(name = "queryOrderStatus", description = "根据订单号查询订单当前状态") public String queryOrderStatus(@ToolParam(description = "订单号") String orderId) { OrderDO order = orderMapper.selectByOrderId(orderId); if (order == null) { return "订单不存在"; } return "订单状态: " + order.getStatus(); } }然后构建ChatClient时把这个工具注册进去:
@Bean ChatClient chatClient(ChatClient.Builder builder, OrderTools orderTools) { return builder .defaultTools(orderTools) .build(); }之后用户问“订单OD20241201到哪一步了”,模型会自动决定调用queryOrderStatus方法,拿到返回结果再组织语言回答。没有写任何调度逻辑,模型自己在做工具选择。
我在实战中最大的体会是:函数调用是把大模型从“玩具”变成“生产力工具”的关键一步。让它能查数据库、能调内部API、能发工单,它才能真正承担业务动作。RAG负责“知识补给”,函数调用负责“动手做事”,两者配合起来,AI应用的能力边界一下子就被打开了。
6. 避坑实录:Spring AI 2.0实战中我踩过的7个坑
6.1 JDK与Lombok的经典冲突
踩过最直接的一个坑是开发环境用JDK 21,但Lombok版本太旧,一编译就报java: you aren't using a compiler supported by lombok, so lombok will not work。这个报错看着吓人,实际原因就是Lombok版本和JDK版本不匹配。解决方案是升级Lombok到1.18.30及以上版本,同时在IDE的编译配置里确认注解处理器是开启状态。如果项目里还有老版本的mapstruct等注解处理库,得一起升级,否则会连锁报错。
遇到java.lang.NoClassDefFoundError: java/applet/Applet这种异常时,先检查依赖树里是不是有某个老库引用已被新版JDK移除的类。这个报错通常是第三方依赖和JDK版本不兼容导致的,常见解法是升级那个老库,或者排除掉无用的旧依赖。
6.2 Spring Boot版本与自动配置不匹配
Spring AI对Spring Boot版本要求非常严格。我一度在旧项目上直接把Spring AI 2.0的依赖塞进去,结果启动时ChatModel的Bean根本不生成,后面所有注入ChatClient的地方都直接抛NoSuchBeanDefinitionException。
排查方向很明确:去Spring Initializr生成一个正确的项目模板,对照依赖版本号和父POM版本修自己的pom。最稳妥的做法是别在自己老项目里硬改版本,新建一个配置正确的项目骨架,然后把业务代码迁移过来。这比纠结一整天版本的性价比高很多。
6.3 BaseUrl和API Key填错时的经典表现
这类配置错误通常不会直接报错,而是表现为“请求超时”或者“HTTP 404/401”。其中一个印象深刻的场景是:我把base-url配成了平台的主站地址而不是API端点地址,模型接口请求一直404。排查时先打印配置属性确认base-url和api-key是否正确加载,再比对服务商文档里的API Path。
还有一个比较隐蔽的坑是API Key带上了多余的前缀。比如某些平台要求Key以特定前缀开头,配置时又重复加了一遍,结果一直鉴权失败。这类问题在看日志时容易发现,对你所在的服务商文档逐字节比对即可。
6.4 超时与长任务:别让对话接口半路卡死
同步调用大模型接口,最怕的就是默认超时时间过短。模型响应本来就要几秒,遇到高峰期甚至几十秒,连接超时一旦触发,客户端就会拿到504。Spring AI底层走的是RestClient,超时配置可以在application.yml里用spring.ai.openai.http-client配置,或者自定义一个ClientHttpRequestFactory。RAG场景尤其要注意,因为一次回答涉及向量检索+模型生成,总耗时可能比纯对话多出一倍,不给足超时时间肯定不行。
流式输出也有自己的坑,当客户端断开连接后,服务端如果响应流没有正确取消,会出现线程堆积和内存泄漏。项目里我的处理是给Flux加超时与取消订阅机制,避免异常断开时无限生成。
6.5 结构化输出在老旧模型上翻车
章节4.1的结构化输出很美好,但它依赖模型对JSON Schema的理解和执行能力。本地部署的小参数模型、比较老旧的API版本,可能返回的内容里有额外说明文字,导致JSON解析失败甚至得到null。遇到这种情况,先去模型侧确认版本是否支持结构化输出,再考虑让模型“只输出合法JSON”作为兜底提示。
要是上了生产还经常翻车,建议彻底切换到结构化输出兼容性更强的商用模型。这类问题在本地用小模型测试时最容易放水,因为测试数据少,偶尔解析失败不会引起重视,上线后量一旦上来就会暴露。
6.6 依赖冲突:Jackson和WebFlux的“跟班问题”
Spring AI 2.0的依赖里包含Jackson和Reactor相关组件,如果你的项目里这两个库有老版本定制,极其容易产生NoSuchMethodError这类运行时错误。
排查经验是把核心依赖的版本和Spring Boot BOM对齐,用mvn dependency:tree查看冲突点。如果是老项目从Spring Boot 2.x升级到3.x,这里可能会花不少时间。涉及到JsonMapper的定制、ObjectMapper的全局配置覆盖,都会影响结构化输出的反序列化行为。建议在项目里统一通过Spring Boot的Jackson自动配置来定制ObjectMapper,而不是到处new ObjectMapper。
6.7 Observation和指标监控:可观测性链路别忽略
Spring AI 2.0集成了Micrometer Observation机制,可以在模型调用、MCP工具调用时自动产生Metrics和Trace。这是个很好的能力,但也是一个坑位:如果你引入相关依赖却配置不当,在某些场景下可能影响调用链路。我之前排查过一次MCP调用偶尔挂起的问题,最后定位到是观测处理器的订阅逻辑在异常断开后没有释放连接。
遇到AI调用偶发卡住的情况,记得顺手看一眼Actuator暴露的Metrics,如果模型调用计数器正常但工具调用耗时异常,优先排查MCP和观测链路,而不是盲目怀疑模型API本身。
写在最后:哪些场景我现在敢真正去掉Python,哪些我还会留一手
经过这一轮实战,我个人的判断是:企业内部知识问答、客服助手、结构化数据抽取、NL2SQL这类偏业务集成的大模型场景,Java团队现在完全可以自己用Spring AI承接,不用再依赖Python中转服务。RAG和函数调用这两个能力覆盖了绝大多数内部工具类需求,开发效率也不输Python生态。
但有两类场景我依然不会把Python甩开。一类是大规模模型微调和训练,数据清洗、训练脚本、GPU调度这套东西Python生态的积累是碾压性的,用Java硬写属于自讨没趣。另一类是重度依赖Python科学计算库的复杂数据处理,比如音视频分析、复杂的统计模型、深度调优的RAG整体稳定性评测,这些场景用Python工具链快速验证,把结果用接口暴露给Java侧反而更合理。
我觉得更务实的策略是:全公司的AI基础设施,让Python团队去优化模型侧的东西;而业务侧的AI应用,由Java团队用Spring AI直接写。两边通过标准API解耦,而不是靠一堆不稳定的中间服务耦合。最后一个建议是开发顺序上,先把最简单的ChatClient对话接口跑通,再逐步加结构化输出、加RAG、加函数调用,每走一步都做一次小验证。不要一上来就想搭一个大而全的AI平台,步子迈太大,最后只会卡在踩坑和调错的泥潭里。