ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Spring AI 2.0实战:Java程序员用Spring Boot快速集成大模型

Spring AI 2.0实战:Java程序员用Spring Boot快速集成大模型 Spring AI 2.0 出来之后我身边越来越多写 Java 的同事开始认真思考一件事集成大模型是不是真的可以不用 Python我的答案是可以。最近我在两个内部项目里都用 Spring AI 2.0 做了大模型接入一个接的是云厂商的模型接口另一个接的是本地部署的开源模型。整个过程让我挺感慨Java 生态这次总算把 AI 应用开发这件事拉回到了自己熟悉的技术栈里。这篇内容主要面向被 Java 业务、技术方案、面试问题逼着接触大模型的开发者我想用实际踩坑的经验讲讲Spring AI 2.0 到底怎么帮你用熟悉的 Spring Boot 套路快速集成大模型以及为什么要摆脱“做 AI 就必须会 Python”的惯性思维。标题说 10 分钟集成不是夸张。我这边第一次跑通从建工程到返回模型结果确实不到 10 分钟。但别高兴太早后面做结构化输出、函数调用、RAG 这些工程化功能时还是有挺多细节要处理的。这篇文章会把整个链路拆开讲清楚保证你看完能直接照着做。1. 为什么说 Spring AI 2.0 是 Java 程序员入局大模型的快捷方式1.1 Java 做 AI 应用过去到底卡在哪说实话过去两年我见过太多 Java 团队做 AI 功能时陷入一个特别别扭的处境。业务后端是 Java 写的Spring Boot 那一套微服务体系已经建得很完善了结果一做大模型能力就得在旁边另起一个 Python 服务。Python 服务负责调模型 API、处理上下文、做向量检索Java 服务再通过 HTTP 去调 Python。链路一旦变长问题就跟着来了分布式排查麻烦、部署又多一套环境、Python 服务没人维护、出 bug 时两边开发互相甩锅。更要命的是有些 Java 程序员并不是不会写 Python而是根本没精力去管那个“额外的 AI 服务”。我们做后端的人日常已经被业务需求、接口设计、数据库优化塞满了根本没有时间再去维护一套单独的语言环境。但如果不这么做要直接在 Java 里调 OpenAI、通义千问这些模型的 HTTP API就得自己处理请求封装、重试、超时、流式响应、token 统计、上下文拼接等一堆基础问题。这些问题本身不难但非常琐碎而且每个模型厂商的接口格式都不一样。今天接 OpenAI 的协议明天换通义的协议代码就得推倒重来一部分。我之前自己封装过一个简单的调用客户端结果换模型服务商时光适配请求体就花了一个下午。1.2 Spring AI 2.0 到底解决了什么Spring AI 这个项目本质上是 Spring 官方给 Java 生态提供的一套 AI 应用开发框架。它做的事情和 Spring JDBC、Spring Cloud 在各自领域干的事是一样的把重复的、容易出错的底层操作抽象掉给你一套统一的、可配置的开发体验。具体到大模型接入上Spring AI 做了几件很关键的事情。第一屏蔽了不同模型厂商的差异。你写的业务代码不需要关心对面是 OpenAI、通义千问、Ollama 还是 Azure OpenAI只要用统一的 ChatClient 接口去发消息具体协议适配都交给 starter 处理。第二把 AI 应用的常见能力组件化了比如 Prompt 模板、结构化输出、RAG 向量检索、函数调用、可观测性埋点这些不是靠你自己拼凑而是框架里现成的组件。Spring AI 2.0 这个版本相比早期版本最大的改进是 API 终于稳定多了而且对可观测性的支持更深入。2.0 里你可以通过 ObservationHandler 拿到一次模型调用的详细指标包括耗时、token 使用量、是否命中缓存等直接把你熟悉的 Micrometer Observation 体系和大模型调用打通。对做中大型系统的人来说这一点特别重要。另外2.0 对 RAG 场景做了很多优化。官方文档里的 RAG 实例变得更加完整文档加载、切分、向量化、存储、检索、增强生成每一步都有对应的抽象组件。公司里如果要做私有知识库问答Spring AI 2.0 提供的就是一条比较顺畅的标准化路径而不是让你从头调研到底用什么向量数据库、用什么切分策略。1.3 什么人适合直接入坑我总结了一下下面这几类人现在开始学 Spring AI 是收益最高的。第一类是后端业务开发尤其是长期用 Spring Boot 写微服务的同学。你们不需要去研究模型训练、微调这些算法层面的东西只是想把模型能力封装成业务接口Spring AI 几乎是零学习成本切入。第二类是技术面试前需要项目亮点的人。现在很多 Java 岗位面试都会问到 AI 相关内容你如果能讲清楚 Spring AI 的 ChatClient、结构化输出、函数调用和 RAG 这几个点并且有真实工程经验说服力比背一堆八股文强太多了。第三类是团队里负责技术选型的人。你不需要把 Python 作为大模型集成的默认前提了Java 本身就能扛起应用层集成这面旗子。我不建议什么人学呢只做算法研究和模型训练的同学不建议。如果你日常工作就是训练模型、调参、做微调那 Python 生态仍然是主流Spring AI 不适合你。Spring AI 解决的是应用层集成问题不是算法研究问题。2. 实操准备把第一个工程跑起来2.1 你需要准备的物料清单10 分钟集成这件事前提是你把准备工作做足。我先列一下需要用到的物料后面再逐一说明。第一是 JDK建议直接用 17 或 21。Spring Boot 3.x 本身就要求 JDK 17 起Spring AI 2.0 也一样。千万不要用 JDK 8你会在启动阶段被各种依赖错误劝退。第二是构建工具Maven 或 Gradle 都行我用的是 Maven后面代码示例都基于 Maven。第三是一个可用的模型服务。有两种路线一种是用云厂商提供的模型 API你需要申请一个 API Key另一种是用 Ollama 这类工具在本地启动开源模型比如 Qwen、Llama 系列不需要 Key但需要你的机器有足够的配置。第四是网络连通性你的服务器或开发机要能访问到对应的模型服务地址。云厂商一般提供公网接口或内网网关本地部署就配置 localhost 就行。这些物料其实都不复杂。有人会问那我还需不需要先学 Python不需要这篇文章里的所有操作都在 Java 和配置文件里完成一行 Python 都不用写。2.2 用官网脚手架快速创建工程创建工程的推荐方式是访问 start.spring.io这是 Spring 官方的初始化工具和过去我们创建 Spring Boot 工程时用的流程完全一样。选好 Maven、Java 版本和 Spring Boot 版本然后在依赖搜索框里输入 spring ai会自动列出相关的 starter。如果你用 IDEA新建 Spring Boot 项目时也能直接搜到 Spring AI 相关依赖。添加依赖后脚手架会帮你生成一个基础工程接着在 pom.xml 里确认依赖就够用。这里要特别提醒一个版本问题。Spring AI 的版本迭代很快而且它和 Spring Boot 的版本有对应关系。你自己切版本的时候最好直接使用 spring-ai-bom 做统一管理避免 starter 之间版本不一致。pom 里大致是这样dependencyManagement dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-bom/artifactId version你的Spring AI版本/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement然后添加你需要的模型 starter。以 OpenAI 兼容接口为例依赖是这样dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-openai/artifactId /dependency如果你准备接通义千问、Ollama、Moonshot 这些就换成对应的 starter包结构和用法类似。核心思路是先加 BOM再加 model starter后面的配置就集中在 application.yml 里。我见过不少人在这里翻车直接不加 BOM 就引 starter结果不同 starter 里的传递依赖版本冲突启动时冒出一堆 NoClassDefFoundError。版本管理这种事交给 BOM 统一管是最省心的做法。2.3 写配置文件把模型服务连起来依赖加好之后最核心的步骤就是配置文件。以 OpenAI 协议为例在 application.yml 里配置 API Key 和模型名称spring: ai: openai: api-key: ${OPENAI_API_KEY} chat: options: model: gpt-4o-mini temperature: 0.7这里我强烈建议用环境变量方式注入 API Key不要硬编码在配置文件里。你永远不知道这个项目会不会被推到公共仓库硬编码 Key 等于把敏感信息送出去。即使只是内部项目也建议用环境变量或配置中心管理。如果你用的是第三方兼容 OpenAI 协议的模型服务可以指定 base-urlspring: ai: openai: base-url: https://你的模型服务地址 api-key: ${MODEL_API_KEY} chat: options: model: qwen-plus如果走 Ollama 本地部署路线配置更简单不需要 Keyspring: ai: ollama: base-url: http://localhost:11434 chat: options: model: qwen2.5要注意不同 Spring AI 版本里配置项的前缀可能会有调整加依赖后最好看一眼 jar 包里的 spring-configuration-metadata.json或者直接起项目看启动日志的配置提示。这种版本差异问题没什么好的记忆办法以当前使用的实际版本为准。配置文件写完后启动工程如果日志里没有报错说明你已经成功和模型服务建立连接了。接下来就可以写真正能返回内容的代码了。3. 核心玩法用 ChatClient 写第一个聊天接口3.1 一个 Controller 就能跑起来的思路Spring AI 的核心编程模型是 ChatClient。如果你用过 Spring 生态里的 WebClient你会觉得这个设计非常眼熟。ChatClient 把“组装提示词、调用模型、拿结果”这一整套流程封装成流式 API写起来很自然。先定义一个 Service 类注入 ChatClient.BuilderService public class AiChatService { private final ChatClient chatClient; public AiChatService(ChatClient.Builder builder) { this.chatClient builder.build(); } public String chat(String message) { return chatClient.prompt() .user(message) .call() .content(); } }这段代码做的事情很简单构造一个 prompt把用户的 message 放进去同步调用模型取回响应文本。你没有手动拼接 HTTP 请求没有处理重试没有解析 JSON这些全被框架消化了。接下来写一个标准的 Controller 暴露接口RestController RequestMapping(/api/ai) public class AiChatController { private final AiChatService aiChatService; public AiChatController(AiChatService aiChatService) { this.aiChatService aiChatService; } PostMapping(/chat) public MapString, String chat(RequestBody MapString, String request) { String reply aiChatService.chat(request.get(message)); return Map.of(reply, reply); } }启动应用用 curl 或 Postman 发一个请求curl -X POST http://localhost:8080/api/ai/chat \ -H Content-Type: application/json \ -d {message: 用一句话介绍Spring AI}只要配置没问题你会在几秒内拿到模型返回的内容。这时候一个最基础的 Java 版大模型接口就跑通了。3.2 系统提示词和多轮对话怎么维护实际业务里你不会满足于这种“用户问一句模型答一句”的模式。日常开发中更多是角色扮演、客服、内容助手这一类场景需要先给模型设定身份和行为准则这就需要用到系统提示词。ChatClient 的链式 API 里提供了 system 方法。比如做一个客服机器人ChatClient chatClient builder.build(); String reply chatClient.prompt() .system(你是一个售后客服助手语气亲切回答尽量简洁不要编造订单信息) .user(我的订单显示已发货但三天没更新物流状态怎么回事) .call() .content();这种情况下模型就会带上你设定的人设来回答。如果你有大量的固定规则可以把它抽成 Prompt 模板用 Spring AI 的 PromptTemplate 去渲染变量不要硬拼接字符串。多轮对话是很多人容易忽略的点。ChatClient 默认是无状态的它不知道上一次用户问了什么你每一次调用它都是独立的。要实现多轮会话需要在业务层自己维护消息记录。Spring AI 2.0 里提供了 ChatMemory 相关的抽象你可以把历史消息存到内存、Redis 或数据库里然后构造请求时把完整消息列表传进去。简单场景下你也可以自己维护一个 List 来存储历史消息组装请求时把历史消息包装后传入。这里给一个基于内存的简单示例ListMessage history new ArrayList(); history.add(new UserMessage(我叫张三)); history.add(new AssistantMessage(你好张三很高兴认识你)); history.add(new UserMessage(你还记得我叫什么吗)); String reply chatClient.prompt() .messages(history) .call() .content();核心思路是模型本身不具备记忆能力你要负责管理它的记忆。不要图省事把全部历史一股脑全塞给模型否则 token 消耗会越来越大到了某个阈值模型反而会“忘掉”最早的上下文。3.3 模型参数别什么都用默认值很多初学者上来直接用默认参数只在 user 里塞一句话。这样跑倒是能跑但离业务可用还有差距。实际项目中比较常调的是 temperature、maxTokens、topP 这几个。temperature 控制随机性范围一般是 0 到 2。我以前有个内容生成项目开始用默认值结果生成的内容总是过于发散经常带出一些不在原始材料里的说法。后来把 temperature 调到 0.3 左右输出明显稳定很多。如果你做的是客服问答、代码生成、数据抽取这类任务建议把 temperature 调低如果是做创意文案、头脑风暴可以适当调高到 0.8 左右。maxTokens 控制生成的最大 token 数。这个参数决定了模型最多能输出多长内容。如果你明确知道答案不会很长把它设置得小一点可以明显加快响应速度。比如做关键词抽取maxTokens 设置 100 就够了省得模型啰嗦。这些参数可以在 application.yml 里统一配置也可以在代码中按某次请求单独设置String reply chatClient.prompt() .user(写一封请假邮件大约50字) .options(ChatOptions.builder() .temperature(0.5) .maxTokens(200) .build()) .call() .content();我的建议是默认配置值只是让系统能跑起来不是让你不管质量。真正上线前一定要针对你的业务场景把所有参数都压测一轮尤其是 temperature 和 maxTokens这两个直接关系到输出质量和成本。4. 进阶实战从聊天到企业级功能4.1 结构化输出让模型返回 JSON 而不是散文如果只是做聊天机器人把模型当作一个文本生成器就够了。但到了企业业务里你往往需要模型输出可以被程序直接解析的数据。比如从用户评价中抽取“产品名、评分、问题类别”从合同里抽取“甲方、乙方、金额、日期”从工单里提取“紧急程度、责任方”等等。传统做法是让模型返回一大段文字然后你自己写正则或者规则去解析这实在太容易翻车了。模型的语言组织是千变万化的你永远猜不到它会把数字放在哪里。Spring AI 的结构化输出能力就是用来解决这个问题的。它支持通过 BeanOutputConverter 指定一个 Java 类型让模型按照这个类型的格式返回然后自动帮你把结果转成对应对象。假设你要从一条商品评价里抽取结构化字段public record ReviewInfo(String productName, int rating, String complaintType) {}调用方式大致是这样BeanOutputConverterReviewInfo converter new BeanOutputConverter(ReviewInfo.class); String response chatClient.prompt() .system(你是一个信息抽取引擎从用户评价中抽取结构化信息) .user(手机屏幕用了三天出现一条绿线申请换货) .options(converter.getResponseFormat()) .call() .content(); ReviewInfo info converter.convert(response); System.out.println(info.productName());这种写法比让模型自由发挥要可靠得多。我实际测试下来只要提示词把抽取目标说清楚结构化输出的准确率比自由文本高很多。更重要的是它返回的格式是固定可解析的你不需要再写正则去碰运气。有一个细节要提醒定义 record 时字段名尽量用英文小写驼峰避免过于复杂的嵌套结构。模型本质上是根据字段名来推断语义的字段名取得越直白输出越稳定。比如 complaintType 就比 ct 好得多。4.2 函数调用让模型去查数据库、查你们的接口对话功能跑通之后很快就有人问能不能让模型帮我查一下订单状态能不能让它调一个内部接口这就涉及到函数调用也叫工具调用。它的原理是你给模型描述有哪些函数可以用、各自是干什么的模型在回答时如果觉得需要查数据会返回一个函数调用请求你的代码收到请求后执行真实业务方法再把结果回传给模型模型根据结果组织最终回答。在 Spring AI 里你可以把普通方法包装成一个 FunctionCallback 注册成 Bean。比如有一个查天气的方法Bean public FunctionCallback weatherFunction() { return FunctionCallback.builder() .description(根据城市名查询当前天气) .inputType(WeatherRequest.class) .targetFunction((WeatherRequest request) - weatherService.getWeather(request.city())) .build(); }然后在对话请求里声明这个函数可用String reply chatClient.prompt() .user(北京今天适合出行吗) .function(weatherFunction, 查询天气) .call() .content();模型很聪明它看到问题里涉及“北京”和“天气”会自动触发 weatherFunction 去查询查询结果回来后它不会给你原样丢一个 JSON而是会用自然语言组织成一句通顺的回答。这个能力对企业应用特别有价值。你不需要把数据库暴露给模型也不需要把公司接口变成模型的知识你只需要授权模型在特定场景下调用你封装好的函数。权限、校验、风控逻辑全在你的函数里模型只是多了一个决策出口。4.3 RAG把公司私有知识库接进来结构化输出解决的是“让模型好好说话”的问题函数调用解决的是“让模型能动手查数据”的问题还有一个企业场景绕不开公司内部的文档、制度、产品说明模型根本不知道。你不可能训练一个私有模型去记这些东西成本太高了实际方案就是用 RAG检索增强生成。RAG 的思路很直接把公司文档先切段、向量化后存入向量数据库。用户提问时先从向量库里检索出和问题相关的几个片段然后把“问题检索到的片段”一起交给大模型让模型基于这些片段来回答。Spring AI 2.0 对 RAG 的支持比较完整。从文档读取、文本切分、向量化、存储到检索回答都有对应组件。最简单的做法是用 SimpleVectorStore 存在本地适合数据量不大的场景。大致步骤是这样先用 Document 对象装载文档内容用 EmbeddingModel 把内容转成向量存入 VectorStore然后在 ChatClient 的 prompt 里开启 RAG 增强。// 1. 读取文档并存入向量库 Document doc new Document(公司年假制度正式员工每年享有10天年假...); vectorStore.add(List.of(doc)); // 2. 用户提问时启用RAG顾问 String reply chatClient.prompt() .user(我入职一年了能休几天年假) .advisors(QuestionAnswerAdvisor.builder().vectorStore(vectorStore).build()) .call() .content();这里关键点是你不需要用网上找来的公开知识而是把公司自己的资料喂给系统。这也是 RAG 和直接 Prompt 最本质的区别Prompt 是命令模型“按我的要求说”RAG 是先“根据我的资料找证据”再让模型组织答案。RAG 项目想做好难点不在框架本身而在“资料整理”。同一个意思有多种表达文档切分粒度太大检索会引入噪音切分太小又可能丢失上下文。我的经验是先用小批量文档把链路跑通再逐步调整切分大小和召回数量不要一上来就想把整个文库都塞进去。4.4 可观测性和本地模型两个容易被忽视的点提到 Spring AI 2.0很多人会忽略它的可观测性能力。2.0 把大模型调用接入到 Micrometer Observation 体系你可以自定义 ObservationHandler在一此模型调用前、成功后、失败后分别做埋点。实际开发中我认为这个能力比大部分花哨功能都实用。你上线一个对话接口总得知道每天有多少次调用、平均耗时多少、token 消耗多少、失败率多高。这些数据如果靠自己在业务代码里埋点会写得非常凌乱。Spring AI 官方的做法是支持你注册一个 handler统一收集这些指标。如果你是个人项目或者本地实验可以用 Ollama 部署本地模型。好处是数据不出内网、没有 API 费用、响应速度快。Spring AI 对 Ollama 的支持和云模型服务是同一套抽象配置里改一下 base-url 和 model 就能切换不需要改业务代码。这对要做私有化交付的场景很有吸引力。5. 常见问题与避坑指南5.1 启动报错先检查版本再查代码我见过太多人引入 Spring AI 后启动直接失败第一反应是代码写错了其实大半都是版本和依赖问题。最常见的一种是 JDK 版本太低。Spring Boot 3.x 和 Spring AI 2.0 都要求 JDK 17 起步你如果还在用 JDK 8启动时会看到各种类找不到或者 UnsupportedClassVersionError。别犹豫先升级 JDK。第二种是 spring-ai-bom 版本和 Spring Boot 版本不匹配。Spring AI 对 Spring Boot 版本有对应关系跨版本硬搭轻则自动配置不生效重则启动直接报依赖冲突。我的建议是始终使用 BOM 管理版本不要自己手写一堆 starter 的具体版本号。第三种比较隐蔽如果你启动时看到一个报错内容是 java.applet.Applet 找不到不要慌。这种一般是你某个老的三方依赖还在引用 JDK 8 时代的遗留类库而 JDK 17 之后 Applet API 已经被移除了。解决办法是找到那个引入老类库的依赖并升级而不是去配什么启动参数强行兼容后者在正式项目里是给自己埋雷。这里还要提醒一个常见误操作有些同学看到 NoClassDefFoundError 就直接在搜索引擎里找答案把某个 jar 包乱加进去最后问题越解越多。正确做法是先看完整堆栈找到第一个引起问题的依赖坐标再去调整版本或排除传递依赖。5.2 请求超时、限流和突发流量大模型接口和普通 HTTP 接口有本质区别它慢。一次调用可能耗时几秒甚至几十秒如果你用了同步调用接口线程会被长时间占用。我们第一个线上版本就遇到过这种情况高峰期大量请求同时打进来模型服务开始限流用户端表现为转圈很久然后超时。解决思路有三个层面。第一调大 HTTP 客户端的超时时间。Spring AI 默认超时配置可能比较短在模型响应慢的时段容易触发超时。你需要根据模型服务的表现规划一个合理的 connect-timeout 和 read-timeout。第二在业务层做异步化。如果调用方不需要立刻得到结果可以先把请求扔到消息队列里模型生成结果后再通过回调或轮询返回。第三加一层结果缓存。相同或相似的问题完全可以缓存结果不需要每次都消耗真实模型调用。限流也得提前考虑。云厂商的模型 API 一般都有每分钟请求数和 token 数限制。如果你的用户量比较大建议在网关或应用层做本地限流不要让请求直接打到模型服务上。模型服务一旦开始返回 429你的降级策略必须跟上。5.3 模型胡说八道靠 Prompt 约束而不是靠模型换在使用过程中模型偶尔输出和事实不符的内容几乎是所有 AI 应用都会遇到的问题。有些人一遇到这种问题就想换更大的模型这不一定错但成本高、见效慢。我自己的经验是先检查 Prompt 设计再做结构化约束比盲目换模型有效。第一在系统提示词里明确“基于给定材料回答没有材料就说不知道”。对于客服、知识库问答场景这句话能显著降低胡编率。第二使用 RAG 时设置低一些的相似度阈值检索不到相关内容时宁可拒绝回答也不要让模型自由发挥。第三适当加入 few-shot 示例给模型一两个标准正确的输入输出样例它会更容易按照预期格式回答。我实际测试过一个场景不限制模型、不加任何约束时它会编造公司制度里不存在的内容。加了“只基于资料回答资料没有的明确说无法回答”之后胡编率明显下降。这就是 Prompt 设计的价值。还有一个容易被忽略的点模型输出不是你 Response 里的全部内容。Spring AI 会返回丰富的元数据包括 token 使用量、完成原因等。你要在日志里把 token 消耗记录下来这样才能知道成本到底花在哪儿也方便后续做优化。6. 写在最后一点实际体验我刚开始用 Spring AI 时心里也犯嘀咕觉得会不会又是一个被官方快速放弃的实验性项目。后来把结构化输出和函数调用做完后我的态度变了。它确实让 Java 程序员能用自己熟悉的框架思想去做 AI 应用而不是在一个全新的语言生态里重学一套开发模式。我觉得比较理想的打法是小步快跑先用 ChatClient 把最基础的对话链路跑通然后根据业务需求逐步引入结构化输出、RAG、函数调用和可观测性。不要一上来就搞大而全的方案那样只会把自己绕晕。最后给一个小建议做这类项目时一定要在日志里留下足够的现场信息。模型返回的内容、输入的 Prompt、耗时时长、token 消耗这些数据是排查线上问题和评估成本的关键。没有这些数据你在遇到问题时就只能靠猜。Spring AI 2.0 已经把这个能力很自然地放进来了别浪费它。
返回列表