实战步骤
第一步:修复启动报错
打开 RagDemoApplication.java,把类上面的注解改成这样:
java
package com.example.ragdemo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration;
@SpringBootApplication(exclude = {
DataSourceAutoConfiguration.class,
DataSourceTransactionManagerAutoConfiguration.class
})
public class RagDemoApplication {
public static void main(String[] args) {
SpringApplication.run(RagDemoApplication.class, args);
}
}
保存后重启。如果看到 Started RagDemoApplication in x.x seconds,说明启动成功。
第二步:写一个 SSE 测试接口
在 src/main/java/com/example/ragdemo/controller 下新建 TestSseController.java:
java
package com.example.ragdemo.controller;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import java.time.Duration;
import java.util.List;
@RestController
public class TestSseController {
@GetMapping(value = "/test/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> testStream() {
List<String> words = List.of("你好", ",", "我", "是", "AI", "助手", "。");
return Flux.fromIterable(words)
.delayElements(Duration.ofMillis(500));
}
}
重启,浏览器访问:
text
http://localhost:8080/test/stream
你会看到文字一个一个蹦出来。这就是流式输出。
这一步的目的:先不碰 AI,确认你的 WebFlux + SSE 链路是通的。
第三步:加 Spring AI 依赖
打开 pom.xml,在 <dependencyManagement> 里加 BOM:
xml
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>2.0.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
在 <dependencies> 里加 OpenAI Starter:
xml
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
点 IDEA 右上角 Maven 刷新,等依赖下载完。
第四步:配置 API Key
先设置环境变量。在 IDEA 里:
-
点击右上角运行配置(
RagDemoApplication旁边)→Edit Configurations; -
在
Environment variables里加一行:
text
OPENAI_API_KEY=你的真实key
如果你用的是兼容 OpenAI 的代理服务,再加一行
OPENAI_BASE_URL=你的服务地址。
然后在 application.yml 写:
yaml
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY}
base-url: ${OPENAI_BASE_URL:https://api.openai.com}
chat:
options:
model: gpt-4o-mini
第五步:写 AI 对话接口
新建 ChatController.java:
java
package com.example.ragdemo.controller;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
@RestController
@RequestMapping("/api/chat")
public class ChatController {
private final ChatClient chatClient;
public ChatController(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
@GetMapping("/simple")
public String simple(@RequestParam String message) {
return chatClient.prompt()
.user(message)
.call()
.content();
}
@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> stream(@RequestParam String message) {
return chatClient.prompt()
.user(message)
.stream()
.content();
}
}
第六步:测试
重启项目,然后:
普通对话:
text
http://localhost:8080/api/chat/simple?message=你好
流式对话:
text
http://localhost:8080/api/chat/stream?message=用一句话解释RAG
你现在按顺序做
-
先改
RagDemoApplication,重启成功; -
写
TestSseController,确认/test/stream能逐字输出; -
加 Spring AI 依赖,刷新 Maven;
-
配环境变量和
application.yml; -
写
ChatController; -
测试两个接口。
关键点学习📖
一、 核心基石:ChatClient 的注入(计划 2.4 开头)
java
private final ChatClient chatClient;
public ChatController(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
-
为什么是
Builder? 正如你计划里写的,ChatClient是对底层ChatModel的封装。Spring AI 的设计非常巧妙,它没有直接给你一个写死配置的ChatClient,而是给了你一个ChatClient.Builder。 -
背后的自动装配 :因为我们引入了
spring-ai-starter-model-openai依赖,Spring Boot 启动时,自动配置类会读取你的application.yml(API Key、Base URL、模型名),帮你把最底层的OpenAiChatModel创建好,然后包装成一个ChatClient.Builder放在 Spring 容器里。 -
你要做的 :通过构造器注入拿到这个
Builder,调用.build()生成你专属的ChatClient。这样,后续所有对话都在复用这个已经配置好密钥的客户端。
二、 普通对话:/simple(计划 2.4)
java
@GetMapping("/simple")
public String simple(@RequestParam String message) {
return chatClient.prompt()
.user(message)
.call()
.content();
}
-
链式调用(Fluent API) :这段代码读起来像一句英文。
prompt()开启一次对话构建,user(message)填入用户的问题。 -
同步阻塞 :计划里写得很清楚,
.call()发起的是同步调用。此时,Tomcat 的这条线程会一直挂起等待,直到 OpenAI 的服务器把整段话全部生成完毕,返回一个完整的 JSON。 -
最终返回 :
.content()从响应对象里把 AI 回复的纯文本字符串抠出来,交还给前端。 -
适用场景:适合对响应时间不敏感,或者后端需要对完整结果做后续处理的场景(比如存入数据库、提取关键词等)。
三、 流式输出:/stream(计划 2.5)
java
@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> stream(@RequestParam String message) {
return chatClient.prompt()
.user(message)
.stream() // 关键变化
.content();
}
这一段是整个第一天的高光时刻,它实现了从"同步"到"响应式"的跨越:
-
.call()变成了.stream():这告诉 Spring AI,不要等全部生成完,而是利用 HTTP 的流式传输特性,每生成一个词(Token),就立刻往回传一点。 -
返回类型变成了
Flux<String>:Flux是 Reactor 框架里的概念,代表"0 到 N 个元素的异步序列"。在这里,你可以把它想象成一条传送带,AI 生成的字块就像包裹一样,一个接一个地放到这条传送带上,源源不断地流向前端。 -
produces = MediaType.TEXT_EVENT_STREAM_VALUE:这是最关键的一行。它告诉 Spring Boot:"别用普通的application/json格式返回了,请使用 SSE(Server-Sent Events) 协议格式返回"。SSE 协议规定,每个数据块前面要加上data:,并以\n\n结尾。Spring WebFlux 会自动帮你做好这个封装。
在前端测试上的BUG
查清了。不是模型或接口的问题,是页面发出去的请求根本没打到 8080 上的 Spring Boot ,而 [回答结束] 是 onerror 打印的,看起来像"答完了",其实是"一个字节都没收到"。
证据(直接打你正在跑的那个实例)
复制
GET /api/chat/simple?message=hi → 200 "Hi! How's it going? ..." (模型和 key 都正常)
GET /api/chat/stream?message=用一句话解释一下RAG
→ 200 text/event-stream
data:R
data:AG
data:(
data:检索
...
后端流式输出一点问题没有。所以问题在前端。
为什么只剩一句 [回答结束]
原代码里写的是相对路径:
js
复制
new EventSource('/api/chat/stream?message=' + ...)
相对路径"打到哪台服务器"取决于页面是从哪打开的:
| 打开方式 | /api/chat/stream 实际去向 |
结果 |
|---|---|---|
| http://localhost:8080/test-stream.html | 8080(后端) | 正常 |
双击 html 文件(file://) |
file:///api/chat/stream |
立刻失败 |
IDEA 里右键 "Open in Browser"(localhost:63342) |
IDEA 的静态服务器 | 404,失败 |
后两种都是"页面能正常显示、点提问却什么都没出来"。再加上 onerror 在连接失败 和流正常结束 两种情况下都会触发,代码里又统一往里塞 [回答结束],于是失败被伪装成了"回答结束"------这就是你看到的现象。
我改了两处
-
src/main/resources/static/test-stream.html- 显式算出后端地址
API_BASE:页面来自 8080 才用同源,否则一律指向 http://localhost:8080(还支持?api=http://localhost:8081覆盖)。 - 用
received标志区分「连接失败」和「回答结束」;失败时把实际请求地址、排查步骤打出来,而不是假装答完了。 - 加了状态提示(连接中/生成中/结束)、Enter 提交、
white-space: pre-wrap保留换行、重复点击先关掉旧连接。
- 显式算出后端地址
-
src/main/java/com/example/ragdemo/controller/ChatController.java- 加了
@CrossOrigin(origins = "*"),让file://和 IDEA 63342 这种跨域打开方式也能调通(已实测Origin: null和Origin: http://localhost:63342都返回 200 +Access-Control-Allow-Origin: *)。仅用于本地练习,上线要收窄。
- 加了