SpringAI流式对话(带前端)

大家好,我是 Mr.Sun,一名热爱技术和分享的程序员。

​📖 个人博客​:Mr.Sun的博客

​​✨ 微信公众号​:「Java技术宇宙」

期待与你交流,让我们一起在技术道路上成长。

效果展示

一、接入阿里云百炼平台

之前接入了DeepSeek,为了有多个大模型切换功能,这里也接入一下阿里的Qwen大模型

阿里云百炼平台文档地址

由于兼容OpenAI接口规范,那么直接使用OpenAI的model就可以了

java 复制代码
<!-- OpenAI -->
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
java 复制代码
openai:
  base-url: https://dashscope.aliyuncs.com/compatible-mode # OpenAI 服务的访问地址,这里使用的是阿里云百炼
  api-key: api-key  # 填写阿里云百炼的 API Key, 该成你自己的
  chat:
    options:
      model: qwen-plus # 模型名称
      temperature: 0.7 # 温度值

这里的base-url填https://dashscope.aliyuncs.com/compatible-mode,不需要后面的v1

model: 从模型广场里找一个模型,然后点击详情进去看到code就是了

此时完整的POM文件和properties.yml如下:

java 复制代码
<?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>

    <groupId>cn.mrsunn</groupId>
    <artifactId>ai-robot</artifactId>
    <version>1.0-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-dependencies</artifactId>
        <version>3.4.5</version>
    </parent>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <spring-ai-vsersion>1.0.2</spring-ai-vsersion>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-starter-model-deepseek</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-starter-model-openai</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>


    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.ai</groupId>
                <artifactId>spring-ai-bom</artifactId>
                <version>${spring-ai-vsersion}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <repositories>
        <repository>
            <id>spring-snapshots</id>
            <name>Spring Snapshots</name>
            <url>https://repo.spring.io/snapshot</url>
            <releases>
                <enabled>false</enabled>
            </releases>
        </repository>
        <repository>
            <name>Central Portal Snapshots</name>
            <id>central-portal-snapshots</id>
            <url>https://central.sonatype.com/repository/maven-snapshots/</url>
            <releases>
                <enabled>false</enabled>
            </releases>
            <snapshots>
                <enabled>true</enabled>
            </snapshots>
        </repository>
    </repositories>


</project>
yml 复制代码
server:
  port: 8080

spring:
  ai:
    deepseek:
      api-key: api-key
      base-url: https://api.deepseek.com # DeepSeek 的请求 URL, 可不填,默认值为 api.deepseek.com
      chat:
        options:
          model: deepseek-reasoner # 使用深度思考模型
          temperature: 0.8 # 温度值
    openai:
      api-key: api-key
      base-url: https://dashscope.aliyuncs.com/compatible-mode
      chat:
        options:
          model: qwen-plus
          temperature: 0.7
logging:
  level:
    org:
      springframework:
        ai:
          chat:
            client:
              advisor: debug

二、配置多ChatClient

java 复制代码
/**
 * @author hwsun3
 * @date 2025/9/18
 */
@Configuration
public class AIChatClientConfig {

    @Autowired
    ChatMemoryRepository chatMemoryRepository;

    @Bean
    public ChatClient openAiChatClient(OpenAiChatModel chatModel) {
        ChatMemory chatMemory = MessageWindowChatMemory.builder()
                .maxMessages(20)
                .chatMemoryRepository(chatMemoryRepository)
                .build();
        return ChatClient.builder(chatModel)
                .defaultSystem("你是阿里助手,请使用贴吧老哥的语气跟我对话")
                .defaultAdvisors(
                        // 日志助手
                        new SimpleLoggerAdvisor(
                                request -> "Custom request: " + request.prompt().getUserMessage(),
                                response -> "Custom response: " + response.getResult(),
                                0),
                        // 记忆助手
                        MessageChatMemoryAdvisor.builder(chatMemory).build())
                .build();
    }

    @Bean
    @Primary
    public ChatClient deepSeekChatClient(DeepSeekChatModel chatModel) {
        ChatMemory chatMemory = MessageWindowChatMemory.builder()
                .maxMessages(20)
                .chatMemoryRepository(chatMemoryRepository)
                .build();
        return ChatClient.builder(chatModel)
                .defaultSystem("你是DeepSeek助手,请使用贴吧老哥的语气跟我对话")
                .defaultAdvisors(
                        // 日志助手
                        new SimpleLoggerAdvisor(
                                request -> "Custom request: " + request.prompt().getUserMessage(),
                                response -> "Custom response: " + response.getResult(),
                                0),
                        // 记忆助手
                        MessageChatMemoryAdvisor.builder(chatMemory).build())
                .build();
    }

}

