起:作为一个写了十年CRUD的Java老鸟,我从来没想过,让机器写诗会比写一个查询接口还简单。
1. 选型小帖士:Spring AI vs Spring AI Alibaba
开始写代码之前,先解决一个让我纠结了好久的问题。
Maven仓库里搜"spring-ai",会出来两个看起来差不多的东西------Spring AI 和 spring-ai-alibaba。该用哪个?
后来搞明白了,它们的关系很像 JDBC 和 MySQL 驱动:
- Spring AI 是标准接口层。它定义了一套统一的API------ChatClient、Function Calling、MCP、RAG------不管底层用的是OpenAI、通义千问还是Ollama本地模型,写的代码都一样。
- Spring AI Alibaba 是阿里云基于这套标准的增强实现,深度集成了阿里云百炼平台,额外提供了Graph工作流引擎等独有功能。
其实选型很简单:
| 你的情况 | 推荐选择 |
|---|---|
| 刚开始学,想跑通第一段对话 | Spring AI |
| 公司已经在用阿里云百炼 | Spring AI Alibaba |
| 想折腾本地模型、切换厂商 | Spring AI |
本系列选型:Spring AI + 通义千问(通过OpenAI兼容接口接入)
一句话:学开车,先用标准驾照,别一上来就绑定某个品牌的自动驾驶系统。
2. 环境要求
2.1. 版本要求
- Java版本
-
- 最低:Java17
- 推荐:Java17/Java21
- Spring Boot版本
-
- 最低:Spring Boot 3.2.x
- 推荐:3.2.x/3.3.x
底层依赖 Spring Framework 6,Spring Framework6 强制要求 JDK17
2.2. 获取API Key
本系列使用通义千问(通过OpenAI兼容接口),因为国内访问稳定,注册就有免费额度。
- 打开阿里云百炼平台
- 注册/登录后,开通模型服务
- 在"API Key管理"中创建一个Key
2.3. 创建项目
创建一个spring-boot工程,这个不需要教了吧!
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.yunxi</groupId>
<artifactId>yunxi-spring-ai</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>pom</packaging>
<name>yunxi-spring-ai</name>
<description>yunxi-spring-ai</description>
<modules>
<module>chapter-01-hello-ai</module>
</modules>
<properties>
<java.version>17</java.version>
<spring-ai.version>1.0.0</spring-ai.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
特别注意的是:springAI发展很快,有可能你在某些博客复制的是旧版本。
spring-ai-starter-openai 是旧名字(废弃)
spring-ai-openai-spring-boot-starter(历史旧 artifact,别名)⚠️ 1.0 正式版本官方改名,新项目不要再复制网上老 demo 这个坐标!
spring-ai-starter-model-openai 是 Spring AI 1.0 GA+ 正式新标准名称(现在必须用这个)
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.yunxi</groupId>
<artifactId>yunxi-spring-ai</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>chapter-01-hello-ai</artifactId>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
yaml
spring:
ai:
openai:
api-key: ${AI_API_KEY} # 从环境变量读取
base-url: https://dashscope.aliyuncs.com/compatible-mode
chat:
options:
model: qwen3.7-max
server:
port: 8080
注意:api-key 通过 ${AI_API_KEY} 从环境变量读取,不要把Key直接写进配置文件提交到Git!
启动时设置:export AI_API_KEY=你的Key
3. 第一行AI代码
3.1. 最简写法
java
package com.oldbird.ai.chapter01;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
public class Chapter01Application {
public static void main(String[] args) {
SpringApplication.run(Chapter01Application.class, args);
}
@Bean
CommandLineRunner runner(ChatClient.Builder builder) {
return args -> {
ChatClient chatClient = builder.build();
String response = chatClient.prompt()
.user("请用Java程序员的风格写一首五言绝句,主题是'代码之美'")
.call()
.content();
System.out.println(response);
};
}
}
3.2. 运行结果
启动项目,控制台输出:
txt
```java
/**
* @ClassName: CodeBeauty
* @Description: 咏代码之美
* @Author: JVM_Poet
* @Date: 2023-10-24
*/
public class CodeBeauty {
public static void main(String[] args) {
System.out.println("类聚千般法,");
System.out.println("封装百脉通。");
System.out.println("空指休生恨,");
System.out.println("回收自随风。");
}
}
```
### 📝 Code Review (诗句解析):
* **类聚千般法**(起)
**Java映射**:`Class`(类)与 `Method`(方法)。
**意境**:Java世界"万物皆对象",一个优雅的 `Class` 聚合了千变万化的方法,构建出庞大而有序的业务逻辑,此乃**结构之美**。
* **封装百脉通**(承)
**Java映射**:`Encapsulation`(封装)。
**意境**:面向对象三大特性之首。良好的封装与接口设计,让代码高内聚、低耦合,数据流转如人体百脉贯通,此乃**架构之美**。
* **空指休生恨**(转)
**Java映射**:`NullPointerException`(空指针异常)。
**意境**:NPE 是 Java 程序员最熟悉的"老朋友"。遇到报错不必砸键盘生恨,从容地加上 `Optional` 或判空逻辑,修码亦是修心,此乃**豁达之美**。
* **回收自随风**(合)
**Java映射**:`Garbage Collection`(垃圾回收机制)。
**意境**:JVM 在后台默默运行 GC,自动清理失去引用的对象,释放内存。无需像 C/C++ 那样手动 `free`,无用之物如落叶随风而逝,此乃**机制之美**。
*(注:本诗押平水韵"一东"韵,平仄合规,编译通过,无 Warning,可直接 Run。)*
(每次运行结果会略有不同,毕竟这是生成式AI,不是打印固定字符串。)
4. 加一点工程化:分离Controller和Service
上面的代码全塞在启动类里,太不专业。拆一下:
java
package com.oldbird.ai.chapter01.service;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
@Service
public class ChatService {
private final ChatClient chatClient;
public ChatService(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
public String chat(String message) {
return chatClient.prompt()
.user(message)
.call()
.content();
}
}
java
package com.oldbird.ai.chapter01.controller;
import com.oldbird.ai.chapter01.service.ChatService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ChatController {
private final ChatService chatService;
public ChatController(ChatService chatService) {
this.chatService = chatService;
}
@GetMapping("/chat")
public String chat(@RequestParam(defaultValue = "你好") String message) {
return chatService.chat(message);
}
}
启动后访问:http://localhost:8080/chat?message=用程序员的风格写一首关于加班的诗
go
```java /** * @Title: Overtime.java * @Description: 谨以此诗,献给每一个在深夜与Bug搏斗的开发者 * @Author: 一个还没下班的程序员 * @Date: 202X-XX-XX 03:00:00 AM */ ``` 夜幕是一次静默的 `git commit`, 推送到名为"凌晨"的远程分支。 屏幕的冷光,是唯一的 `stdout`, 输出着我无处安放的 `anxiety`。 键盘敲击着 `while(true)` 的咒语, 试图在逻辑的死锁中寻找 `break` 的奇迹。 外卖盒堆叠成未处理的 `Exception`, 而我的发际线,正面临 `StackOverflow` 的危机。 产品的需求是异步的 `Promise`, 永远在 `pending`,没有 `resolve` 的归期。 我 `try-catch` 了所有的 `NullPointer`, 却 `catch` 不到窗外那抹黎明的晨曦。 当东方泛起 `#FFFFFF` 的色值, 终端终于返回 `0 Errors` 的叹息。 我敲下 `git push --force`, 把疲惫强制覆盖进服务器的 `history`。 现在,调用 `Thread.sleep()` 进入休眠, 释放掉这具躯壳所有的 `dependency`。 直到明早,被 `CronJob` 再次触发, 开启下一个,无限递归的周期。 ```bash > Process finished with exit code 0 > System entering sleep mode... ```
5. 代码解读:ChatClient到底干了什么?
后端老鸟看到chatClient.prompt().user().call().content()这条链式调用,内心可能会问:底层发生了什么?
本质上,它替我们做了之前需要手写的事情:
java
// 没有Spring AI时,你需要这样写:
HttpRequest request = HttpRequest.newBuilder()
.uri("https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions")
.header("Authorization", "Bearer " + apiKey)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString("""
{
"model": "qwen-plus",
"messages": [
{"role": "user", "content": "写一首诗"}
]
}
"""))
.build();
HttpResponse<String> response = httpClient.send(request, BodyHandlers.ofString());
// 然后手动解析JSON,处理异常,处理流式...
Spring AI帮我们把这些脏活全封装了。ChatClient 就是大模型的 RestTemplate,换一个模型厂商就跟换一个数据库连接一样------改配置,不改代码。
6. 本篇避坑指南
6.1. 坑1:base-url 路径写错
通义千问的兼容接口路径是 /compatible-mode,不是/compatible-mode/v1 多写这个后缀会报404。
6.2. 坑2:API Key直接写进yml
用 ${AI_API_KEY} 读环境变量,然后 application.yml 加入 .gitignore 或使用环境变量覆盖。提交到GitHub会被别人扫走。
6.3. 坑3:模型名称不对
qwen-plus、qwen-turbo、qwen-max 是不同的模型,性能差异大。
7. 本篇小结
这第一篇,我们完成了三件事:
- 搞清楚了 Spring AI 和 spring-ai-alibaba 的关系
- 用不到50行代码让AI写了一首诗
- 完成了第一个工程化的项目结构
下一篇,我们要解决一个让程序员浑身难受的问题------Prompt硬编码在代码里,太不优雅了。我要把它放到配置文件里。
本文与DeepSeek协作完成