news 2026/10/1 3:48:51

Spring AI 实战入门:从零构建 Java AI 应用与流式输出

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring AI 实战入门:从零构建 Java AI 应用与流式输出

1. 为什么 Java 开发者现在该认真看一眼 Spring AI

Java 生态里做 AI 集成这件事,过去两年一直有点尴尬。Python 那边 LangChain、LlamaIndex 玩得风生水起,Java 开发者想接个大模型,要么自己封装 HTTP 客户端,要么在项目里塞一堆非 Spring 风格的胶水代码,维护起来相当别扭。Spring AI 出现之后,这个局面算是有了正经的解法——它把大模型调用抽象成了 Spring 生态里熟悉的 Bean、Template、Advisor 这一套东西,写起来跟用 JdbcTemplate 或者 RestTemplate 的感觉差不多。

这篇内容面向的是有 Java 和 Spring Boot 基础、但还没真正上手过 Spring AI 的开发者。我会从零开始,把构建第一个 Java AI 应用的完整路径走一遍:环境怎么搭、ChatClient 怎么配、Prompt 怎么写、流式输出怎么做、常见坑怎么排。中间涉及到的参数选择、依赖取舍、代码组织方式,我都会把背后的理由讲清楚,而不是只丢一段能跑的代码给你。

需要先说明一点:Spring AI 目前迭代速度很快,1.0 之前的版本 API 变动比较频繁,我下面用的写法基于较新的稳定版本,如果你用的是更早的里程碑版本,部分类名和方法签名可能会有差异,遇到对不上的地方优先查官方文档的对应版本说明。

2. 环境准备与项目骨架搭建

2.1 JDK 与构建工具的最低要求

Spring AI 对 JDK 的要求跟着 Spring Boot 走。当前主流版本要求 JDK 17 起步,如果你还在用 JDK 8 或者 11,第一步就是升级。这不是可选项,因为 Spring AI 内部大量使用了 record、密封接口、文本块这些新特性,低版本 JDK 直接编译不过。

构建工具用 Maven 或 Gradle 都行,我个人偏向 Maven,因为 Spring AI 的 BOM 在 Maven 里引入比较直观。Gradle 用户注意一下,依赖管理需要用 platform 语法引入 BOM,否则版本号得一个个手写,很容易出现某个模块版本对不齐导致 NoSuchMethodError。

具体版本选择上,我的建议是:

组件推荐版本说明
JDK17 或 2121 是 LTS,虚拟线程对高并发调用有帮助
Spring Boot3.2.x 及以上3.2 之前的部分自动配置不完整
Spring AI最新稳定版里程碑版本慎用于生产
构建工具Maven 3.8+低版本对 BOM 支持有瑕疵

提示:不要混用 Spring Boot 2.x 和 Spring AI,Spring AI 的自动配置类是基于 Spring Boot 3 的 AutoConfiguration.imports 机制注册的,2.x 根本加载不到。

2.2 用 Spring Initializr 生成骨架的正确姿势

最省事的起步方式还是 Spring Initializr。打开网页或者用 IDE 内置的创建向导,选好 Maven、JDK 17、Spring Boot 3.2.x,然后在依赖里勾选 Spring Web(后面做流式接口要用)和 Spring AI 相关的 starter。

这里有个细节很多人会踩:Spring AI 的 starter 不在默认的依赖列表里,需要手动在 pom.xml 里加仓库配置。因为部分版本还托管在 Spring 的里程碑仓库,如果你的项目拉不到依赖,八成是仓库没配。稳妥的做法是在 pom.xml 里显式声明:

<repositories> <repository> <id>spring-milestones</id> <name>Spring Milestones</name> <url>https://repo.spring.io/milestone</url> <snapshots> <enabled>false</enabled> </snapshots> </repository> </repositories>

然后在 dependencyManagement 里引入 Spring AI 的 BOM,这样后面加具体模型 starter 的时候就不用写版本号了。BOM 的好处是统一版本,避免你手动指定 openai starter 是 0.8.0、core 是 0.9.0 这种错配。

2.3 模型 starter 的选择逻辑

