最新SpringAI 1.0.0正式版-实现流式对话应用

阿里百炼平台整合QWEN与SpringAI

本文属于我的AI应用学习笔记的一部分,之前由于实习公司的技术栈,使用的是JDK8+LangChain4j,现在开始学习使用SpringAI,这也是我的SpringAI学习笔记的第一部分。

注意:使用SpringAI进行开发需要JDK17+,这里使用的是JDK21。

阿里的QWEN3是一个性价比比较高的大模型,非常适合用来练手,本文将会教会大家去使用百炼平台整合简单的大模型应用到您的应用程序中,使用langchain4j。本人也是一个初学者,难免会有一些问题和错误,还请大家指正。

项目地址:ai-chat-demo

阅读本文前应该有的知识基础

  • Java基础

  • Java Web基础

  • 数据库基础

  • 包管理 (Maven、gradle等)

  • Spring基础、SSM整合、SpringBoot等


文本流式对话输出

文本流式对话是一个非常常见的大模型应用,可以让用户的阅读和ai输出同时进行,特别是当输出内容很长时,可以极大优化用户体验。

使用SpringAI,可以十分方便地调用你的API去进行流式对话。

前置准备
  1. 登录你的百炼控制台; 如果没有注册,请先注册新账号,有免费额度可以用。

  2. 获取API KEY ; 官方文档建议使用系统变量,按照官方文档教程即可,但是如果你需要对使用的大模型进行配置,则需要建一个配置表,通过数据库传入大模型配置参数,这个后面会详细介绍。官方文档:百炼控制台

开始测试
  • 进入你的SpringBoot项目,新建一个测试。

  • 引入相关maven依赖,如果需要其它版本,请到Maven仓库中搜索,

xml 复制代码
<!--SpringAI-->
<!-- https://mvnrepository.com/artifact/org.springframework.ai/spring-ai-client-chat -->
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-client-chat</artifactId>
    <version>1.0.0</version>
</dependency>

<!--SpringAI OpenAI组件-->
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-openai</artifactId>
    <version>1.0.0</version>
</dependency>
  • 开始编写测试,正常配置的话应该会在控制台分段输出ai回复
java 复制代码
import io.micrometer.observation.ObservationRegistry;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.metadata.Usage;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.model.tool.ToolCallingManager;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.retry.support.RetryTemplate;
import reactor.core.publisher.Flux;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

@Slf4j
public class SpringAITests {