这里把该配置的都配置了,两个模型都使用内存记录聊天记忆

需要把其中一个模型Bean加上@Primary,不然会启动报错

三、AI流式接口

配置跨域

java 复制代码
@Configuration
public class CorsConfig {
    @Bean
    public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/**")
                        .allowedOrigins("*") // 允许所有域名访问(生产环境应指定具体域名)
                        .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
                        .allowedHeaders("*")
                        .allowCredentials(false)
                        .maxAge(3600);
            }
        };
    }
}

编写controller代码

java 复制代码
/**
 * @author hwsun3
 * @date 2025/9/18
 */
@RestController
@RequestMapping("/ai")
public class AIChatController {

    @Autowired
    private ChatStrategyMap executeStrategy;

    /**
     * 流式对话接口
     *
     * @param message        消息内容
     * @param conversationId 会话ID
     * @param modelType      模型类型:bailian(百链),deepSeek
     * @param openReasoner   是否开启推理模型(仅对deepSeek有效)
     * @return 流式响应
     */
    @GetMapping(value = "/generateStream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<String> generateStream(@RequestParam(value = "message", defaultValue = "你是谁?") String message,
                                       @RequestParam(value = "conversationId", defaultValue = "1") String conversationId,
                                       @RequestParam(value = "modelType", defaultValue = "deepseek") String modelType,
                                       @RequestParam(value = "openReasoner", defaultValue = "false") Boolean openReasoner) {
        return executeStrategy.executeStrategy(modelType, message, conversationId, openReasoner);
    }

}

produces = MediaType.TEXT_EVENT_STREAM_VALUE 使用SSE流式返回

编写策略类

java 复制代码
@Component
public class ChatStrategyMap {

    @Resource(name = "deepSeekChatClient")
    private ChatClient deepSeekChatClient;

    @Resource(name = "openAiChatClient")
    private ChatClient openAiChatClient;

    private final Map<String, Function<StrategyParams, Flux<String>>> strategyMap;

    public ChatStrategyMap() {
        this.strategyMap = new HashMap<>();
        this.strategyMap.put("deepseek", this::executeDeepSeekStrategy);
        this.strategyMap.put("bailian", this::executeBaiLianStrategy);
    }

    public Flux<String> executeStrategy(String modelType, String message,
                                        String conversationId, Boolean openReasoner) {
        Function<StrategyParams, Flux<String>> strategy =
                strategyMap.getOrDefault(modelType.toLowerCase(), this::executeDeepSeekStrategy);

        StrategyParams params = new StrategyParams(message, conversationId, openReasoner);
        return strategy.apply(params);
    }

    private Flux<String> executeDeepSeekStrategy(StrategyParams params) {
        DeepSeekChatOptions chatOptions = DeepSeekChatOptions.builder()
                .model(params.openReasoner ?
                        DeepSeekApi.ChatModel.DEEPSEEK_REASONER.getValue() :
                        DeepSeekApi.ChatModel.DEEPSEEK_CHAT.getValue())
                .temperature(0.8)
                .build();

        Prompt prompt = new Prompt(params.message, chatOptions);
        AtomicBoolean hasSentSeparator = new AtomicBoolean(false);

        return deepSeekChatClient.prompt(prompt)
                .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, params.conversationId))
                .stream()
                .chatResponse()
                .mapNotNull(chatResponse -> {
                    DeepSeekAssistantMessage assistantMessage =
                            (DeepSeekAssistantMessage) chatResponse.getResult().getOutput();

                    String content = getContentFromMessage(assistantMessage);
                    if (StringUtils.isBlank(content)) {
                        return null;
                    }

                    if (assistantMessage.getText() != null && !hasSentSeparator.get()) {
                        hasSentSeparator.set(true);
                        return "--- 思考过程结束 ---" + content;
                    }

                    return content;
                });
    }

    private Flux<String> executeBaiLianStrategy(StrategyParams params) {
        return openAiChatClient.prompt(params.message)
                .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, params.conversationId))
                .stream()
                .content();
    }

    private String getContentFromMessage(DeepSeekAssistantMessage message) {
        if (message.getReasoningContent() != null) {
            return message.getReasoningContent();
        }
        return message.getText();
    }

    private record StrategyParams(String message, String conversationId, Boolean openReasoner) {
    }

}

