资讯动态

Spring AI + 千问3.5:Java一行代码集成大模型实战

发布时间:2026/10/21 14:41:36 来源:尧图企业网站定制
1. 项目动机为什么是 Spring AI 千问 3.5先把结论放在前面Java 生态接入大模型现在确实有一条低门槛的路就是 Spring AI 这个官方框架加上国内可直连的千问 3.5 模型。它的核心卖点是一行代码级别的集成体验让原本需要自己拼 HTTP 请求、处理鉴权、解析流式响应的活儿全部被框架消化掉了。先说为什么选千问 3.5。国内开发者接入大模型第一道坎就是网络连通性和合规性。千问 3.5 的接口域名在国内直接可访问不需要额外处理网络代理问题这对于公司内部环境、服务器部署来说省了非常多事。同时它兼容 OpenAI 的接口协议格式这意味着 Spring AI 里很多现成的抽象可以直接复用不需要为了某一个厂商的私有协议去写适配层。从实际测试来看千问 3.5 在中文理解、代码生成、逻辑推理这几个高频场景下的表现已经能覆盖大多数业务需求尤其是做智能客服、内容摘要、代码辅助这类场景性价比很突出。另外一个很现实的问题是团队技术栈。很多企业级项目的后端就是 Java Spring Boot如果为了接大模型去引入 Python 微服务等于凭空多了一套系统要维护。Spring AI 存在的意义就是让 Java 团队用自己熟悉的依赖注入、配置中心、模板引擎这些老伙计去玩转大模型学习成本被压到很低。项目里集成大模型这件事从“跨团队协作的基建工程”降级成了“普通后端功能开发”。所以这个项目标题里最值得拆解的关键词其实不是“大模型”而是“集成方式”。技术选型的本质是选一种让团队最舒服的接入姿势Spring AI 恰好把这个姿势做到了最佳。提示这个博文不是要科普大模型原理而是聚焦在“Java 项目如何快速、稳地接入大模型能力”这条实操路径上。适合已经有一定 Spring Boot 基础、想在业务系统里加 AI 能力的开发者阅读。2. 整体设计思路拆解Spring AI 到底解决了什么问题2.1 没有 Spring AI 之前Java 接大模型是什么体验在 Spring AI 出现之前Java 项目接大模型基本是两条路。一条是直接用 HTTP 客户端比如 OkHttp、RestTemplate去调大模型厂商的 API自己拼接请求体、处理鉴权头、解析 JSON 响应遇到流式输出还得自己维护 SSE 连接代码写起来又臭又长而且每个厂商的接口细节都不一样换一家模型就得改一遍代码。另一条路是引入一些社区自制的 SDK但这些 SDK 的维护质量参差不齐有的只支持某一家模型有的长期不更新接口设计也谈不上规范用起来心里没底。更麻烦的是一旦业务里要用到大模型的多个能力比如对话、图片生成、Embedding你会发现每个能力都要写一套调用逻辑。代码里充斥着魔法字符串、硬编码的 URL、散落的 API Key维护起来相当痛苦。项目里加一个 AI 功能本来应该是很爽的事结果变成了给代码库埋雷。2.2 Spring AI 的核心设计统一抽象 模块化适配Spring AI 的设计思路很像 JDBC、Spring Data它把大模型厂商的差异封装在统一接口后面。开发者面向接口编程需要换模型厂商时只改配置和依赖核心业务代码几乎不用动。核心抽象有这么几个ChatModel统一对话模型接口不管底层是千问、通义还是别的兼容模型调用方式都是call()或stream()。EmbeddingModel统一向量化模型接口做 RAG检索增强生成时用到。ImageModel统一文生图模型接口。Prompt统一的提示词封装支持多消息结构、系统消息、历史消息。ChatResponse统一的响应封装包含生成文本、元数据、用量信息。架构上采用“核心模块 厂商适配模块”的组合方式。核心模块spring-ai-core定义抽象和通用流程厂商模块比如spring-ai-starter-model-qwen负责把千问的独特协议翻译成核心接口能理解的语言。这个设计的好处是把“变”的部分隔离在适配层把“不变”的接口稳定在核心层。从使用者的视角看最大的感受就是不需要关心千问的 API 是 GET 还是 POST、请求头长什么样、响应里哪个字段是文本内容。这些脏活累活框架全包了你只需要写一个ChatModel的 Bean然后像调用本地 Service 一样用它。2.3 为什么是“一行代码”集成标题里说“一行代码集成大模型”实际体验确实接近这个描述。引入依赖之后只需要在配置类里声明一个ChatModel的 Bean填写模型名称和 API Key然后注入到任何地方Configuration public class AiConfig { Bean public ChatModel chatModel() { return ChatModel.builder() .defaultOptions(QwenChatOptions.builder() .model(qwen3.5) .temperature(0.7) .build()) .build(); } }之后在 Service 里用起来就非常自然了。构造一个Prompt传入用户消息拿到ChatResponse提取生成的文本Service public class ChatService { private final ChatModel chatModel; public ChatService(ChatModel chatModel) { this.chatModel chatModel; } public String ask(String question) { Prompt prompt new Prompt(question); ChatResponse response chatModel.call(prompt); return response.getResult().getOutput().getText(); } }这基本就是最核心的两段代码。要完成“一句话问答”的场景就这么多。对比手写 HTTP 调用的版本代码量至少缩减了百分之七十而且可读性、可维护性完全不在一个量级。3. 核心细节解析与实操要点3.1 依赖引入版本选择是第一个坑Spring AI 从 2024 年开始快速迭代不同版本的 API 有差异。接入前最稳妥的做法是到它的官方文档去查当前版本对应的 Maven 坐标。以 1.0.0 系列为例需要在父级pom.xml里加入dependencyManagement dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-bom/artifactId version1.0.0/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement然后在业务模块里加入dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-qwen/artifactId /dependency依赖版本统一由 BOM 管理避免各个模块版本冲突。注意Spring AI 的版本号迭代非常快不同版本之间 Bean 的名称、方法签名可能有变化。如果你在搜索答案时看到的是半年前的帖子代码大概率跑不通。最好的方式是直接看当前版本的官方文档和示例仓库。另外如果你的项目因为某些原因无法直接访问 Maven 中央仓库可以去看看 Spring 的官方仓库地址这部分在搭建私有仓库时需要提前配置好。3.2 配置项解析API Key 不要硬编码千问模型的 API Key 在 Spring AI 中的配置路径是spring.ai.dashscope.api-key。完整的基础配置如下spring.ai.dashscope.api-key${DASHSCOPE_API_KEY} spring.ai.dashscope.chat.options.modelqwen3.5 spring.ai.dashscope.chat.options.temperature0.7 spring.ai.dashscope.chat.options.max-tokens2048这里有几个要点值得单独拿出来说。环境变量注入 API Key。直接把 Key 写在application.properties里是新手最容易犯的错误代码提交到 Git 仓库后 Key 就泄露了。用${DASHSCOPE_API_KEY}的方式从环境变量读取是基本的安全生产习惯。在公司内部可以接入配置中心统一管理本地开发时在 IDE 的启动配置里添加环境变量即可。温度参数不是越大越好。temperature控制生成内容的随机性取值范围一般是 0 到 1。需要确定性输出的场景比如提取关键信息、生成 JSON建议调到 0.2 左右创意写作、头脑风暴可以调到 0.8。日常开发中拿不准的时候用 0.7 是个比较通用的选择。max-tokens决定单次生成的最大长度。要注意它包含输入和输出的总 token 数不是只算输出。如果业务里经常传长文档进去做摘要默认值可能不够要根据实际测试结果调整。3.3 流式输出聊天机器人体验的关键调用大模型接口时非流式模式下要等后端完全生成完才返回结果长回答可能需要几十秒用户端表现为“一直转圈”。流式输出SSE是聊天场景的标配能边生成边推送大幅改善体验。Spring AI 的流式调用代码非常简洁public void streamChat(String userMessage) { Prompt prompt new Prompt(userMessage); chatModel.stream(prompt).subscribe(chatResponse - { String delta chatResponse.getResult().getOutput().getText(); // 通过 WebSocket 或 SSE 推送给前端 sendToClient(delta); }); }stream()方法返回的是 Reactor 的Flux意味着天然支持背压、组合操作符可以和 WebFlux 无缝配合。前端展示打字机效果的典型实现就是后端拿到增量文本后通过 WebSocket 推送前端逐字渲染。实操经验在流式场景中响应对象里的文本可能是分段返回的前端拼接时不要自行加判断逻辑保持简单的字符串追加即可。结束回话的标记由后端的结束事件处理前端只需要监听关闭事件。3.4 Prompt 的高级玩法系统消息和 Few-Shot很多人的第一个 Demo 只是把用户问题原样传给模型但真实业务里 Prompt 工程才是拉开效果差距的关键。Spring AI 的Prompt支持完整的消息结构。要用好系统消息来约束模型行为import org.springframework.ai.chat.messages.SystemMessage; import org.springframework.ai.chat.messages.UserMessage; SystemMessage system new SystemMessage(你是一个精通 Spring Boot 的架构师回答要简洁、准确优先给出代码示例。); UserMessage user new UserMessage(如何设计微服务的配置中心); Prompt prompt new Prompt(List.of(system, user));更进阶的玩法是 Few-Shot在系统消息里提供几个问答示例让模型模仿示例的格式和风格String systemText 请将用户的输入转换为 SQL 查询语句只输出 SQL不要输出其他内容。 示例1 输入查询所有年龄大于30岁的用户 输出SELECT * FROM users WHERE age 30; 示例2 输入统计每个城市的订单数量 输出SELECT city, COUNT(*) FROM orders GROUP BY city; ;一次性把所有场景约束在系统消息里比在每次用户输入里重复强调要干净得多。模型对系统消息的遵循度通常比对用户消息高这是 Prompt 工程的一个基本规律。3.5 结构化输出不要只拿字符串直接让模型返回文本然后自己解析字符串你迟早会被模型的格式漂移坑哭。让模型输出严格的 JSON 结构是生产级项目的硬性要求。Spring AI 支持把模型输出直接绑定到 POJO 上。以“从用户输入中提取订单信息”为例public record OrderInfo( String orderId, String customerName, ListString items, BigDecimal totalAmount ) {}调用方式ChatClient chatClient ChatClient.builder(chatModel).build(); OrderInfo order chatClient.prompt(帮我查一下订单号 A10023 的客户名字和总价) .call() .entity(OrderInfo.class);框架内部会引导模型生成符合结构的 JSON并自动反序列化。用这种方式业务代码里不用再写 JSON parse 字段校验了模型输出的质量反而更高因为系统提示词里会自动注入目标结构的描述。注意entity()方法要求目标类有可访问的无参构造或支持 record字段名会和模型输出的 key 做映射。在模型输出偶尔缺失字段时反序列化会报错建议对必要字段做容错处理。4. 实操过程与核心环节实现4.1 硬性环境要求实操之前先确认环境这里踩过坑的人不少JDK 17 及以上。Spring AI 基于 Spring Boot 3JDK 8 是跑不起来的。Maven 3.6 或 Gradle 7.5。一个千问模型的 API Key到对应平台申请即可新用户一般有免费额度。可以正常访问外网的服务器或本地环境。4.2 完整接入步骤从零到流式对话第一步创建 Spring Boot 3 项目。这一步用 IDE 自带的初始化工具就行语言选 Java、构建工具选 Maven、Spring Boot 版本选 3.2 以上。第二步引入 Spring AI 依赖按前面说的方式加 BOM 和 qwen starter。第三步配置application.propertiesserver.port8080 spring.application.namespring-ai-qwen-demo spring.ai.dashscope.api-key${DASHSCOPE_API_KEY} spring.ai.dashscope.chat.options.modelqwen3.5 spring.ai.dashscope.chat.options.temperature0.7第四步建一个简单的 ControllerRestController RequestMapping(/api/chat) public class ChatController { private final ChatModel chatModel; public ChatController(ChatModel chatModel) { this.chatModel chatModel; } PostMapping(/ask) public MapString, String ask(RequestBody MapString, String request) { String question request.get(question); String answer chatModel.call(new Prompt(question)) .getResult() .getOutput() .getText(); return Map.of(answer, answer); } }到这一步一个能用的 REST 接口已经完成了。可以用 curl 做冒烟测试curl -X POST http://localhost:8080/api/chat/ask \ -H Content-Type: application/json \ -d {question: 用一句话解释什么是依赖注入}第五步增加流式接口配合前端打字机效果。用 WebFlux 的FluxString直接返回浏览器可以通过 SSE 消费PostMapping(value /ask/stream, produces MediaType.TEXT_EVENT_STREAM_VALUE) public FluxString askStream(RequestBody MapString, String request) { String question request.get(question); Prompt prompt new Prompt(question); return chatModel.stream(prompt) .map(response - response.getResult().getOutput().getText()); }前端用EventSource或fetch读取这个接口的数据流就行。注意fetch对 POST SSE 的支持比较有限实测中建议换成 WebSocket或者用text/event-stream配合专门的 SSE 客户端库。4.3 参数选择与调优一份实测参考提供一个基准配置供参考实际生产环境根据自己的场景调整参数取值适用场景temperature0.2信息抽取、格式化输出、代码生成temperature0.5通用对话、问答temperature0.8创意写作、头脑风暴max-tokens1024短对话、关键词提取max-tokens2048常规问答、摘要max-tokens4096长文档摘要、复杂分析需要提醒的是温度不是唯一影响输出的参数。top_p也有类似作用一般保持默认即可不需要额外调整。如果同时调温度和 top_p可能导致输出不可控。4.4 集成到 Spring 生态缓存和异步大模型调用是昂贵的操作同一问题重复触发很浪费。接入 Spring Cache 的成本极低Service public class CachedChatService { private final ChatModel chatModel; public CachedChatService(ChatModel chatModel) { this.chatModel chatModel; } Cacheable(cacheNames ai-answers, key #question) public String ask(String question) { return chatModel.call(new Prompt(question)) .getResult() .getOutput() .getText(); } }配合 Redis 作为缓存存储相同问题的重复查询直接从缓存返回响应时间从秒级降到毫秒级。要注意的是只有确定答案不会随时间和上下文变化的问题才适合加缓存需要实时性的场景比如查天气就别这么干。异步调用也是生产级必备。用Async注解把耗时操作丢到线程池Async(aiTaskExecutor) public CompletableFutureString askAsync(String question) { return CompletableFuture.completedFuture( chatModel.call(new Prompt(question)).getResult().getOutput().getText() ); }线程池建议单独定义避免和业务线程池互相干扰Bean(aiTaskExecutor) public Executor aiTaskExecutor() { ThreadPoolTaskExecutor executor new ThreadPoolTaskExecutor(); executor.setCorePoolSize(5); executor.setMaxPoolSize(20); executor.setQueueCapacity(100); executor.setThreadNamePrefix(ai-); executor.initialize(); return executor; }5. 常见问题与排查技巧实录5.1 API Key 报错、401 认证失败这是接入时最常碰到的问题占比很高。排查顺序看环境变量是否设置成功。在启动类里临时加一行System.out.println(System.getenv(DASHSCOPE_API_KEY))启动时确认值非空。确认 Key 没有多余空格。从网页复制 Key 时很容易把换行符或空格带进去可以先.trim()试一下。确认 Key 对应的账号有该模型的访问权限。有些模型需要单独申请开通没开通的话即使 Key 正确也会报 401。查看官方文档确认请求接口地址是否配置正确。Spring AI 的默认地址一般没问题但如果你在配置里覆盖了 URL就要仔细核对。5.2 依赖冲突、NoClassDefFoundError引入 Spring AI 后项目起不来多半是传递依赖冲突。常见情况Spring Boot 版本过低。Spring AI 要求 Spring Boot 3.x3.0 以下直接编译报错。Jackson 版本被项目中其它库覆盖。Spring AI 依赖 Jackson 处理 JSON如果项目里有旧版本 Jackson序列化就会出问题。日志实现冲突。排查思路是先用mvn dependency:tree -Dincludesorg.springframework.ai查看依赖树确认引入的版本是否正确。5.3 流式输出乱码、前端收到一整段文本SSE 流式输出乱码九成是字符集问题。确认响应头设置了Content-Type: text/event-stream;charsetUTF-8Controller 的方法上可以用produces强制指定。前端解析时也要用 UTF-8 解码。如果前端没有收到增量而是一次性拿到全部内容可能有几个原因后端实际调用的是非流式接口代码里用了call()。网关或代理缓冲了响应。Nginx 默认会对 SSE 做缓冲需要在代理配置里关掉缓冲。前端没有正确处理增量事件把数据攒到最后才渲染。5.4 有时返回空字符串或内容被截断这种现象出现频率不高但遇到会很崩溃。通常和以下原因有关输入超长。检查请求内容是否超过模型的上下文长度超长时模型可能返回空或截断。需要做输入截断或者换用上下文更大的模型版本。命中了内容安全策略。某些敏感输入会被安全策略拦截返回空的拒绝信息。这属于正常机制代码层面做兜底文案提示即可。max-tokens 设置过小导致生成到一半就停了。这时候检查响应的getMetadata里的finishReason确认是否因为max_tokens而终止。5.5 Token 费用失控生产环境里最容易被忽视的问题。控制成本的经验对用户输入做长度限制超长直接拒绝。增加缓存层减少重复请求。设置单日调用次数上限超出后返回友好提示。流式输出虽然体验好但同等长度下费用更高因为多次请求传递的消息更长。高频小消息场景建议走非流式。5.6 经验速查表症状大概率原因优先级动作启动失败版本不兼容、依赖冲突查看依赖树升级 Spring Boot 到 3.2401Key 错误/未开通模型权限检查环境变量和账号权限超时网络问题/模型响应慢加大超时时间或换异步调用返回格式乱没有用结构化输出改用entity()绑定 POJO截断max-tokens 太小/输入超长调大 max-tokens截断输入内容为空安全策略/输入超长检查finishReason和输入长度6. 从 Demo 到生产几个容易被忽略的工程化问题6.1 模型版本管理千问 3.5 这类模型名称在不同时期可能有版本后缀的变化。代码里写死modelqwen3.5的问题在于模型方升级时可能兼容性有变动。建议做法将模型名称放到配置中心或环境变量里不要硬编码在代码中。上线前先到平台查一下当前可用的模型版本避免配置一个已下线的名称。遇到模型行为突然变化优先怀疑是不是版本被切换了。6.2 鉴权与多租户如果你的系统有多个租户、多个业务方API Key 可能不止一个。Spring AI 允许在每次调用前动态设置授权信息而不仅仅在启动时配置一个固定的 Key。实际做法是扩展一个RequestOptions在构造Prompt时传入不同的模型配置。这要求你对 Spring AI 的 Bean 创建方式做一点调整不能直接用Value注死一个 Key。多租户场景下把 Key 存在数据库里每次请求根据上下文动态选择是相对稳妥的方案。6.3 Retry 与兜底策略大模型接口不是 100% 可用网络抖动、服务端过载都可能发生。Spring AI 支持集成 Spring RetryConfiguration public class RetryConfig { Bean public ChatModel chatModel(ChatModel delegate) { return RetryableChatModel.create( delegate, RetryConfig.builder() .maxAttempts(3) .backoff(BackoffConfig.builder() .delay(Duration.ofSeconds(1)) .multiplier(2) .build()) .build() ); } }但要注意Retry 只对幂等请求安全。如果业务逻辑里有副作用比如生成后直接入库重试可能导致重复数据需要配合去重逻辑。兜底策略指的是当模型不可用时返回内置的默认回答或者降级到上一个版本的缓存结果。这块需要和业务方明确需求不要默默吞掉异常。6.4 局部代码里的敏感内容过滤生产环境接大模型内容安全是绕不开的课题。模型提供方自身有安全过滤但业务侧也要有防线。特别是 UGC 类场景用户输入和模型输出都要过一遍关键词过滤或内容审核接口避免违规内容进入业务流程。这一点在写技术方案时容易被忽略但上线评审时基本必问提前做比事后补要省心得多。7. 一些亲历的坑和建议这个项目做完之后最大的感受是 Spring AI 把接入成本降得很低但工程化的问题一个都没少。表面上是“一行代码集成”实际要把缓存、鉴权、重试、监控全部补上才能算一个合格的生产功能。几件值得记住的事第一版本锁定。Spring AI 迭代太快如果项目会长期维护建议在 GitHub 上关注它的发布记录每个版本的 Breaking Change 都列得很清楚。升级时不要一把梭先在测试环境跑一遍全量测试。第二日志要留请求和响应摘要。线上排查问题时没有请求日志直接抓瞎。可以写一个简单的过滤切面把每次调用的 token 数、耗时、模型名记录下来。这个数据同时可以做成本核算和性能监控。第三Prompt 模板要独立管理。把 Prompt 直接写在 Java 代码里后续维护会很难受尤其当这些 Prompt 要经过多次调优的时候。建议用模板文件或者配置中心管理甚至在系统里做一个简单的 Prompt 管理页面业务人员也能参与调优。第四模型的能力边界要提前跟产品对齐。千问 3.5 虽然很强但也有不擅长的事比如超长文档的精确引用、高精度数学计算、敏感的时效性查询。把这些边界在需求评审阶段讲清楚能避免后面很多扯皮。这个项目后续扩展的空间也很大。可以做函数调用Function Calling让模型触达业务 API可以做 RAG 让模型基于私有知识库回答也可以做多轮对话的状态管理。Spring AI 对这些场景都有对应的抽象顺着这条技术路线走下去Java 团队完全能撑起一个有业务深度的 AI 产品。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