    @Test
    public void springAIStreamChat() throws InterruptedException {
        // 计时器
        CountDownLatch countDownLatch = new CountDownLatch(1);

        // 为对话提供高级指令。例如,您可以使用系统消息指示生成器像某个角色一样行事,或以特定格式提供答案。
        Message sysMessage = // 添加系统消息
                new SystemMessage("你是一个负责任的、礼貌的AI助手,你使用的模型是Claude-4.0.");

        // 用户输入的问题文本,它们代表问题、提示或您希望生成器响应的任何输入。
        Message userMessage =
                new UserMessage("你是Claude-4.0吗?能做什么?之前我们在干什么?");

        // 提供有关对话中先前交流的背景信息
        Message assistantMessage =
                new AssistantMessage("我们正在交流关于SpringAI技术的相关问题");

        OpenAiApi openAiApi = OpenAiApi.builder()
                // 填入自己的API KEY
                .apiKey("sk-...")
                // 填入自己的API域名,如果是百炼,即为https://dashscope.aliyuncs.com/compatible-mode
                // 注意:这里与langchain4j的配置不同,不需要在后面加/v1
                .baseUrl("https://dashscope.aliyuncs.com/compatible-mode")
                .build();

        // 模型选项
        OpenAiChatOptions chatOptions = OpenAiChatOptions.builder()
                // 模型生成的最大 tokens 数
                .maxTokens(2048)
                // 模型生成的 tokens 的概率质量范围,取值范围 0.0-1.0 越大的概率质量范围越大
                .topP(0.9)
                // 模型生成的 tokens 的随机度,取值范围 0.0-1.0 越大的随机度越大
                .temperature(0.9)
                // 模型名称
                .model("qwen-turbo-latest")
                // 打开流式对话token计数配置,默认为false
                .streamUsage(true)
                .build();

        // 工具调用管理器 暂时为空
        ToolCallingManager toolCallingManager = ToolCallingManager.builder().build();

        // 重试机制,设置最多3次
        RetryTemplate retryTemplate = RetryTemplate.builder()
                .maxAttempts(3)
                .build();

        // 观测数据收集器
        ObservationRegistry observationRegistry = ObservationRegistry.NOOP;

        ChatModel model = new OpenAiChatModel(openAiApi,
                chatOptions,
                toolCallingManager,
                retryTemplate,
                observationRegistry);

        // 提示词
        Prompt prompt = Prompt.builder()
                .chatOptions(chatOptions)
                // 输入的问题,messages和content设置只能有其中一个,建议在输入问题时使用messages
//                .content("你是谁?能做什么?之前我们在干什么?")
                // 添加系统、用户、背景信息
                .messages(sysMessage, userMessage, assistantMessage)
                .build();

        // 反应式对话流
        Flux<ChatResponse> responseFlux = model.stream(prompt);
        // 用于跟踪最后一个 ChatResponse
        AtomicReference<ChatResponse> lastResponse = new AtomicReference<>();

        // 订阅 Flux 实现流式输出(控制台输出或 SSE 推送)
        responseFlux.subscribe(
                token -> {
                    // 获取当前输出内容片段
                    if(token.getResult()!=null){
                        log.info("输出内容:{}", token.getResult().getOutput().getText());
                    }
                    // 更新最后一个响应
                    lastResponse.set(token);
                },
                error -> {
                    log.error("出错:", error);
                    // 错误,停止倒计时
                    countDownLatch.countDown();
                }, // 错误处理
                () -> {// 流结束
                    log.info("\n回答完毕!");
                    // 从最后一个响应中获取 Token 使用信息
                    ChatResponse chatResponse = lastResponse.get();
                    if (chatResponse != null) {
                        Usage usage = chatResponse.getMetadata().getUsage();
                        log.info("===== Token 使用统计 =====");
                        log.info("输入 Token 数(Prompt Tokens): {}", usage.getPromptTokens());
                        log.info("输出 Token 数(Completion Tokens): {}", usage.getCompletionTokens());
                        log.info("总 Token 数(Total Tokens): {}", usage.getTotalTokens());
                    } else {
                        log.warn("未获取到 Token 使用信息,可能模型未返回或配置未启用");
                    }
                    countDownLatch.countDown();
                });

        // 阻塞主线程最多60s 等待结果
        countDownLatch.await(60, TimeUnit.SECONDS);
    }

}
测试结果
bash 复制代码
17:22:50.079 [ForkJoinPool.commonPool-worker-1] INFO com.losgai.ai.SpringAITests -- 输出内容:
17:22:50.085 [ForkJoinPool.commonPool-worker-1] INFO com.losgai.ai.SpringAITests -- 输出内容:我是
17:22:50.086 [ForkJoinPool.commonPool-worker-1] INFO com.losgai.ai.SpringAITests -- 输出内容:Cla
17:22:50.087 [ForkJoinPool.commonPool-worker-1] INFO com.losgai.ai.SpringAITests -- 输出内容:ude
17:22:50.088 [ForkJoinPool.commonPool-worker-1] INFO com.losgai.ai.SpringAITests -- 输出内容:-
17:22:50.219 [HttpClient-2-Worker-0] INFO com.losgai.ai.SpringAITests -- 输出内容:4.0,
17:22:50.273 [HttpClient-2-Worker-0] INFO com.losgai.ai.SpringAITests -- 输出内容:这是我的模型版本
17:22:50.360 [HttpClient-2-Worker-0] INFO com.losgai.ai.SpringAITests -- 输出内容:。我能够帮助
17:22:50.509 [HttpClient-2-Worker-0] INFO com.losgai.ai.SpringAITests -- 输出内容:您解答各种问题
17:22:50.569 [HttpClient-2-Worker-0] INFO com.losgai.ai.SpringAITests -- 输出内容:,进行深度对话
17:22:50.653 [HttpClient-2-Worker-0] INFO com.losgai.ai.SpringAITests -- 输出内容:,提供信息和
17:22:50.703 [HttpClient-2-Worker-0] INFO com.losgai.ai.SpringAITests -- 输出内容:建议。

