Spring AI day1(SSE+AI)

实战步骤

第一步:修复启动报错

打开 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 里:

  1. 点击右上角运行配置(RagDemoApplication 旁边)→ Edit Configurations;

  2. 在 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

你现在按顺序做

  1. 先改 RagDemoApplication,重启成功;

  2. 写 TestSseController,确认 /test/stream 能逐字输出;

  3. 加 Spring AI 依赖,刷新 Maven;

  4. 配环境变量和 application.yml;

  5. 写 ChatController;

  6. 测试两个接口。

关键点学习📖

一、 核心基石: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();
}

这一段是整个第一天的高光时刻,它实现了从"同步"到"响应式"的跨越:

  1. .call() 变成了 .stream():这告诉 Spring AI,不要等全部生成完,而是利用 HTTP 的流式传输特性,每生成一个词(Token),就立刻往回传一点。

  2. 返回类型变成了 Flux<String> :Flux 是 Reactor 框架里的概念,代表"0 到 N 个元素的异步序列"。在这里,你可以把它想象成一条传送带,AI 生成的字块就像包裹一样,一个接一个地放到这条传送带上,源源不断地流向前端。

  3. 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 在连接失败 和流正常结束 两种情况下都会触发,代码里又统一往里塞 [回答结束],于是失败被伪装成了"回答结束"------这就是你看到的现象。

我改了两处

  1. src/main/resources/static/test-stream.html

    • 显式算出后端地址 API_BASE:页面来自 8080 才用同源,否则一律指向 http://localhost:8080(还支持 ?api=http://localhost:8081 覆盖)。
    • 用 received 标志区分「连接失败」和「回答结束」;失败时把实际请求地址、排查步骤打出来,而不是假装答完了。
    • 加了状态提示(连接中/生成中/结束)、Enter 提交、white-space: pre-wrap 保留换行、重复点击先关掉旧连接。
  2. 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: *)。仅用于本地练习,上线要收窄。
相关推荐
斑鸠喳喳1 小时前
线程本地存储 ThreadLocal
java·后端
孙启超1 小时前
【FDE开发指南】第 5 课:中国市场的 FDE
人工智能·ai·职场技能
sunneo1 小时前
每周AI新动态:GPT-6.1与Gemini 4重磅发布
人工智能
和裕1 小时前
定制纸箱刀模费全解析:费用定义与可减免合作场景
大数据·运维·网络·人工智能·算法
归秋1421 小时前
深度解读Work Agent长程任务执行的底层机制
人工智能
IT古董1 小时前
《FDE前沿部署工程师实战教程》32 - Enterprise AI Testing:Agent测试与质量工程
人工智能
一木 之林1 小时前
RAG开发学习总结:从 LangChain 入门到检索增强生成链路的全栈实战-4/6
人工智能·学习·计算机视觉·langchain
CAE虚拟与现实2 小时前
MLP多层感知机(Multilayer Perceptron)
人工智能·机器学习·mlp
Wang's Blog2 小时前
Java框架 SpringCloud 快速入门: 服务拆分案例 Demo
java·开发语言·spring cloud