☰
Spring AI 实战:Java 开发者构建第一个 AI 应用
2026/9/29 11:59:55 网站建设 项目流程

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

Java 生态里做 AI 集成这件事,过去两年一直有点尴尬。Python 那边 LangChain、LlamaIndex 玩得风生水起,Java 开发者想接个大模型,要么自己手写 HTTP 客户端拼 JSON,要么在项目里塞一个 Python 微服务做中转,维护成本高得离谱。Spring AI 出现之后,这个局面算是有了一个"官方味道"的解法——它把大模型调用抽象成了 Spring 风格的 API,跟JdbcTemplate、RestTemplate一个思路,用依赖注入和自动配置把底层差异抹平。

这篇内容面向的是有 Java 和 Spring Boot 基础、但还没碰过 Spring AI 的开发者。我会从零开始,把"构建第一个 Java AI 应用"这件事拆开讲透:为什么这么设计、ChatClient到底怎么用、Prompt 怎么写才不踩坑、配置项怎么填、遇到报错怎么排查。看完你应该能自己跑起来一个能对话、能带上下文、能切换模型的 Spring Boot 应用,而不是停留在"抄了个 demo 但不知道为什么"的状态。

需要先说明一点:Spring AI 迭代非常快,1.0 之前的版本 API 变动频繁,网上很多教程的包名和类名已经对不上了。我下面讲的内容以当前主流的 1.0.x 稳定线为准,如果你用的是里程碑版本,类路径可能略有差异,遇到对不上的地方优先看官方仓库的当前文档,别硬套老教程。

2. 动手之前:把 Spring AI 的定位和核心概念理清楚

2.1 Spring AI 到底解决了什么问题

先打个比方。没有 Spring AI 的时候,你调用大模型就像每次做饭都要自己去菜市场挑菜、砍价、洗切——每个厂商的接口格式、鉴权方式、返回结构都不一样,OpenAI 一套、通义一套、Ollama 本地又一套。你写一次业务逻辑,换模型就得改一遍代码。

Spring AI 干的事,相当于给你配了个"中央厨房"。它定义了一套统一的抽象层:ChatModel负责底层模型通信,ChatClient负责上层对话交互,Prompt封装输入,ChatResponse封装输出。你面向接口编程,换模型只需要换配置和依赖,业务代码基本不动。这就是 Spring 一贯的"面向抽象、依赖注入"哲学在 AI 场景的延伸。

它主要覆盖这几块能力:同步和流式的对话调用、Prompt 模板化、对话记忆(多轮上下文)、结构化输出(把模型返回映射成 Java 对象)、函数调用(让模型触发你的 Java 方法)、向量库集成和 RAG 检索。对绝大多数业务应用来说,前四项就已经能撑起 80% 的场景了。

2.2 几个必须搞懂的核心概念

ChatModel 与 ChatClient 的分工。ChatModel是底层接口,直接对接具体厂商,返回的是原始的ChatResponse,用起来比较"裸"。ChatClient是上层门面,提供prompt().user(...).call().content()这种链式写法,还内置了记忆、模板、默认系统提示等能力。日常开发优先用ChatClient,只有在需要精细控制底层参数时才直接碰ChatModel。

Prompt 的构成。一个 Prompt 通常包含三部分:系统消息(System Message,设定角色和行为边界)、用户消息(User Message,本次输入)、以及可选的助手历史消息(Assistant Message,多轮对话时带上)。很多人第一次用只写用户消息,结果模型行为飘忽不定,问题就出在没给系统消息定调。

对话记忆(Chat Memory)。大模型本身是无状态的,它不记得你上一句说了什么。所谓"多轮对话",本质是每次请求都把历史消息一起发过去。Spring AI 用ChatMemory帮你管理这个历史窗口,避免你手动拼接。但要注意,历史越长 token 消耗越大,所以它提供了窗口大小限制。

结构化输出。让模型返回一段 JSON,再自动映射成 Java 的 record 或 POJO,这是 Spring AI 很实用的一个能力。底层靠的是在 Prompt 里注入格式约束,再配合转换器解析。用好了能省掉大量手工解析字符串的脏活。

2.3 环境与依赖准备

基础环境就三样:JDK 17 或以上(Spring Boot 3.x 的硬性要求)、Maven 或 Gradle、一个可用的模型服务(云端 API 或本地 Ollama 都行)。我建议新手先用本地 Ollama 跑通流程,不花钱、不依赖网络、调试快,等逻辑通了再换云端模型。

Maven 里核心依赖是 Spring AI 的 BOM 加具体模型 starter。用 BOM 的好处是统一版本,避免各个 starter 版本打架:

<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-bom</artifactId> <version>1.0.0</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-ollama</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>

注意:Spring AI 的 starter 命名在 1.0 前后改过。老版本叫spring-ai-ollama-spring-boot-starter,新版本统一成了spring-ai-starter-model-ollama这种格式。如果你复制老教程的依赖发现拉不下来,八成是命名对不上,去中央仓库搜一下当前 artifactId 即可。