关于Spring
17:22:50.854 [HttpClient-2-Worker-0] INFO com.losgai.ai.SpringAITests -- 输出内容:AI技术的讨论
17:22:50.876 [HttpClient-2-Worker-0] INFO com.losgai.ai.SpringAITests -- 输出内容:,我们之前的交流
17:22:50.999 [HttpClient-2-Worker-0] INFO com.losgai.ai.SpringAITests -- 输出内容:可能涉及了Spring
17:22:51.051 [HttpClient-2-Worker-0] INFO com.losgai.ai.SpringAITests -- 输出内容:框架与人工智能技术
17:22:51.204 [HttpClient-2-Worker-0] INFO com.losgai.ai.SpringAITests -- 输出内容:的结合,比如
17:22:51.224 [HttpClient-2-Worker-0] INFO com.losgai.ai.SpringAITests -- 输出内容:如何在Spring应用
17:22:51.343 [HttpClient-2-Worker-0] INFO com.losgai.ai.SpringAITests -- 输出内容:中集成AI功能
17:22:51.397 [HttpClient-2-Worker-0] INFO com.losgai.ai.SpringAITests -- 输出内容:,使用Spring AI
17:22:51.487 [HttpClient-2-Worker-0] INFO com.losgai.ai.SpringAITests -- 输出内容:库进行机器学习
17:22:51.667 [HttpClient-2-Worker-0] INFO com.losgai.ai.SpringAITests -- 输出内容:模型的部署,
17:22:51.670 [HttpClient-2-Worker-0] INFO com.losgai.ai.SpringAITests -- 输出内容:或者探讨Spring Boot
17:22:51.714 [HttpClient-2-Worker-0] INFO com.losgai.ai.SpringAITests -- 输出内容:与AI服务的
17:22:51.821 [HttpClient-2-Worker-0] INFO com.losgai.ai.SpringAITests -- 输出内容:整合等话题。

17:22:51.909 [HttpClient-2-Worker-0] INFO com.losgai.ai.SpringAITests -- 输出内容:如果您想继续深入
17:22:52.105 [HttpClient-2-Worker-0] INFO com.losgai.ai.SpringAITests -- 输出内容:讨论SpringAI技术
17:22:52.124 [HttpClient-2-Worker-0] INFO com.losgai.ai.SpringAITests -- 输出内容:,或者有其他
17:22:52.193 [HttpClient-2-Worker-0] INFO com.losgai.ai.SpringAITests -- 输出内容:任何问题,请随时
17:22:52.278 [HttpClient-2-Worker-0] INFO com.losgai.ai.SpringAITests -- 输出内容:告诉我!
17:22:52.283 [HttpClient-2-Worker-0] INFO com.losgai.ai.SpringAITests -- 输出内容:
17:22:52.286 [HttpClient-2-Worker-0] INFO com.losgai.ai.SpringAITests -- 
回答完毕!
17:23:33.982 [HttpClient-2-Worker-0] INFO com.losgai.ai.SpringAITests -- ===== Token 使用统计 =====
17:23:33.982 [HttpClient-2-Worker-0] INFO com.losgai.ai.SpringAITests -- 输入 Token 数(Prompt Tokens): 63
17:23:33.982 [HttpClient-2-Worker-0] INFO com.losgai.ai.SpringAITests -- 输出 Token 数(Completion Tokens): 102
17:23:33.982 [HttpClient-2-Worker-0] INFO com.losgai.ai.SpringAITests -- 总 Token 数(Total Tokens): 165
Disconnected from the target VM, address: '127.0.0.1:59988', transport: 'socket'

Process finished with exit code 0

🎉恭喜你!到这里你就已经掌握了使用SpringAI进行流式对话的一般配置和方法了。

改造为Web应用

参考我之前使用LangChain4j实现流式对话的方案,到了JDK17+,我们使用反应式流来进行流式对话,大大提升了性能和效率。