Spring AI 支持多种模型提供方,OpenAI、Azure OpenAI、Anthropic、Ollama、以及国内的几家都有对应的 starter。选哪个取决于你的实际场景:

  • 想快速验证、不折腾本地环境:用 OpenAI 的 starter,配一个 API Key 就能跑。
  • 数据不能出内网、要本地推理:用 Ollama,本地拉个模型跑起来,starter 直接连本地端口。
  • 企业内已经有 Azure 资源:用 Azure OpenAI starter,配置项多一些但合规性好。

我下面以 OpenAI starter 为主线演示,因为它的 API 最典型,换成其他 starter 时核心的 ChatClient 用法几乎不变,只是配置项前缀和模型名不一样。这个抽象层设计正是 Spring AI 的价值所在——业务代码不绑死具体厂商。

3. ChatClient 核心用法拆解

3.1 ChatClient 与 ChatModel 的关系

刚接触 Spring AI 的人容易把 ChatClient 和 ChatModel 搞混。简单说,ChatModel 是底层接口,负责真正跟模型服务通信,不同厂商有不同实现;ChatClient 是上层门面,提供 fluent API,让你用链式调用的方式组织请求。类比一下,ChatModel 像 JdbcTemplate 底层的 DataSource,ChatClient 像你直接用的 JdbcTemplate。

实际写业务代码时,绝大多数情况你只需要注入 ChatClient,不用直接碰 ChatModel。Spring AI 的自动配置会根据你引入的 starter 自动创建对应的 ChatModel Bean,然后你可以基于它构建一个 ChatClient:

@Configuration public class AiConfig { @Bean public ChatClient chatClient(ChatModel chatModel) { return ChatClient.builder(chatModel) .defaultSystem("你是一个严谨的 Java 技术助手,回答尽量给出可运行的代码示例。") .build(); } }

这里用 builder 而不是直接 new,是因为 builder 允许你设置默认的 system prompt、默认的 advisor、默认的选项参数。把 system prompt 放在这里统一管理,比每次调用都手写一遍要清爽得多,也方便后续统一调整语气和约束。

3.2 一次完整调用的代码结构

注入 ChatClient 之后,最简单的调用长这样:

@Service public class ChatService { private final ChatClient chatClient; public ChatService(ChatClient chatClient) { this.chatClient = chatClient; } public String ask(String question) { return chatClient.prompt() .user(question) .call() .content(); } }

这段代码里有几个关键点值得展开。prompt()开启一次请求构建,user()设置用户消息,call()表示同步调用,content()取出模型返回的文本内容。整条链是懒执行的,只有到call()或者stream()的时候才真正发请求。

如果你需要更细的控制,比如同时设置 system 和 user 消息,可以这样:

String answer = chatClient.prompt() .system("用不超过三句话回答") .user("解释一下什么是 AQS") .call() .content();

注意.system()会覆盖掉 builder 里设置的默认 system prompt,如果你希望保留默认的再追加,得用 advisor 的方式处理,这个后面讲。

3.3 Prompt 模板与参数化

硬编码 prompt 在 demo 里没问题,真实项目里几乎一定要用模板。Spring AI 提供了 PromptTemplate,支持占位符替换:

PromptTemplate template = new PromptTemplate( "请用{style}的风格,解释{concept}这个概念,控制在{words}字以内。" ); Prompt prompt = template.create(Map.of( "style", "通俗易懂", "concept", "面向对象编程", "words", "200" )); String result = chatClient.prompt(prompt).call().content();

用模板的好处是 prompt 可以外置到配置文件或者数据库,改文案不用重新编译。我一般会把常用的 prompt 模板放在 resources 目录下的独立文件里,通过 Resource 加载,这样产品和运营也能参与调整。

注意:占位符的 key 如果传了 null,PromptTemplate 默认会抛异常。如果你的参数可能为空,要么提前做默认值处理,要么在模板里用条件语法,别指望它自动忽略。

3.4 结构化输出:让模型返回对象而不是字符串

直接拿字符串在很多场景下不够用,比如你想让模型返回一个 JSON 然后映射成 Java 对象。Spring AI 提供了.entity()方法做这件事:

record BookInfo(String title, String author, int year) {} BookInfo info = chatClient.prompt() .user("给我介绍一下《Effective Java》这本书的作者和出版年份") .call() .entity(BookInfo.class);

底层它会自动在 prompt 里追加格式说明,然后把返回的文本反序列化成你指定的类型。实测下来,对于 record 和简单的 POJO 效果不错,但字段一多、嵌套一深,模型偶尔会漏字段或者格式跑偏。我的经验是:结构化输出尽量保持扁平,字段控制在五六个以内,嵌套层级不要超过两层,稳定性会好很多。