3. 第一个 AI 应用:从配置到跑通对话

3.1 配置文件怎么写才不出错

application.yml里主要是配模型服务的地址、模型名和参数。以本地 Ollama 为例:

spring: ai: ollama: base-url: http://localhost:11434 chat: options: model: qwen2.5:7b temperature: 0.7 num-ctx: 4096

这里几个参数值得说清楚。temperature控制随机性,0 到 2 之间,写代码、做抽取这类要稳定的任务调到 0.1~0.3,做创意文案可以到 0.8 以上。num-ctx是上下文窗口大小,决定了模型一次能"看到"多少 token,设太小会导致长对话被截断,设太大又吃内存,7B 模型一般 4096 够用。model必须是你本地已经ollama pull下来的模型名,写错了启动不报错,但调用时会返回模型不存在的错误。

如果换成云端服务,配置结构类似,只是把ollama换成对应厂商的节点,鉴权信息通常走api-key。我强烈建议把 key 放到环境变量里,别硬编码进 yml 提交到仓库,这是最基本的安全习惯。

3.2 注入 ChatClient 并发出第一次调用

Spring AI 的自动配置会帮你把ChatModel和ChatClient.Builder都注册成 Bean,你直接注入就能用。最简写法:

@RestController public class ChatController { private final ChatClient chatClient; public ChatController(ChatClient.Builder builder) { this.chatClient = builder .defaultSystem("你是一个严谨的 Java 技术助手,回答简洁,代码示例优先。") .build(); } @GetMapping("/chat") public String chat(@RequestParam String message) { return chatClient.prompt() .user(message) .call() .content(); } }

启动应用,访问/chat?message=什么是依赖注入,就能拿到模型回复。这段代码里有几个设计点值得琢磨:defaultSystem设定了全局角色,避免每次调用都重复写系统提示;prompt().user().call().content()这条链是 Spring AI 的标准调用姿势,call()是同步阻塞,content()取出纯文本。如果你想要流式输出,把call()换成stream(),返回Flux<String>,配合 SSE 就能做打字机效果。

3.3 Prompt 模板化:别把字符串拼接到处写

直接在代码里用+拼 Prompt 是新手最常见的坏习惯,一旦要改格式就得满项目找。Spring AI 提供了PromptTemplate,用占位符管理:

PromptTemplate template = new PromptTemplate(""" 请用{style}的风格解释下面这个概念,控制在{words}字以内: 概念:{concept} """); Prompt prompt = template.create(Map.of( "style", "通俗", "words", "100", "concept", "响应式编程" )); String result = chatClient.prompt(prompt).call().content();

模板的好处是格式和内容分离,改提示词不用动 Java 代码,还能把模板放到配置文件或数据库里做动态管理。实际项目里我习惯把常用模板集中到一个prompts目录,按业务命名,方便版本管理和复用。

3.4 加上对话记忆,让它记住上下文

单轮问答跑通后,下一步就是多轮。Spring AI 用ChatMemory管理历史,配合MessageChatMemoryAdvisor自动把历史注入每次请求:

@Bean ChatMemory chatMemory() { return MessageWindowChatMemory.builder() .maxMessages(20) .build(); } @Bean ChatClient chatClient(ChatClient.Builder builder, ChatMemory memory) { return builder .defaultSystem("你是一个耐心的技术顾问。") .defaultAdvisors(MessageChatMemoryAdvisor.builder(memory).build()) .build(); }

maxMessages(20)表示只保留最近 20 条消息,超出的自动丢弃。这个值要结合模型上下文窗口和单条消息长度来定,设太大容易超 token 限制,设太小又记不住关键信息。生产环境里更稳妥的做法是按 token 数而非消息条数来裁剪,或者用向量库做长期记忆检索,这个后面进阶再展开。

提示:MessageWindowChatMemory默认是内存实现,应用重启历史就没了。多实例部署时每个实例的记忆是独立的,用户请求打到不同实例会"失忆"。要跨实例共享,得换成基于 Redis 等外部存储的ChatMemoryRepository实现。

4. 结构化输出与函数调用:让 AI 真正接入业务

4.1 把模型返回映射成 Java 对象

让模型返回一段自由文本,再自己写正则去解析,是件很痛苦的事。Spring AI 的.entity()方法能直接把返回映射成 Java 类型:

record BookInfo(String title, String author, int year, List<String> tags) {} BookInfo info = chatClient.prompt() .user("介绍一下《Effective Java》这本书") .call() .entity(BookInfo.class);

底层原理是 Spring AI 会根据目标类型生成格式说明,注入到 Prompt 里约束模型输出 JSON,再用转换器反序列化。这里有个坑:模型不一定每次都严格返回合法 JSON,尤其是小参数模型。所以生产代码里一定要对解析失败做兜底,比如捕获异常后重试一次,或者降级返回默认值。另外字段类型尽量用包装类型和List,避免模型返回 null 时拆箱报错。