之前的LangChain4j流式封装实现参考:SpringBoot整合AI应用-流式对话 (使用langchain4j)

将刚才的测试改造为Web应用有多种方案,我目前的做法是将AI模型的配置项全部储存到数据库里,按需选择和读取,使用起来十分灵活。

这里只贴出部分核心代码,如果需要完整代码,请看我的代码仓库:ai-chat-demo

Service层封装

接口

java 复制代码
package com.losgai.ai.service;

import com.losgai.ai.dto.AiChatParamDTO;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import java.util.concurrent.CompletableFuture;

public interface AiChatService {
    
    /**
     * 反应流处理流式对话
     * */
    CompletableFuture<Boolean> sendQuestionAsync(AiChatParamDTO aiChatParamDTO, String sessionId);
    

    /** 获取流式返回结果*/
    SseEmitter getEmitter(String sessionId);
}

服务实现

java 复制代码
import cn.hutool.core.util.StrUtil;
import com.losgai.ai.dto.AiChatParamDTO;
import com.losgai.ai.entity.AiConfig;
import com.losgai.ai.entity.AiMessagePair;
import com.losgai.ai.enums.AiMessageStatusEnum;
import com.losgai.ai.global.SseEmitterManager;
import com.losgai.ai.mapper.AiConfigMapper;
import com.losgai.ai.mapper.AiMessagePairMapper;
import com.losgai.ai.service.AiChatService;
import com.losgai.ai.service.AiChatMessageService;
import com.losgai.ai.util.OpenAiModelBuilderSpringAi;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.metadata.Usage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import reactor.core.publisher.Flux;

import java.io.IOException;
import java.time.Instant;
import java.util.Date;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

@Service
@RequiredArgsConstructor
@Slf4j
public class AiChatServiceImpl implements AiChatService {

    private final AiMessagePairMapper aiMessagePairMapper;

    private final SseEmitterManager emitterManager;

    private final AiChatMessageService aiChatMessageService;

    private final AiConfigMapper aiConfigMapper;