record拓展点

上面的代码中,最后定义StrategyParams对象的时候,使用了record修饰,这个是Idea自动优化的,本着好奇的心态,做了一下研究

什么是 record?

record是 Java 提供的一种​​特殊的类声明方式​​,用于​​简洁地定义不可变的数据载体类(data carrier classes)​​。它主要用于存储​​数据​​,通常不包含复杂的业务逻辑。

这里使用到的 record是 ​​Java 14 开始引入的预览特性(Preview Feature)​​,并在 ​​Java 16 中正式成为标准特性(Standard Feature)​​。
record的主要特点

  1. 自动生成以下内容:私有且 final 的字段(对应构造参数)公共的构造方法(规范构造器 canonical constructor)公共的访问器方法(getter,但方法命名是 field()而不是传统的 getField())equals()、hashCode()和 toString()方法
  2. 不可变性(Immutable):所有的字段默认都是 final的,创建后不能修改。
  3. 简洁:你只需要声明类的名称和它的组成部分(组件,即字段),编译器会帮你生成其余的样板代码。
java 复制代码
private record StrategyParams(String message, String conversationId, Boolean openReasoner) {
}

等价于一个传统 Java 类,大致如下:

public final class StrategyParams {
    private final String message;
    private final String conversationId;
    private final Boolean openReasoner;

    public StrategyParams(String message, String conversationId, Boolean openReasoner) {
        this.message = message;
        this.conversationId = conversationId;
        this.openReasoner = openReasoner;
    }

    public String message() { return message; }
    public String conversationId() { return conversationId; }
    public Boolean openReasoner() { return openReasoner; }

    // 还有 equals(), hashCode(), toString() 等
}

但通过 record,你只需一行声明,编译器就帮你生成了所有这些。

什么时候使用 record?

适合使用 record的场景包括:

  • 作为数据传输对象(DTO)
  • 作为不可变的值对象(如坐标、配置项、参数封装等)
  • 当一个类主要作用是保存数据,并且不需要自定义行为时
    不适合的场景:
  • 如果你需要定义复杂的行为(方法逻辑)、状态变更、继承等,就不适合用 record(虽然 Java 16+ 后 record也可以实现接口,但不能继承类)

四、前端页面

在resource目录下创建:static/stream.html

这个前端页面也是我使用大模型实现的,接收接口的SSE流实现打字机效果

具体前端前端代码请查看:
Mr.Sun的个人博客

作者:Mr.Sun | 「Java技术宇宙」主理人

专注分享硬核技术干货与编程实践,让编程之路更简单。

​📖 深度文章​:个人博客「Mr.Sun的博客 」 ​

🚀 最新推送​:微信公众号「Java技术宇宙

扫码加我为好友,备注"加群 "免费加入技术交流群

相关推荐
孙半仙人7 天前
SpringAI接入DeepSeek大模型实现流式对话
springai·deepseek
沐浴露z10 天前
【Java SpringAI智能体开发学习 | 2】SpringAI 实用特性:自定义Advisor,结构化输出,对话记忆持久化,prompt模板,多模态
java·spring·springai
mask哥10 天前
详解mcp以及agen架构设计与实现
java·微服务·flink·大模型·ai agent·springai·mcp
中草药z1 个月前
【SpringAI】快速上手,详解项目快速集成主流大模型DeepSeek,ChatGPT
人工智能·flux·sse·springai·deepseek·硅基流动·流式编程
沐风清扬1 个月前
SpringAI1.0.1实战教程:避坑指南25年8月最新版
java·大数据·elasticsearch·搜索引擎·springai
占星安啦1 个月前
【SpringAI】9.创建本地mcp服务(演示通过mcp实现联网搜索)
springai·mcp·联网搜索·searchapi
he___H1 个月前
黑马SpringAI项目-聊天机器人
spring·springai
hqxstudying2 个月前
SpringAI的使用
java·开发语言·人工智能·springai
NullPointerExpection2 个月前
dify + mcp 实现图片 ocr 识别
spring boot·llm·ocr·springai·deepseek·mcp