4.2 函数调用:让模型触发你的 Java 方法

函数调用(Function Calling)是让 AI 从"聊天"走向"干活"的关键。思路是:你把一个 Java 方法注册给模型,模型判断需要时返回一个调用意图,Spring AI 帮你执行方法并把结果回传给模型继续推理。

@Bean @Description("根据城市名查询当前天气") Function<WeatherRequest, WeatherResponse> weatherFunction() { return request -> weatherService.query(request.city()); }

注册后在调用时通过.tools()挂上,模型遇到"北京今天天气怎么样"这类问题,就会自动触发你的方法。这里的设计精髓在于:模型只负责"决定调哪个函数、传什么参数",真正的业务逻辑还是你的 Java 代码在跑,安全边界清晰。要注意的是,函数描述(@Description)写得越清楚,模型判断越准,含糊的描述会导致它该调不调、不该调乱调。

4.3 流式输出与前端配合

聊天类应用基本都要打字机效果。Spring AI 的流式接口返回Flux<String>,配合 Spring WebFlux 或 Spring MVC 的 SSE 都能实现:

@GetMapping(value = "/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux<String> stream(@RequestParam String message) { return chatClient.prompt() .user(message) .stream() .content(); }

前端用EventSource接收即可。实测下来,流式不仅体验好,首字延迟也明显更低——用户不用等整段生成完才看到内容。但要注意,流式模式下拿不到完整的ChatResponse元数据(比如 token 用量统计),如果业务需要计费或监控,得在流结束时单独处理。

5. 常见报错与排查速查

实际跑起来,新手最容易卡在几个地方。我把高频问题和排查思路整理成表,遇到问题先对照:

现象可能原因排查方向
启动报找不到 ChatModel Bean依赖没引对或模型服务未配置检查 starter artifactId 是否为当前版本命名,确认 yml 中模型节点存在
调用返回 404 或连接拒绝模型服务地址错误或未启动本地 Ollama 确认ollama serve在跑,base-url端口是否为 11434
提示模型不存在模型名拼写错误或未拉取执行ollama list核对模型名,注意带不带 tag
返回内容被截断上下文窗口或输出长度限制调大num-ctx,检查是否有 max-tokens 限制
结构化输出解析失败模型返回非法 JSON换更大参数模型,或在 Prompt 中强化格式约束,加异常兜底
多轮对话"失忆"记忆未配置或实例不共享确认 Advisor 已挂载,多实例场景换外部存储
中文乱码编码未统一确认请求和响应均为 UTF-8

除了表里的,还有两个我踩过的坑值得单独说。一是依赖版本冲突:Spring AI 对 Spring Boot 版本有要求,混用不兼容版本会出现各种诡异的 Bean 创建失败,用 BOM 统一管理能规避大部分问题。二是Prompt 被内容安全策略拦截:某些云端服务会对输入做合规检查,返回类似"prompt 被标记为可能违规"的提示。这种情况通常是输入里带了敏感词或特殊符号,换个表述、拆解输入往往能解决,别急着怀疑代码。

注意:调试阶段建议把日志级别调到 DEBUG,Spring AI 会打印实际发送的 Prompt 和收到的原始响应。很多"模型不听话"的问题,一看实际 Prompt 就明白了——往往是你以为传进去的内容和真正发出去的不一样。

6. 一些实操心得和后续扩展方向

跑通第一个应用只是起点。我在实际项目里积累了几条经验,分享给准备深入的人。

第一,别迷信大模型能搞定一切。涉及精确计算、强一致性的逻辑,老老实实写 Java 代码,让模型只做它擅长的语言理解和生成。函数调用就是干这个的——把确定性逻辑留在代码里,把模糊判断交给模型。

第二,Prompt 要当代码管理。版本化、可回滚、有测试。我习惯给关键 Prompt 写几个固定输入和期望输出的用例,改 Prompt 后跑一遍,避免"改了一处崩了另一处"。

第三,成本要提前算。云端模型按 token 计费,多轮对话历史越长越贵。合理设置记忆窗口、对长文本做摘要压缩、能缓存的别重复请求,这些都是省钱的关键。

后续想继续深入,可以往这几个方向走:接入向量库做 RAG,让模型基于你的私有文档回答;用spring-ai-alibaba对接国内模型生态;把 AI 能力封装成独立的 Agent 服务,通过 gRPC 或 HTTP 给其他系统调用。Spring AI 的抽象层设计得比较干净,这些扩展基本都是在现有基础上加组件,不用推翻重来。

最后分享一个小技巧:本地开发时把模型响应缓存起来,写单元测试时用缓存回放,既快又稳定,还不用每次测试都真调模型烧钱。这个习惯在 CI 环境里尤其重要。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询