    /**
     * @param aiChatParamDTO 对话参数
     * @param sessionId      SSE会话ID
     * @return 是否处理成功
     * @apiNote AI对话请求,基于虚拟线程实现异步处理,SpringAI实现
     */
    @Override
    public CompletableFuture<Boolean> sendQuestionAsync(AiChatParamDTO aiChatParamDTO, String sessionId) {
        return CompletableFuture.supplyAsync(() -> {
            if (emitterManager.isOverLoad())
                return false;
            // 获取会话id对应的sseEmitter
            SseEmitter emitter = emitterManager.getEmitter(sessionId);
            // 先发送一次队列人数通知
            emitterManager.notifyThreadCount();
            // 没有则先创建一个sseEmitter
            if (emitter == null) {
                if (emitterManager.addEmitter(sessionId, new SseEmitter(0L))) {
                    emitter = emitterManager.getEmitter(sessionId);
                } else {
                    // 创建失败,一般是由于队列已满,直接返回false
                    return false;
                }
            }
            // 最终指向的emitter对象
            SseEmitter finalEmitter = emitter;
            StringBuffer sb = new StringBuffer();
            // 开始对话,返回token流
            // 封装插入的信息对象
            AiMessagePair aiMessagePair = new AiMessagePair();
            aiMessagePair.setSseSessionId(sessionId);
            aiMessagePair.setSessionId(aiChatParamDTO.getChatSessionId());

            // 标志位,判断是否更新成功,防止重复插入
            AiConfig aiConfig = aiConfigMapper.selectByPrimaryKey(aiChatParamDTO.getModelId());
            Flux<ChatResponse> chatResponseFlux = OpenAiModelBuilderSpringAi.buildModel(aiConfig,
                    "你是一个礼貌的AI助手",
                    aiChatParamDTO.getQuestion(),
                    "");
            // 用于跟踪最后一个 ChatResponse
            AtomicReference<ChatResponse> lastResponse = new AtomicReference<>();
            chatResponseFlux.subscribe(
                    token -> {
                        // 获取当前输出内容片段
                        String text = "";
                        if (token.getResult() != null) {
                            text = token.getResult().getOutput().getText();
                        }
                        if (StrUtil.isNotBlank(text)) {
                            sb.append(text);
                            log.info("当前段数据:{}", text);
                            // 换行符转义:token换行符转换成<br>
                            text = text.replace("\n", "<br>");
                            // 换行符转义:如果token以换行符为结尾,转换成<br>
                            text = text.replace(" ", "&nbsp;");
                        }
                        // 发送返回的数据
                        try {
                            if (StrUtil.isNotBlank(text)) {
                                finalEmitter.send(SseEmitter.event().data(text));
                            }
                        } catch (IOException e) {
                            throw new RuntimeException(e);
                        }
                        // 更新最后一个响应
                        lastResponse.set(token);
                    },
                    // 反应式流在报错时会直接中断
                    e -> {
                        log.error("ai对话 流式输出报错:{}", e.getMessage());
                        int usageCount = 0;
                        ChatResponse chatResponse = lastResponse.get();
                        if (chatResponse != null) {
                            Usage usage = chatResponse.getMetadata().getUsage();
                            usageCount = usage.getTotalTokens();
                        } else {
                            log.warn("未获取到 Token 使用信息,可能模型未返回或配置未启用");
                        }
                        // 更新中断的状态
                        tryUpdateMessage(aiMessagePair,
                                sb.toString(),
                                true,
                                usageCount);
                        finalEmitter.completeWithError(e);
                        emitterManager.removeEmitter(sessionId); // 出错时也移除
                    }, // 错误处理
                    () -> {// 流结束
                        log.info("\n回答完毕!");
                        // 从最后一个响应中获取 Token 使用信息
                        ChatResponse chatResponse = lastResponse.get();
                        int usageCount = 0;
                        if (chatResponse != null) {
                            Usage usage = chatResponse.getMetadata().getUsage();
                            usageCount = usage.getTotalTokens();
                        } else {
                            log.warn("未获取到 Token 使用信息,可能模型未返回或配置未启用");
                        }
                        finalEmitter.complete();
                        emitterManager.removeEmitter(sessionId); // 只在流结束后移除
                        log.info("最终拼接的数据:{}", sb);
                        log.info("token使用:{}", usageCount);
                        // 更新正常完成的状态
                        tryUpdateMessage(aiMessagePair,
                                sb.toString(),
                                false,
                                usageCount);
                    });
            return true;
        }, Executors.newVirtualThreadPerTaskExecutor());
    }

    /**
     * ai回答推流sse
     */
    @Override
    public SseEmitter getEmitter(String sessionId) {
        // 获取对应sessionId的sseEmitter
        SseEmitter emitter = emitterManager.getEmitter(sessionId);
        if (emitter != null) {
            emitter.onCompletion(() -> emitterManager.removeEmitter(sessionId));
            emitter.onTimeout(() -> emitterManager.removeEmitter(sessionId));
            return emitter;
        }
        return null;
    }

    /**
     * 在用户发送消息后就插入消息,在对话流结束或报错时再更新状态。尝试更新消息的方法
     */
    private void tryUpdateMessage(AiMessagePair message,
                                  String content,
                                  boolean isInterrupted,
                                  Integer tokenUsed) {
        int status = isInterrupted ? AiMessageStatusEnum.STOPPED.getCode() : AiMessageStatusEnum.FINISHED.getCode();
        message.setStatus(status);
        message.setAiContent(content);
        message.setTokens(tokenUsed);
        message.setResponseTime(Date.from(Instant.now()));
        aiMessagePairMapper.updateBySseIdSelective(message);
    }

}
会话管理类
java 复制代码
package com.losgai.ai.global;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 统一处理sse连接
 */
@Component
@Slf4j
public class SseEmitterManager {
    // 支持的同时在线人数=SESSION_LIMIT-1 有一个监控sse
    private static final int SESSION_LIMIT = 101;

    private final Map<String, SseEmitter> emitterMap = new ConcurrentHashMap<>();

    /**
     * 将对话请求加入队列
     */
    public boolean addEmitter(String sessionId, SseEmitter emitter) {
        if (emitterMap.size() < SESSION_LIMIT) {
            emitterMap.put(sessionId, emitter);
            // 推流当前线程数
            this.notifyThreadCount();
            return true;
        }
        return false;
    }