4. 流式输出与接口层实现

4.1 为什么流式输出是刚需

大模型生成一段几百字的回答,同步调用可能要等好几秒甚至十几秒。用户盯着一个转圈的加载图标等十秒,体验是很差的。流式输出让内容一个字一个字往外蹦,首字延迟通常在一秒以内,感知上快很多。做聊天类应用,流式基本是标配。

Spring AI 的流式调用用.stream()替代.call(),返回的是一个 Flux(响应式流):

Flux<String> stream = chatClient.prompt() .user("讲讲 Java 的垃圾回收机制") .stream() .content();

这里返回的 Flux 每个元素是一小段文本增量,不是完整句子,前端拼接起来才是完整回答。

4.2 用 SSE 把流推到前端

后端要做的就是把 Flux 转成 SSE(Server-Sent Events)推给浏览器。Spring MVC 和 WebFlux 的写法略有不同,用 WebFlux 更自然:

@RestController public class ChatController { private final ChatClient chatClient; public ChatController(ChatClient chatClient) { this.chatClient = chatClient; } @GetMapping(value = "/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux<String> stream(@RequestParam String q) { return chatClient.prompt() .user(q) .stream() .content(); } }

前端用 EventSource 接收即可。这里有个容易忽略的点:produces必须显式声明为text/event-stream,否则浏览器不会按 SSE 解析,你会看到一堆原始文本堆在一起。

4.3 流式场景下的异常处理

流式接口的异常处理比同步麻烦,因为响应头可能已经发出去了,这时候再想返回一个 500 错误页已经来不及。我的做法是在 Flux 上加onErrorResume,把异常转成一段友好的文本推给前端:

return chatClient.prompt() .user(q) .stream() .content() .onErrorResume(e -> Flux.just("[生成中断] " + e.getMessage()));

同时后端要打日志记录完整堆栈,方便排查。前端收到这种带标记的文本时,可以特殊渲染成错误提示样式。

提示:流式接口不要设置过短的超时。有些网关默认 30 秒超时,长回答会被截断。检查一下你的 Nginx 或者网关配置里的 proxy_read_timeout。

5. 常见问题排查与避坑经验

5.1 依赖冲突与 Bean 找不到

最常见的报错是启动时提示找不到 ChatModel 或者 ChatClient 相关的 Bean。原因通常有三个:一是 starter 没引对,比如引了 core 但没引具体厂商的 starter;二是 API Key 没配,自动配置因为缺少必要属性而跳过了 Bean 创建;三是包扫描路径不对,自定义的配置类没被扫到。

排查顺序建议这样走:先看启动日志里有没有ChatModel相关的自动配置报告,Spring Boot 的--debug模式会打印条件评估结果,能直接告诉你哪个条件没满足。然后检查 application.yml 里的配置前缀是否正确,不同 starter 的前缀不一样,OpenAI 是spring.ai.openai,Ollama 是spring.ai.ollama,写错了不会报错,只会静默不生效。

5.2 Prompt 被拦截或返回异常

有时候请求发出去,返回的是一段错误提示,说 prompt 违反了使用政策。这种情况多半是 prompt 里包含了敏感词或者被模型判定为不当请求。排查时先把 prompt 简化到最短,确认基础调用没问题,再逐步加回内容,定位是哪部分触发的。

另外注意 prompt 长度。每个模型都有上下文窗口限制,超了会直接报错。粗略估算:一个中文字符大约对应 1 到 2 个 token,英文单词大约 1.3 个 token。如果你要拼接很长的历史对话,记得做截断或者摘要,别一股脑全塞进去。

5.3 常见问题速查表

现象可能原因处理方式
启动报找不到 ChatModelstarter 未引入或 Key 未配检查依赖和配置前缀
调用返回 401API Key 无效或过期重新生成 Key 并更新配置
流式接口无输出produces 未声明或网关超时检查注解和网关超时设置
结构化输出字段缺失prompt 约束不够或模型能力不足简化结构、加强格式说明
响应特别慢模型选择或网络问题换更小的模型或检查网络链路
中文乱码编码未统一为 UTF-8检查请求和响应编码配置

5.4 几个我踩过的坑

第一个坑是 advisor 的顺序。Spring AI 的 advisor 链是有顺序的,如果你同时用了日志 advisor 和记忆 advisor,顺序不对会导致日志里看不到完整的上下文。默认顺序不一定符合你的预期,必要时显式指定 order。

第二个坑是对话记忆。默认情况下每次调用都是无状态的,模型不记得上一轮说了什么。要做多轮对话得引入 ChatMemory 相关的 advisor,并且注意内存的清理策略,否则长时间运行会越占越多。

第三个坑是并发。ChatClient 本身是线程安全的,可以单例注入。但如果你在 advisor 里放了可变状态,并发下就会出问题。我见过有人在自定义 advisor 里用一个成员变量存当前请求的上下文,高并发时串得一塌糊涂。记住 advisor 要么无状态,要么用 ThreadLocal 隔离。

6. 从 Demo 到可用应用的扩展方向

跑通第一个应用之后,往生产方向走还有几块要补。一是重试和降级,模型服务偶尔会抖动,加个带退避的重试策略能显著提升稳定性,Spring AI 本身对 RetryTemplate 有支持,配置一下就行。二是可观测性,把每次调用的耗时、token 消耗、模型名打成指标,接进 Micrometer,后面做成本分析和容量规划都用得上。三是 prompt 的版本管理,把 prompt 当代码一样管理,改动走评审,出问题能回滚。

RAG 是另一个绕不开的方向。单纯靠模型自身知识,回答企业私有领域的问题往往不准。把文档切块、向量化、存进向量库,检索后再拼进 prompt,这套流程 Spring AI 有对应的模块支持,Advisor 里也有现成的 QuestionAnswerAdvisor 可以用。不过 RAG 的坑主要在切块策略和检索质量上,这块展开又是另一个话题了。

至于到底用 Spring AI 还是别的 Java 方案,我的判断标准很简单:如果你的项目本来就是 Spring Boot 技术栈,团队熟悉 Spring 的编程模型,那 Spring AI 的迁移成本最低,心智负担最小。它的抽象层设计让你在换模型厂商时几乎不用改业务代码,这个价值在模型快速迭代的当下很实在。

最后分享一个实际用下来的小技巧:把 system prompt 里加上明确的输出格式约束和角色设定,比事后用代码去清洗模型输出要省事得多。比如要求"只返回 JSON,不要任何解释性文字",能省掉大量解析异常的处理逻辑。模型这东西,你越早把规矩立清楚,后面越省心。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 3:48:12

Unity投影阴影原理与自定义Shader接入:从Shadow Mapping到问题排查

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/1 3:47:48

HarmonyOS Java华容道开发:状态管理与UI解耦实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/1 3:43:46

AutoML实战:TPOT用遗传算法自动搜索机器学习管道

刚开始接触机器学习那段时间&#xff0c;我对手动调参有一种莫名的执念——总觉得要自己一个模型一个模型地试、一个参数一个参数地调&#xff0c;才算是“真正懂算法”。直到后来接了一个业务需求&#xff0c;老板只给三天时间就要出一版能跑的模型&#xff0c;几十个特征还带…

作者头像 李华
网站建设 2026/10/1 3:43:39

大模型推理提速三板斧:量化、投机采样与PD分离实战指南

每次我在社区帮人排查大模型推理速度问题时&#xff0c;都会遇到一个相同场景&#xff1a;显卡明明在跑&#xff0c;显存也没爆&#xff0c;但生成速度就是上不去&#xff0c;一个千字回答要等上一两分钟。任务管理器里看GPU利用率只有百分之二三十&#xff0c;算力根本没吃满。…

作者头像 李华
网站建设 2026/10/1 3:42:45

Qoder AI编程IDE全流程指南:安装配置、模型选型与Credits计费

最近 AI 编程工具真的是卷到飞起&#xff0c;前有 Cursor 打开局面&#xff0c;后有各种 Agent 工具轮番上阵。Qoder 是我最近在几个项目里实际用下来的一款 AI 编程 IDE/插件&#xff0c;如果你平时写前端、做全栈&#xff0c;或者一个人要扛好几个项目&#xff0c;它会很对你…

作者头像 李华
网站建设 2026/10/1 3:41:41

PMX骨骼名称对照与映射:解决MMD动作套用错位

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华