    /**
     * 获取Map中的sse连接
     */
    public SseEmitter getEmitter(String sessionId) {
        return emitterMap.get(sessionId);
    }

    public void removeEmitter(String sessionId) {
        emitterMap.remove(sessionId);
        // 推流当前线程数
        this.notifyThreadCount();
    }

    public boolean isOverLoad() {
        return emitterMap.size() >= SESSION_LIMIT;
    }

    public int getEmitterCount() {
        return emitterMap.size();
    }
    
    /**
     * 获取线程监控实例
     */
    public void addThreadMonitor() {
        // 添加一个线程监控
        emitterMap.put("thread-monitor", new SseEmitter(0L));
    }

    /**
     * 手动发送消息,通知当前占用的线程数
     */
    public void notifyThreadCount() {
        SseEmitter sseEmitter = emitterMap.get("thread-monitor");
        try {
            if (emitterMap.containsKey("thread-monitor")) {
                sseEmitter.send(this.getEmitterCount());
            }else {
                addThreadMonitor();
                sseEmitter = emitterMap.get("thread-monitor");
                sseEmitter.send(this.getEmitterCount());
            }
        } catch (IOException e) {
            sseEmitter.completeWithError(e);
            emitterMap.remove("thread-monitor"); // 清除失效连接
        }
    }

    /**
     * 将sse连接全部关闭
     */
    public void closeAll() {
        for (SseEmitter emitter : emitterMap.values()) {
            emitter.complete();
        }
        emitterMap.clear();
    }

}
进行测试

可以使用POSTMANAPIFOX等。

测试网址

javascript 复制代码
POST http://localhost:[端口号]/chat/send

测试请求体

json 复制代码
{
  "question": "你是谁?",
  "modelId": 1
}

参数解释

  • question: 问题

  • modelId:数据库中对应配置的主键ID

输出结果
bash 复制代码
2025-06-20T10:59:51.405+08:00  INFO 18588 --- [ai] [onPool-worker-2] c.l.ai.service.impl.AiChatServiceImpl    : 当前段数据:你好
2025-06-20T10:59:51.407+08:00  INFO 18588 --- [ai] [onPool-worker-2] c.l.ai.service.impl.AiChatServiceImpl    : 当前段数据:!
2025-06-20T10:59:51.408+08:00  INFO 18588 --- [ai] [onPool-worker-2] c.l.ai.service.impl.AiChatServiceImpl    : 当前段数据:我
2025-06-20T10:59:51.507+08:00  INFO 18588 --- [ai] [ient-4-Worker-0] c.l.ai.service.impl.AiChatServiceImpl    : 当前段数据:是一个
2025-06-20T10:59:51.642+08:00  INFO 18588 --- [ai] [ient-4-Worker-0] c.l.ai.service.impl.AiChatServiceImpl    : 当前段数据:AI助手,我的
2025-06-20T10:59:51.725+08:00  INFO 18588 --- [ai] [ient-4-Worker-0] c.l.ai.service.impl.AiChatServiceImpl    : 当前段数据:名字是通义
2025-06-20T10:59:51.823+08:00  INFO 18588 --- [ai] [ient-4-Worker-0] c.l.ai.service.impl.AiChatServiceImpl    : 当前段数据:千问。我可以
2025-06-20T10:59:52.044+08:00  INFO 18588 --- [ai] [ient-4-Worker-0] c.l.ai.service.impl.AiChatServiceImpl    : 当前段数据:帮助你回答问题
2025-06-20T10:59:52.088+08:00  INFO 18588 --- [ai] [ient-4-Worker-0] c.l.ai.service.impl.AiChatServiceImpl    : 当前段数据:、创作文字、
2025-06-20T10:59:52.341+08:00  INFO 18588 --- [ai] [ient-4-Worker-0] c.l.ai.service.impl.AiChatServiceImpl    : 当前段数据:表达观点,甚至
2025-06-20T10:59:52.386+08:00  INFO 18588 --- [ai] [ient-4-Worker-0] c.l.ai.service.impl.AiChatServiceImpl    : 当前段数据:可以进行一些创造
2025-06-20T10:59:52.508+08:00  INFO 18588 --- [ai] [ient-4-Worker-0] c.l.ai.service.impl.AiChatServiceImpl    : 当前段数据:性的任务,比如
2025-06-20T10:59:52.717+08:00  INFO 18588 --- [ai] [ient-4-Worker-0] c.l.ai.service.impl.AiChatServiceImpl    : 当前段数据:写故事、写
2025-06-20T10:59:52.738+08:00  INFO 18588 --- [ai] [ient-4-Worker-0] c.l.ai.service.impl.AiChatServiceImpl    : 当前段数据:公文、写
2025-06-20T10:59:52.844+08:00  INFO 18588 --- [ai] [ient-4-Worker-0] c.l.ai.service.impl.AiChatServiceImpl    : 当前段数据:邮件、写剧本
2025-06-20T10:59:52.969+08:00  INFO 18588 --- [ai] [ient-4-Worker-0] c.l.ai.service.impl.AiChatServiceImpl    : 当前段数据:等等。我还可以
2025-06-20T10:59:53.053+08:00  INFO 18588 --- [ai] [ient-4-Worker-0] c.l.ai.service.impl.AiChatServiceImpl    : 当前段数据:进行逻辑推理、
2025-06-20T10:59:53.218+08:00  INFO 18588 --- [ai] [ient-4-Worker-0] c.l.ai.service.impl.AiChatServiceImpl    : 当前段数据:编程等。如果你
2025-06-20T10:59:53.263+08:00  INFO 18588 --- [ai] [ient-4-Worker-0] c.l.ai.service.impl.AiChatServiceImpl    : 当前段数据:有任何问题或者需要
2025-06-20T10:59:53.314+08:00  INFO 18588 --- [ai] [ient-4-Worker-0] c.l.ai.service.impl.AiChatServiceImpl    : 当前段数据:帮助,随时告诉我
2025-06-20T10:59:53.382+08:00  INFO 18588 --- [ai] [ient-4-Worker-0] c.l.ai.service.impl.AiChatServiceImpl    : 当前段数据:!
2025-06-20T10:59:53.386+08:00  INFO 18588 --- [ai] [ient-4-Worker-0] c.l.ai.service.impl.AiChatServiceImpl    : 
回答完毕!
2025-06-20T10:59:53.386+08:00  INFO 18588 --- [ai] [ient-4-Worker-0] c.l.ai.service.impl.AiChatServiceImpl    : 最终拼接的数据:你好!我是一个AI助手,我的名字是通义千问。我可以帮助你回答问题、创作文字、表达观点,甚至可以进行一些创造性的任务,比如写故事、写公文、写邮件、写剧本等等。我还可以进行逻辑推理、编程等。如果你有任何问题或者需要帮助,随时告诉我!
2025-06-20T10:59:53.386+08:00  INFO 18588 --- [ai] [ient-4-Worker-0] c.l.ai.service.impl.AiChatServiceImpl    : token使用:95

总结

通过上面的操作,我们就实现了使用SpringAI实现流式对话输出,并进行了一定程度的方法封装,之后我计划再添加上多轮对话记忆、RAG等功能。

相关推荐
2025学习4 小时前
Spring循环依赖导致Bean无法正确初始化
后端
parade岁月4 小时前
从浏览器存储到web项目中鉴权的简单分析
前端·后端
用户91453633083915 小时前
ThreadLocal详解:线程私有变量的正确使用姿势
后端
IT_10245 小时前
SpringBoot扩展——发送邮件!
java·spring boot·后端
用户4099322502125 小时前
如何在FastAPI中实现权限隔离并让用户乖乖听话?
后端·ai编程·trae
阿星AI工作室5 小时前
n8n教程:5分钟部署+自动生AI日报并写入飞书多维表格
前端·人工智能·后端
郝同学的测开笔记5 小时前
深入理解 kubectl port-forward:快速调试 Kubernetes 服务的利器
后端·kubernetes
Ray665 小时前
store vs docValues vs index
后端
像污秽一样6 小时前
软件开发新技术复习
java·spring boot·后端·rabbitmq·cloud