Spring AI

参考网址

https://spring.p2hp.com/index.html

https://spring.io/projects/spring-ai

介绍

Spring AI 是一款 AI 工程领域的应用程序框架,核心是将 Spring 生态的可移植性、模块化等设计原则延伸至 AI 领域,推广以 POJO 作为 AI 应用的构建块。它提供了开发 AI 大模型应用所需的基础抽象模型及多种实现方式,支持开发者以少量代码改动完成组件替换,同时具备友好的 API,最终目的是简化 AI 大模型应用的开发流程。

Spring AI 的主要功能

  • 对主流 AI 大模型供应商提供了支持,比如:OpenAI、DeepSeek、Microsoft、Ollama、Amazon、Google HuggingFace 等。
  • 支持 AI 大模型类型包括:聊天、文本到图像、文本到声音等。
  • 支持主流的 Embedding Models(嵌入模型)和向量数据库,比如:Azure Vector Search、Chroma、Milvus、Neo4j、Redis、PineCone、PostgreSQL/PGVector 等。
  • 把 AI 大模型输出映射到简单的 Java 对象(POJOs)上。
  • 支持了函数调用 (Function Calling) 功能。
  • 为数据工程提供 ETL(数据抽取、转换和加载)框架。
  • 支持 Spring Boot 自动配置和快速启动,便于运行 AI 模型和管理向量库。

Chat API

注底层厂商的请求格式、认证方式、响应解析等细节。

简单说:Spring AI Chat API 是开发者与 LLM 之间的 “翻译官” 和 “统一接口”,让你写一次代码,就能适配多个大模型。

核心定位与解决的问题

在 Spring AI 出现前,对接不同大模型需要处理:

  • 不同厂商的请求参数(如 OpenAI 的 messages vs Anthropic 的 prompt);
  • 认证方式差异(API Key、OAuth 等);
  • 响应格式解析(不同结构的 JSON 结果);
  • 流式 / 非流式调用的实现差异;
  • 对话上下文管理(多轮对话的历史消息维护)。

Spring AI Chat API 统一了这些差异,提供:

  • 标准化的 “请求 - 响应” 模型;
  • 内置的对话上下文管理;
  • 一致的流式 / 非流式调用接口;
  • 可插拔的大模型适配(通过 ChatClient 实现);
  • 与 Spring 生态(如 Spring Boot、依赖注入)无缝集成。

核心概念

  • ChatClient:作为各主流大模型的标准化客户端抽象,核心职责是封装与底层模型的通信逻辑,包括请求的构建与发送、响应的接收与解析,是开发者与大模型交互的统一入口。
  • Prompt:对话请求的顶层封装载体,内部聚合了两类核心要素:一是构成对话内容的Message集合,二是影响模型生成行为的配置选项(Option)。
  • Message:对话交互的基础数据单元,不仅承载着待传递给大模型的具体内容,还包含角色标识(如用户、系统、助手)、附加属性等元信息,同时通过明确的类型划分(如用户消息、系统提示、工具反馈消息)适配不同交互场景。
  • Option:模型生成过程的参数配置项,用于精细化控制输出效果。例如temperature参数(取值范围通常为 0-1),其值越低,模型输出越聚焦事实、严谨一致;值越高,则输出越具随机性与创造性。
  • ChatResponse:大模型响应结果的统一封装对象,内部聚合了生成结果、响应元数据(如调用耗时、Token 统计)等核心信息,其中核心数据为Generation。
  • Generation:封装了大模型生成的具体业务内容,是ChatResponse中最核心的有效数据载体,直接对应开发者所需的模型输出结果。

三个层次

前面我们知道SpringAI支持大模型的多种能力,聊天只是其中一种,因此就有一个代表最顶层的抽象层,与大模型有关的各种能力,都在此有个定义,然后是代表各种能力的抽象层,如聊天、图片、嵌入式处理等,最后是每一种能力在各类具体大模型上的实现,如下图所示
Alt

到现在为止咱们还没有看一行代码一个API,但是从理论上对Chat API的定位、关系已经基本了解了

官方示意图
Alt

最下面橙色这层,中间是client,这里有两种,ModelClient代表了常规的请求响应,StreamingModelClient代表了流式响应(数据并非一次性传输,而是建立链接后源源不断的输出)

client的左侧是request,里面包含了option,至于prompt,那是Chat的概念,所以不会出现在橙色这一层

client右侧是response,同样只有抽象的ResponseMeta和ResultMetaData,generation是Chat的概念,不会在橙色这一层出现

再往上看,绿色的就是功能抽象层了,ChatClient继承了ModelClient,Prompt继承了ModelRequest,代表Chat领域的请求,同理CharResponse继承了ModelResponse

有了理论基础,一张官方图就让我们看清了Chat API的大概,现在还缺点东西,就是具体的实现层,毕竟有很多种大模型能,最终编码时还是要用到实现层的类,有没有什么方式将实现层完美的展现出来?

实现层和功能抽象层的关系
Alt

这张图是 Spring AI 中 ChatClient 相关组件的类层级与接口继承关系图,核心呈现了 “通用 API 抽象 → 专用接口 → 厂商具体实现” 的设计架构,清晰展示了 Spring AI 如何通过标准化接口统一不同大模型的对话调用能力,以下是详细拆解:

一、核心架构层级(从上到下:抽象→实现)

图中组件按 “通用抽象 → 专用扩展 → 厂商适配” 分为 3 个核心层级,每层职责明确:

1. 最顶层:通用模型 API 抽象(基础接口)

定义了所有 AI 模型交互的通用规范,不局限于 “对话” 场景,是 Spring AI 统一接口设计的根基:

  • ModelClient:
    • 核心定位:通用模型 API 顶层接口,支持所有模型类型(文本、图像、音频等)的同步调用。
    • 核心方法:call(TReq) : TResp(接收泛型请求 TReq,返回泛型响应 TResp),实现 “输入 - 输出” 的标准化映射。
  • StreamingModelClient:
    • 核心定位:通用流式模型 API 接口,继承自通用模型 API 的设计思路,专注于 “流式响应” 场景。
    • 核心方法:streamingCall(TReq) : Flux(返回响应式编程的 Flux 流,支持逐段接收模型输出,如 ChatGPT 打字效果)。

2. 中间层:对话专用 API 接口(功能扩展)

基于顶层通用 API,针对 “对话交互” 场景做专项抽象,屏蔽非对话模型的无关能力,聚焦聊天场景需求:

  • ChatClient:
    • 核心定位:通用对话完成 API 接口,继承自 ModelClient 的设计思想(图中 “IS A” 表示继承关系),专门用于非流式对话调用。
    • 核心方法:call(Prompt) : ChatResponse(输入为 Spring AI 标准化的 Prompt 对象,输出为对话专用的 ChatResponse,适配多轮对话、角色消息等场景)。
  • StreamingChatClient:
    • 核心定位:流式对话 API 接口,继承自 StreamingModelClient 的设计思路,专门用于流式对话调用。
    • 核心方法:call(Prompt) : Flux(输入为 Prompt,输出为 ChatResponse 的 Flux 流,支持实时接收对话响应)。

3. 最底层:厂商具体实现类(落地适配)

针对不同大模型厂商的对话模型,提供 ChatClient 或 StreamingChatClient 的具体实现,是开发者实际使用的 “厂商专属客户端”,无需关注底层调用细节:

图中包含 11 个主流厂商 / 平台的实现类,覆盖云厂商、开源模型、本地部署模型等场景:

  • OpenAI 生态:OpenAiChatClient(OpenAI 原生模型,如 GPT-3.5/4)、AzureOpenAiChatClient(Azure 托管的 OpenAI 模型)。
  • Google 生态:VertexAiPaLm2ChatClient(Google Vertex AI 上的 PaLM 2 模型)、VertexAiGeminiChatClient(Google Vertex AI 上的 Gemini 模型)。
  • AWS Bedrock 生态:BedrockAnthropicChatClient(Bedrock 上的 Anthropic Claude 模型)、BedrockTitanChatClient(Bedrock 上的 Titan 模型)、BedrockLlama2ChatClient(Bedrock 上的 Llama 2 模型)、BedrockCohereChatClient(Bedrock 上的 Cohere 模型)。
  • 开源 / 本地模型:MistralAiChatClient(Mistral 开源模型)、HuggingfaceChatClient(Hugging Face 开源模型,如 BERT、GPT-2)、OllamaChatClient(Ollama 本地部署模型,如 Llama 2、Mistral 本地版)。

二、核心设计思想与价值

这张图直观体现了 Spring AI 的核心设计理念:

  1. 接口抽象解耦:通过 ModelClient/ChatClient 等顶层接口定义标准,厂商实现类专注于 “适配自身 API”,开发者依赖接口编程,无需修改代码即可切换模型。
  2. 流式与非流式分离:将 “同步单次响应”(ChatClient)与 “流式增量响应”(StreamingChatClient)拆分为两个接口,适配不同业务场景(如简单问答用非流式,实时聊天用流式)。
  3. 覆盖全场景模型:实现类涵盖云厂商(OpenAI、Azure、Google、AWS)、开源社区(Hugging Face、Mistral)、本地部署(Ollama),满足不同用户的模型选择需求。

核心类和接口

Alt
ChatClient

  • 大模型聊天功能的客户端接口,在进程中,其实现就是各大模型对应的客户端类
  • 主要是call方法,这就是最常规的聊天功能,调用call发送请求,返回值就是大模型的响应

StreamingChatClient

  • 这也是客户端类,用于调用大模型的功能,与ChatClient不同的是,ChatClient是请求响应,返回对象ChatResponse就是大模型返回的全部内容,而StreamingChatClient返回的是Flux,这是流式返回,可以讲大模型的响应进行流式输出,如果您使用过各种大模型聊天工具,会发现响应的内容并非一次性展现,而是一段一段的内容,持续不断的展现出来,这就是流式响应的效果
  • 注意注解FunctionalInterface,表明这是个函数式接口

Prompt

  • 前面看过了ChatClient和StreamingChatClient,会发现入参都是Prompt,可见这就是和大模型一次聊天的入参
  • 下面是Prompt的源码,去掉了构造函数、toString这些之后就会发现,最重要的是Message和ChatOption,所以Prompt只是个打包,真正要提交到大模型的其实是Message和ChatOption
  • 如果您对OpenAI有所了解,就知道prompt(提示词)并非只有用户输入的聊天内容那么简单,而是system、user 、assistant等多种类型 ,所以这里的Prompt并非只是一个外壳那么简单,它与不同类型的message、不同的辅助类等一起提供了完善的提示词功能,这个会有单独的文章来说明和实战,本篇只要记得它的最终形态就是打好的包用于提交给大模型
  • 如果只是最基本的聊天,下面这个构造方法来创建对象就行了
public Prompt(String contents) {
     this(new UserMessage(contents));
}

Message

  • Message很好理解:在聊天过程中,聊天内容对应的对象,请求和响应用的都是Message,不过由于消息类型的多样性,Message被设计成了接口,根据不同类型都有对应的实现
  • Message自身非常简单,能保证使用方取到消息内容、类型即可
  • 另外要注意的是消息类型,一共四种
public enum MessageType {
      
   USER("user"),
      
   ASSISTANT("assistant"),
      
   SYSTEM("system"),
      
   FUNCTION("function");
}

ChatOptions

  • ChatOptions代表可以传递给大模型的控制参数,具体有哪些参数和大模型自身开放的特性有关,举个例子,下面是OpenAI开放的参数
  • presencePenalty : 影响模型在生成文本时重复词语或概念的倾向
  • frequencyPenalty:影响模型在生成文本时对已出现过词语的偏好程度
  • 按照上面的解释,既然各种大模型都有自己的参数,那么设计ChatOptions能干啥?应该能放一些通用的控制参数吧,打开代码一看果然如此,共有三个通用参数。
      /**
       * The ChatOptions represent the common options, portable across different chat models.
       */
      public interface ChatOptions extends ModelOptions {
      	// 大模型生成的内容应该更严谨还是更有创造性
      	Float getTemperature();
      	// 返回概率超过P的所有内容
      	Float getTopP();
      	// 返回概率最高的前K个内容
      	Integer getTopK();
      }
  • ChatOptions只是接口,对应的实现是ChatOptionsImpl,源码没啥好看的,就是temperature、topP、topK的get和set而已,为了实例化ChatOptionsImpl,还有配套工具ChatOptionsBuilder,用法如下
      ChatOptions portablePromptOptions = ChatOptionsBuilder.builder()
      			.withTemperature(0.9f)
      			.withTopK(100)
      			.withTopP(0.6f)
      			.build();

ChatResponse

  • 看完请求该看响应了,既然Generation才是真正的响应内容,那么ChatResponse也就是个壳,里面包了Generation,打开源码一看,只有Generation和ChatResponseMetadata,这个ChatResponseMetadata可以理解为元信息,主要返回了大模型的API的使用情况说明,以及限速的详细信息
      public class ChatResponse implements ModelResponse<Generation> {
      
          private final ChatResponseMetadata chatResponseMetadata;
      	private final List<Generation> generations;
      
      	@Override
      	public ChatResponseMetadata getMetadata() {...}
      
          @Override
      	public List<Generation> getResults() {...}
      
          // other methods omitted
      }

Generation

  • Generation中有响应的具体信息,由ChatGenerationMetadata和AssistantMessage组成
      public class Generation implements ModelResult<AssistantMessage> {
      
      	private AssistantMessage assistantMessage;
      	private ChatGenerationMetadata chatGenerationMetadata;
      
      	@Override
      	public AssistantMessage getOutput() {...}
      
      	@Override
      	public ChatGenerationMetadata getMetadata() {...}
      
          // other methods omitted
      }
  • ChatGenerationMetadata代表返回内容的元信息,包含了结束原因、生成内容的过滤规则
  • AssistantMessage更容易理解了:类型是ASSISTANT的消息,这个assistant就是助理角色,assistant消息就是大模型返回的聊天响应,源码如下
      public class AssistantMessage extends AbstractMessage {
      
      	public AssistantMessage(String content) {
      		super(MessageType.ASSISTANT, content);
      	}
      
      	public AssistantMessage(String content, Map<String, Object> properties) {
      		super(MessageType.ASSISTANT, content, properties);
      	}
      
      	@Override
      	public String toString() {
      		return "AssistantMessage{" + "content='" + getContent() + '\'' + ", properties=" + properties + ", messageType="
      				+ messageType + '}';
      	}
      
      }

示例代码

同步对话

后端代码

依赖

    <properties>
        <java.version>17</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <spring-boot.version>3.3.4</spring-boot.version>
        <spring-ai.version>1.0.0-M6</spring-ai.version>
    </properties>
    
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
            <version>${spring-ai.version}</version>
        </dependency>
    
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-core</artifactId>
            <version>${spring-ai.version}</version>
        </dependency>
    
        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
            <version>2.0.55</version>
        </dependency>
    </dependencies>
    
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <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>

application.yml主配置文件

    spring:
      ai:
        openai:
          base-url: https://dashscope.aliyuncs.com/compatible-mode # 基础 URL
          api-key: sk-a4ea34ca69f********91167245b1 # 百炼大模型API 密钥
          chat:
            options:
              model: qwen-plus # 模型名称
              temperature: 0.1 # 温度,控制生成文本的随机性,默认值为 0.1, 范围为 [0.0, 1.0], 较高的温度会使生成的文本更加随机,而较低的温度会使生成的文本更加确定
              max-tokens: 2000 # 最大令牌数,控制生成文本的长度,默认值为 2000, 范围为 [100, 4000]
      main:
        allow-bean-definition-overriding: true
      datasource:
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/mall_116?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
        username: root
        password: root1234
    server:
      port: 8080
    
    logging:
      level:
        org.springframework.ai: DEBUG
        com.woniuxy.spring.ai.base: DEBUG

温度 (Temperature)

是控制生成文本随机性与创造性的核心参数,数值越高输出越灵活多样,越低则越确定、保守。

1.核心作用

  • 本质是调节模型采样概率:低温时,模型优先选择概率最高的词;高温时,会放大低概率词的选中概率。
  • 直接影响输出风格:是 “按规矩作答” 还是 “自由发挥” 的关键开关。

2.数值范围与效果

  • 低温(0–0.3):输出高度确定、精准,重复率可能较高。
    适合场景:事实问答、代码生成、专业文档撰写等需要准确的场景。
  • 中温(0.4–0.7):平衡准确性与创造性,输出自然流畅。
    适合场景:日常对话、文案创作、邮件撰写等多数通用场景。
  • 高温(0.8–1.0):输出随机性强,充满创意但可能偏离主题、逻辑跳跃。
    适合场景:诗歌创作、脑洞故事、营销创意发散等需要突破常规的场景。

3.关键注意点

  • 数值并非越高越好,过高可能导致输出无意义、逻辑混乱。
  • 不同模型的 Temperature 基准略有差异,但核心逻辑一致。

聊天配置类

    import org.springframework.ai.chat.client.ChatClient;
    import org.springframework.ai.chat.model.ChatModel;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    public class ChatConfig {
        @Bean
        public ChatClient chatClient(ChatModel chatModel) {
            return ChatClient.builder(chatModel).build();
        }
    }

同步聊天服务接口

    public interface ChatService {
        public String chat(String message);
    }

服务实现类

    import com.woniuxy.spring.ai.base.memory.RedisChatMemory;
    import com.woniuxy.spring.ai.base.service.ChatRecordService;
    import com.woniuxy.spring.ai.base.service.ChatService;
    import lombok.RequiredArgsConstructor;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.ai.chat.client.ChatClient;
    import org.springframework.stereotype.Service;
    
    @Slf4j
    @Service
    @RequiredArgsConstructor
    public class ChatServiceImpl implements ChatService {
    
        private final ChatClient chatClient;
        private final RedisChatMemory redisChatMemory;
        private final ChatRecordService chatRecordService;
        /**
         * 同步对话
         */
        @Override
        public String chat(String message) {
            log.info("发送消息到阿里百炼: {}", message);
            
            String response = chatClient.prompt(message)
                    .call()
                    .content();
            
            log.info("收到阿里百炼响应: {}", response);
            return response;
        }
    
    }

控制层接口

    import com.woniuxy.woniuspringai.service.ChatService;
    import lombok.RequiredArgsConstructor;
    import org.springframework.http.MediaType;
    import org.springframework.web.bind.annotation.*;
    import reactor.core.publisher.Flux;
    
    import java.util.Map;
    
    @RestController
    @RequestMapping("/api/ai")
    @RequiredArgsConstructor
    public class ChatController {
    
        private final ChatService chatService;
    
        /**
         * 同步对话
         */
        @PostMapping("/chat")
        public Map<String, String> chat(@RequestBody Map<String, String> request) {
            String message = request.get("message");
            String response = chatService.chat(message);
            return Map.of("response", response);
        }
    }

跨域配置

    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.cors.CorsConfiguration;
    import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
    import org.springframework.web.filter.CorsFilter;
    
    @Configuration
    public class RedisConfigurationclass CorsConfig {
    
        @Bean
        public CorsFilter corsFilter() {
            CorsConfiguration config = new CorsConfiguration();
    
            // 1. 允许所有域名
            config.addAllowedOriginPattern("*"); 
    
            // 2. 允许所有请求头
            config.addAllowedHeader(CorsConfiguration.ALL);
    
            // 3. 允许所有请求方法
            config.addAllowedMethod(CorsConfiguration.ALL);
    
            // 4. 允许携带Cookie
            config.setAllowCredentials(true);
    
            // 5. 预检请求有效期
            config.setMaxAge(3600L);
    
            UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
            source.registerCorsConfiguration("/**", config);
    
            return new CorsFilter(source);
        }
    }
前端代码

安装axios、router、element-plus

    npm install axios vue@3 vue-router@4
    npm install element-plus --save

app.vue

    <template>
      <router-view />
    </template>
    
    <script setup>
    
    </script>

封装axios
src/api/aiChat.js

    import axios from 'axios';
    
    const baseURL = 'http://localhost:8080'
    
    // 创建axios实例
    const apiClient = axios.create({
        baseURL: baseURL + '/api/ai', // 后端接口基础路径
        timeout: 10000,
        headers: {
            'Content-Type': 'application/json;charset=UTF-8'
        }
    });
    
    // 普通对话接口
    export const chat = async (message) => {
        const res = await apiClient.post('/chat', { message });
        return res.data;
    };

聊天页面
src/views/AiChat.vue

    <template>
      <el-container style="max-width: 800px; margin: 0 auto; padding: 20px; font-family: Arial, sans-serif;">
        <el-header>
          <h1 style="margin: 0; color: #303133;">AI 对话助手</h1>
        </el-header>
    
        <el-main>
          <!-- 普通对话 -->
          <el-card shadow="hover" style="margin-bottom: 20px;">
            <template #header>
              <span style="font-size: 16px; font-weight: 600;">普通对话</span>
            </template>
    
            <el-form :inline="true" :model="normalForm" style="margin-bottom: 15px;">
              <el-form-item label="" prop="message">
                <el-input
                  v-model="normalForm.message"
                  placeholder="请输入对话内容..."
                  style="width: 100%;"
                ></el-input>
              </el-form-item>
              <el-form-item>
                <el-button
                  type="primary"
                  @click="sendNormalChat"
                  :loading="normalChatLoading"
                >
                  发送
                </el-button>
              </el-form-item>
            </el-form>
    
            <el-card v-if="normalChatResponse" shadow="never" style="background: #f8f9fa; border: 1px solid #e9ecef;">
              <div style="white-space: pre-wrap; color: #303133;">
                <span style="font-weight: 600;">AI 回复:</span>
                {{ normalChatResponse }}
              </div>
            </el-card>
          </el-card>
        </el-main>
      </el-container>
    </template>
    
    <script setup>
    import { ref, reactive } from 'vue';
    import { ElMessage } from 'element-plus'; // 引入消息提示组件
    import { chat } from '@/api/aiChat';
    
    // 普通对话 - 改用 reactive 表单对象(Element Plus 推荐)
    const normalForm = reactive({
      message: ''
    });
    const normalChatLoading = ref(false);
    const normalChatResponse = ref('');
    
    // 普通对话发送方法
    const sendNormalChat = async () => {
      if (!normalForm.message.trim()) {
        ElMessage.warning('请输入对话内容!');
        return;
      }
    
      normalChatLoading.value = true;
      normalChatResponse.value = '';
      
      try {
        const res = await chat(normalForm.message.trim());
        normalChatResponse.value = res.response;
      } catch (err) {
        console.error('普通对话失败:', err);
        normalChatResponse.value = '对话失败,请重试';
        ElMessage.error('对话失败:' + err.message || '未知错误');
      } finally {
        normalChatLoading.value = false;
      }
    };
    
    </script>
    
    <style scoped>
    </style>

main.js

    import { createApp } from 'vue';
    import App from './App.vue';
    import AIChat from './views/AIChat.vue';
    import { createRouter, createWebHistory } from 'vue-router';
    import ElementPlus from 'element-plus'
    import 'element-plus/dist/index.css'
    
    // 路由配置
    const routes = [
      { path: '/', component: AIChat }
    ];
    
    const router = createRouter({
      history: createWebHistory(),
      routes
    });
    
    const app = createApp(App);
    app.use(router);
    app.use(ElementPlus)
    app.mount('#app');

流式对话

后端实现
SSE

SSE(Server-Sent Events,服务器推送事件)是一种 基于 HTTP 的简单流式通信协议,核心是让服务器能主动、持续地向客户端推送数据,而无需客户端反复发起请求(即 “一次连接,多次推送”)。

可以把它理解为:客户端和服务器建立一条 “长连接”,服务器像 “发消息” 一样,把数据一块一块推给客户端,客户端收到就实时处理
Alt

核心特点

  1. 单向通信:只有服务器往客户端推数据,客户端不能通过这条连接给服务器发数据(如果客户端要发消息,需单独用 HTTP 请求);
  2. 基于 HTTP/HTTPS:不用额外开端口,和普通接口用一样的协议,防火墙、代理都能兼容;
  3. 文本协议:推送的数据是文本格式(通常是 JSON 字符串),简单易解析;
  4. 自动重连:客户端(如浏览器的 EventSource API)默认支持断连后自动重连,不用手动处理;
  5. 长连接:连接建立后会保持,直到服务器主动关闭或客户端断开(区别于普通 HTTP 短连接)。

和普通 HTTP 接口的核心区别
在这里插入图片描述

常见使用场景

  • AI 流式回复(如 ChatGPT、你的 chat 接口场景):AI 生成内容时逐字 / 逐句推给前端,避免用户等待;
  • 实时通知(如系统公告、订单状态变更):服务器有新消息时,立即推给在线用户;
  • 实时数据更新(如股票行情、监控数据):周期性推送最新数据,客户端实时展示;
  • 长文本返回(如 AI 写文章、生成报告):分块推送避免 “加载空白屏”。
Flux

Flux是 Reactor 响应式编程框架(Spring WebFlux 核心依赖)中的核心类型,代表0 到 N 个字符串类型元素的异步序列,是实现「流式响应」(如 AI 流式回复、实时日志推送)的关键。

简单理解:

  • 传统同步返回(如 String):一次性返回所有数据;
  • Flux:异步、分批次返回数据,每产生一个元素就推送给前端,直到序列结束 / 出错。
    在这里插入图片描述

代码实现

    /**
     * 流式对话
     */
    public Flux<String> chatStream(String message) {
        log.info("发送流式消息到阿里百炼: {}", message);
    
        return chatClient.prompt(message)
            .stream()
            .content()
            .concatWith(Mono.just("[DONE]")) // 确保最后发送[DONE]标记
            .onErrorResume(e -> {
                log.error("流式对话异常", e);
                return Flux.just("对话出错:" + e.getMessage(), "[DONE]");
            });
    }

控制层接口

    /**
     * 流式对话接口:必须设置 Content-Type: text/event-stream
     */
    @GetMapping(value = "/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<String> chatStream(@RequestParam("message") String message) {
        return chatService.chatStream(message);
    }
前端
EventSource

EventSource 是 HTML5 新增的服务器推送事件(Server-Sent Events,SSE) 的前端实现,用于建立浏览器与服务器的单向长连接,让服务器可以主动、持续地向客户端推送数据(仅支持文本格式)。
Alt

核心特点:

  • 单向通信:仅服务器 → 客户端(如需双向通信用 WebSocket);
  • 基于 HTTP:无需额外协议,兼容现有 HTTP 生态(如 CORS、认证);
  • 自动重连:连接断开后默认自动重试(可配置);
  • 轻量级:API 简单,无需复杂封装,适合流式文本传输(如 AI 流式回复、实时日志)。

核心属性 / 方法
在这里插入图片描述

代码实现

aiChat.js

    // 流式对话
    export const chatStream = (message, onMessage, onComplete) => {
        const source = new EventSource(`${baseURL}/api/ai/chat/stream?message=${encodeURIComponent(message)}`);
    
        source.onmessage = (event) => {
            let rawData = event.data.trim();
    
            // 检测到[DONE]:关闭连接 + 触发完成回调
            if (rawData.includes('[DONE]')) { 
                source.close();
                // 通知页面:流式结束,关闭加载状态
                typeof onComplete === 'function' && onComplete();
                return;
            }
    
            let content = rawData.replace(/^data:\s*/, '').replace(/^\[DONE\]*/, '');
            if (content) {
                onMessage(content);
            }
        };
    
        // 新增:连接错误时也触发完成回调(兜底)
        source.onerror = () => {
            source.close();
            typeof onComplete === 'function' && onComplete();
        };
    
        return () => {
            if (source.readyState !== EventSource.CLOSED) {
                source.close();
            }
        };
    };

AiChat.vue

- 新增标签

    <el-card shadow="hover">
        <template #header>
    <span style="font-size: 16px; font-weight: 600;">流式对话</span>
        </template>
    
        <el-form :inline="true" :model="streamForm" style="margin-bottom: 15px;">
            <el-form-item label="" prop="message">
                <el-input
                          v-model="streamForm.message"
                          :rows="4"
                          placeholder="请输入对话内容..."
                          style="width: 100%;"
                          ></el-input>
            </el-form-item>
            <el-form-item>
                <el-button
                           type="primary"
                           @click="sendStreamChat"
                           :loading="streamChatLoading"
                           :disabled="streamChatLoading"
                           >
                    发送
                </el-button>
                <el-button
                           type="danger"
                           @click="stopStreamChat"
                           v-show="streamChatLoading"
                           >
                    停止
                </el-button>
            </el-form-item>
        </el-form>
    
        <el-card shadow="never" style="background: #f8f9fa; border: 1px solid #e9ecef; min-height: 100px;">
            <div style="white-space: pre-wrap; color: #303133;">
                <span style="font-weight: 600;">AI 回复:</span>
                {{ streamChatResponse }}
            </div>
        </el-card>
    </el-card>

- 新增代码

    import { chat, chatStream } from '@/api/aiChat';
    
    // 流式对话发送方法
    const sendStreamChat = () => {
      if (!streamForm.message.trim()) {
        ElMessage.warning('请输入对话内容!');
        return;
      }
    
      streamChatLoading.value = true;
      streamChatResponse.value = '';
      
      // 关闭之前的流(如果有)
      if (closeStream) closeStream();
      
      // 调用流式接口
      closeStream = chatStream(
        streamForm.message.trim(),
        (chunk) => {
          streamChatResponse.value += chunk;
        },
        // 完成回调:关闭加载状态 + 提示
        () => {
          streamChatLoading.value = false;
          closeStream = null;
          ElMessage.success('流式对话完成!');
        }
      );
    };
    
    // 停止流式对话(增加消息提示)
    const stopStreamChat = () => {
      if (closeStream) {
        closeStream();
        closeStream = null;
      }
      streamChatLoading.value = false;
      ElMessage.info('已停止流式对话');
    };

ChatClient

ChatClient 是 Spring AI 框架中为大模型对话交互设计的核心客户端工具,也是开发者与 AI 模型(如 OpenAI、阿里通义千问、百度文心一言等)进行对话的 “极简入口”。它基于 Spring AI 抽象的 ChatModel 构建,封装了底层复杂的 API 调用、参数拼接、响应解析逻辑,让开发者以 “声明式、链式调用” 的方式快速实现同步 / 流式对话、多轮上下文管理、Prompt 模板调用等核心能力,无需关注底层通信细节。
Alt

核心定位与价值

  1. 简化交互成本:无需手动构建 HTTP 请求、解析 JSON 响应,一行链式代码即可完成 “发送 Prompt → 接收 AI 响应” 的全流程;

  2. 统一交互范式:无论对接 OpenAI、阿里百炼还是其他兼容 OpenAI 协议的模型,ChatClient 提供完全一致的调用方式,切换模型无需修改业务代码;

  3. 灵活扩展能力:支持绑定对话参数(温度、最大令牌数)、上下文内存(ChatMemory)、Prompt 模板,适配同步对话、流式输出、多轮对话等全场景。

Prompt

Prompt(提示词)是开发者 / 用户向 AI 模型传递意图、约束、要求的结构化指令,也是 Spring AI 与大模型交互的核心载体 —— 它不仅是简单的文本提问,更是一套 “告诉 AI 该做什么、怎么做、输出什么格式” 的完整逻辑表达,直接决定 AI 响应的精准度和贴合度。
Alt

在 Spring AI 体系中,Prompt 既可以是纯文本形式(如 “帮我写一份 3 天成都旅行攻略”)
Alt

也可以是通过PromptTemplate构建的结构化模板(绑定动态参数、系统提示、格式约束),其核心价值在于将模糊的业务需求转化为 AI 可理解的清晰指令:比如为 “生成家常菜菜谱” 的需求,Prompt 会明确食材、口味、烹饪难度、输出格式(如分点的食材清单 + 步骤),甚至加入否定条件(“不要复杂步骤”)和风格要求(“口语化、新手友好”),让 AI 跳出通用化输出,精准匹配实际场景。
Alt

从构成来看,一个优质的 Prompt 通常包含角色定位(如 “资深家常菜大厨”)、核心需求(如 “用土豆和牛肉做微辣家常菜”)、约束条件(如 “3 人份、步骤≤5 步”)、输出格式(如 Markdown 列表)四大要素,而非单一的 “提问式文本”。在 Spring AI 的ChatClient调用中,Prompt 是连接业务逻辑与 AI 能力的桥梁 —— 通过prompt()方法传入后,AI 模型会基于 Prompt 的指令完成思考、推理并生成响应,而PromptTemplate则进一步让 Prompt 实现 “模板化、参数化”,适配旅行攻略、朋友圈文案、菜谱生成等多样化的生活化场景,大幅降低重复编写指令的成本。
Alt

Message

介绍

Message(消息)是 Spring AI 构建对话交互的原子级数据载体,也是构成 Prompt 和 Response 的核心基础 —— 它并非简单的文本字符串,而是封装了 “内容 + 角色 + 元数据” 的结构化对象,是大模型理解 “谁在说话、说什么、为什么说” 的最小单元,贯穿从 Prompt 构建到 Response 解析的全流程。
Alt

在 Spring AI 体系中,所有对话交互(无论是用户提问、系统提示,还是 AI 回复)都会被封装为 Message 实例,其核心价值在于:通过明确的 “角色标识” 区分对话参与方(用户 / 系统 / AI 助手),通过 “元数据” 携带额外上下文(如消息 ID、时间戳、模型参数),让大模型能够理解对话的语义边界和上下文关联,而非单纯处理无差别的文本。

  1. 角色(Role):定义消息的 “发送方身份”

角色是 Message 最关键的属性,决定了大模型对消息内容的解读逻辑,Spring AI 中核心角色包括:
在这里插入图片描述

  1. 内容(Content):消息的核心文本 / 数据

即消息的实际内容,可分为两种形式:

  • 纯文本:最常见的形式,如用户的提问、系统的提示词、AI 的文本响应;
  • 多模态内容:高级场景下支持文本 + 图片 / 文件(如 “分析这张图片中的菜品做法”),Spring AI 通过 MultiModalContent 封装。
  1. 元数据(Metadata):附加的上下文信息

可选的键值对数据,用于携带业务或技术上下文,典型用途:

  • 技术元数据:消息 ID、发送时间戳、关联的会话 ID、模型参数(如本次消息对应的 temperature=0.8);
  • 业务元数据:用户 ID、消息优先级、是否需要保留上下文、响应格式要求(如 “返回 Markdown”)。

核心类型与典型用法

Spring AI 围绕 Message 抽象提供了多个具象实现,适配不同对话场景:

  1. UserMessage:用户消息(最常用)

封装用户发起的指令 / 提问,是构建 Prompt 的核心组件:

    public Flux<String> userMessageStream(String message) {
        // 创建用户消息
        UserMessage userMessage = new UserMessage(message);
        // 发送用户消息并返回流式响应
        return chatClient.prompt()
            .messages(userMessage)
            .stream()
            .content()
            .concatWith(Mono.just("[DONE]")) // 确保最后发送[DONE]标记
            .onErrorResume(e -> {
                log.error("流式对话异常", e);
                return Flux.just("对话出错:" + e.getMessage(), "[DONE]");
            });
    }

提取公共代码

    // 提取的公共方法 - 处理流式对话的核心逻辑
    private Flux<String> handleStreamingChat(ChatClient.StreamResponseSpec streamResponseSpec) {
        return streamResponseSpec.content()
            .concatWith(Mono.just("[DONE]")) // 确保最后发送[DONE]标记
            .onErrorResume(e -> {
                log.error("流式对话异常", e);
                return Flux.just("对话出错:" + e.getMessage(), "[DONE]");
            });
    }

修改userMessageStream

    public Flux<String> userMessageStream(String message) {
        // 创建用户消息
        UserMessage userMessage = new UserMessage(message);
    
        ChatClient.StreamResponseSpec stream = chatClient
            .prompt()
            .messages(userMessage)
            .stream();
        // 发送用户消息并返回流式响应
        return handleStreamingChat(stream);
    }
  1. SystemMessage:系统消息(约束 AI 行为)

用于设定 AI 的角色、规则,优先级高于用户消息,是打造精准 Prompt 的关键:

    public Flux<String> systemMessageStream(String message) {
        // 创建系统消息
        SystemMessage systemMessage = 
            new SystemMessage("你是朋友圈文案创作专家,风格要求沙雕有趣,字数控制在50字以内,带1-2个emoji,不要矫情");
        // 创建用户消息
        UserMessage userMessage = new UserMessage(message);
    
        // 创建用户消息, 并添加到消息列表
        ChatClient.StreamResponseSpec stream = chatClient
            .prompt()
            .messages(systemMessage, userMessage)
            .stream();
        // 发送系统消息并返回流式响应
        return handleStreamingChat(stream);
    }

3.不使用Message

    public Flux<String> generateTravelGuide(TravelGuide travelGuide) {
        // 生活化的模板,语气亲切,符合日常旅行规划需求
        String template = """
            你是一位擅长小众路线的旅行规划师,主打「性价比高、人少景美」的非热门路线。
            请按以下要求生成{days}天{destination}旅行攻略({travelType}出行,预算{budget}元):
            ### 核心要求(优先级:性价比>体验>景点数量)
            1. 输出格式:按天数分模块,每个模块包含「景点+美食+住宿」,用Markdown列表展示;
            2. 景点约束:
            - 不推荐XX/XX等网红打卡点;
            - 每个景点标注「开放时间+门票+交通方式」;
            3. 成本约束:
            - 美食人均≤50元,附具体店铺名称;
            - 住宿≤200元/晚,按区域推荐(比如古城/郊区);
            4. 额外要求:
            - 补充1个当地小众玩法(比如菜市场逛吃);
            - 语言口语化,像朋友推荐,不要官方话术;
            - 严格控制总花费不超预算。
            5. 字数200以内。
            """;
            PromptTemplate promptTemplate = new PromptTemplate(template);
        Prompt prompt = promptTemplate.create(Map.of(
            "destination", travelGuide.getDestination(),
            "days", travelGuide.getDays(),
            "travelType", travelGuide.getTravelType(),
            "budget", travelGuide.getBudget()
        ));
        // 发送提示并返回流式响应
        return handleStreamingChat(chatClient.prompt(prompt).stream());
    }

TravelGuide

    @Data
    public class TravelGuide {
        private String destination; // 目的地
        private Integer days;       // 天数
        private String travelType;  // 旅行类型
        private String budget;      // 预算
    }

控制层接口

    /**
     * 生成旅游指南接口
     */
    @PostMapping("/travel/guide")
    public Flux<String> generateTravelGuide(@RequestBody TravelGuide travelGuide) {
        return chatService.generateTravelGuide(travelGuide);
    }

利用Psotman进行测试,测试参数

    {
        "destination":"成都",
        "days": 1,
        "travelType": "汽车",
        "budget": 200
    }

4.最佳实践

实际开发中,可结合「模板化 + 系统消息」提升灵活性,比如给 PromptTemplate 补充系统消息(兼顾规则约束和参数化):

    // 融合写法:SystemMessage(角色约束) + PromptTemplate(参数化用户指令)
    public Flux<String> generateTravelGuide(TravelGuide travelGuide) {
        // 1. 系统消息:定义AI角色(全局约束)
        SystemMessage systemMsg = new SystemMessage("你是旅行规划师,主打小众性价比路线,语言口语化像朋友推荐");
    
        // 2. 模板化用户消息:参数化指令
        String userTemplate = """
            请帮我生成一份{days}天的{destination}旅行攻略,适配{travelType}出行,预算{budget}元左右。
            要求:
            1. 按天数拆分行程,每天推荐2-3个核心景点;
            2. 推荐当地必吃的特色美食(附具体店铺名称+人均消费);
            3. 推荐性价比高的住宿(按区域划分,标注价格区间)。
            4. 字数200以内。
            """;
            PromptTemplate userPromptTemplate = new PromptTemplate(userTemplate);
        Message userMsg = new UserMessage(userPromptTemplate.create(Map.of(
            "destination", travelGuide.getDestination(),
            "days", travelGuide.getDays(),
            "travelType", travelGuide.getTravelType(),
            "budget", travelGuide.getBudget()
        )).getContents());
    
        // 3. 组合为Prompt
        Prompt prompt = new Prompt(List.of(systemMsg, userMsg));
        return handleStreamingChat(chatClient.prompt(prompt).stream());
    }
```java


## 提示词技巧

Prompt(提示词)是 Spring AI 与大模型交互的核心,好的 Prompt 能让 AI 输出更精准、贴合需求的结果。

1**技巧 1:明确 “角色 + 目标”,让 AI 精准定位**

核心逻辑:先给 AI 设定清晰的角色(比如 “旅行规划师”“家常菜大厨”),再明确输出目标,避免 AI 输出偏离方向。

正确示例(旅行攻略)
```java
    String template = """
            你是一位擅长做小众旅行攻略的资深旅行规划师,主打性价比高、人少景美的路线。
            请帮我生成一份{days}天的{destination}旅行攻略,适配{travelType}出行,预算{budget}元左右。
            """;

反例(模糊无角色)

    String template = """
            帮我写个{destination}的旅行攻略,玩{days}天,花{budget}元。
            """;

效果差异

  • 正例:AI 会以 “小众旅行规划师” 的视角,优先推荐非热门景点,符合 “性价比 + 人少” 的核心需求;
  • 反例:AI 可能输出通用的热门景点攻略,忽略 “小众、性价比” 的核心诉求。

2、技巧 2:拆解 “约束条件”,越具体越精准

核心逻辑:把需求拆成 可量化、可执行的约束条件(比如字数、格式、内容维度),避免模糊的 “要详细一点”“要好看一点”。

正确示例(朋友圈文案)

    String template = """
            请帮我写一条适合朋友圈的文案,要求:
            1. 场景:{scene},风格:{style};
            2. 字数控制在{wordLimit}字以内(严格遵守);
            3. 必须带1-2个贴合场景的emoji,不堆砌;
            4. 语言自然不矫情,提供3个不同版本。
            """;

反例(模糊约束)

    String template = """
            帮我写个{scene}的朋友圈文案,要{style}风格,字数别太长,带点emoji。
            """;

效果差异

  • 正例:AI 会严格控制字数,精准匹配 emoji,且输出 3 个版本供选择;
  • 反例:AI 可能输出超长文案,emoji 堆砌(比如🍉🍹☀️🥳),且只有 1 个版本。

3**、技巧 3:指定 “输出格式”,适配业务场景**

核心逻辑:明确要求 AI 按固定格式输出(比如 Markdown、列表、JSON、分点),避免后续解析麻烦。

正确示例(家常菜谱)

    String template = """
            请以{ingredients}为主要食材,创作一道家常菜,要求按以下格式输出:
            ### 【菜名】
            1. 食材清单(含用量):
               - 主料:XXX(XX克)
               - 辅料:XXX(XX克)
            2. 烹饪步骤(分点,每步不超过2句话):
               ① XXX
               ② XXX
            3. 烹饪小技巧(1-2条):
               - XXX
            4. 营养亮点:XXX
            """;

反例(无格式要求)

    String template = """
            用{ingredients}做一道菜,写清楚食材和步骤。
            """;

效果差异

  • 正例:AI 输出结构化的菜谱,可直接复制到 APP / 文档,甚至后续能解析成 Java 对象;
  • 反例:AI 输出大段纯文本,食材和步骤混在一起,阅读和复用成本高。

4、技巧 4:加入 “否定条件”,规避无效输出

核心逻辑:明确告诉 AI “不要做什么”,比如 “不要官方话术”“不要复杂步骤”,进一步缩小输出范围。

正确示例(旅行攻略)

    String template = """
            请生成{days}天{destination}旅行攻略,要求:
            1. 推荐的景点不要包含XX(热门网红打卡点);
            2. 住宿推荐不要推荐五星级酒店(超预算);
            3. 语言不要用官方旅游宣传话术,口语化像朋友推荐。
            """;

反例(无否定条件)

    String template = """
            生成{days}天{destination}旅行攻略,推荐便宜的住宿和小众景点。
            """;

效果差异

  • 正例:AI 会精准避开网红点、五星级酒店,输出符合预算和 “小众” 需求的内容;
  • 反例:AI 可能仍会夹杂热门景点,或推荐高端民宿,偏离 “便宜” 的核心。

5、技巧 5:使用 “示例引导”,对齐输出风格

核心逻辑:如果对输出风格有明确要求(比如 “沙雕风”“治愈风”),直接给 1 个示例,AI 会更贴合预期。

正确示例(朋友圈文案)

    String template = """
            请帮我写{scene}的朋友圈文案,风格:{style},要求:
            1. 字数{wordLimit}字以内,带1-2个emoji;
            2. 参考示例风格:
               【沙雕风撸猫示例】:我家逆子今天终于肯让摸了😼,感动到想给它开罐罐!
            3. 提供3个版本,不要和示例重复。
            """;

反例(无示例)

    String template = """
            写{scene}的沙雕风朋友圈文案,{wordLimit}字以内,带emoji。
            """;

效果差异

  • 正例:AI 输出的文案会贴合 “沙雕 + 口语化” 的风格,比如 “撸猫五分钟,挨揍两小时😾,这届猫主子太难伺候了”;
  • 反例:AI 可能输出生硬的文案,比如 “今日撸猫,心情愉悦😺,沙雕的一天”,风格不符。

6、技巧 6:分层 “优先级”,突出核心需

核心逻辑:当需求有多个维度时,明确 “优先级”(比如 “性价比优先 > 景点数量”),避免 AI 顾此失彼。

正确示例(旅行攻略)

    String template = """
            生成{days}天{destination}旅行攻略,预算{budget}元,要求:
            1. 优先级:性价比>景点数量>打卡效率;
            2. 每日行程控制在2-3个景点,总花费不超预算;
            3. 优先推荐免费/低价景点,美食人均不超50元。
            """;

反例(无优先级)

    String template = """
            生成{days}天{destination}旅行攻略,要便宜、景点多、玩得快。
            """;

效果差异

  • 正例:AI 会优先选择免费景点,控制美食成本,哪怕景点数量少,也保证不超预算;
  • 反例:AI 可能推荐大量景点,导致人均消费超标,偏离 “便宜” 的核心。

7、技巧 7:适配 “模型特性”,优化 Prompt 长度

核心逻辑:

不同模型(比如 qwen-turbo/qwen-plus)对 Prompt 长度的支持不同,遵循 “短而精” 原则:

  1. 轻量模型(qwen-turbo):Prompt 控制在 500 字以内,核心信息前置;
  2. 高性能模型(qwen-plus):可适当增加细节,但避免冗余。

优化示例(轻量模型适配)

    // 核心信息前置,删减冗余描述
    String template = """
            角色:小众旅行规划师(性价比优先)
            需求:{days}天{destination}@{travelType},预算{budget}元
            要求:
            1. 每日2-3个小众景点(免费/低价);
            2. 美食人均≤50元,住宿≤200元/晚;
            3. 口语化,分天数输出。
            """;

实战:结合技巧的完整 Prompt 模板(旅行攻略)

    public Flux<String> generateTravelGuide(TravelGuide travelGuide) {
        // 融合所有核心技巧:角色+约束+格式+否定+优先级
        String template = """
            你是一位擅长小众路线的旅行规划师,主打「性价比高、人少景美」的非热门路线。
            请按以下要求生成{days}天{destination}旅行攻略({travelType}出行,预算{budget}元):
            ### 核心要求(优先级:性价比>体验>景点数量)
            1. 输出格式:按天数分模块,每个模块包含「景点+美食+住宿」,用Markdown列表展示;
            2. 景点约束:
            - 不推荐XX/XX等网红打卡点;
            - 每个景点标注「开放时间+门票+交通方式」;
            3. 成本约束:
            - 美食人均≤50元,附具体店铺名称;
            - 住宿≤200元/晚,按区域推荐(比如古城/郊区);
            4. 额外要求:
            - 补充1个当地小众玩法(比如菜市场逛吃);
            - 语言口语化,像朋友推荐,不要官方话术;
            - 严格控制总花费不超预算。
            5. 字数200以内。
            """;
            PromptTemplate promptTemplate = new PromptTemplate(template);
        Prompt prompt = promptTemplate.create(Map.of(
            "destination", travelGuide.getDestination(),
            "days", travelGuide.getDays(),
            "travelType", travelGuide.getTravelType(),
            "budget", travelGuide.getBudget()
        ));
        return handleStreamingChat(chatClient.prompt(prompt).stream());
    }

总结:Prompt 编写黄金公式

角色定位 + 清晰目标 + 量化约束 + 输出格式 + 否定条件 + 优先级
  • 角色定位:让 AI 知道 “我是谁”;
  • 清晰目标:让 AI 知道 “要做什么”;
  • 量化约束:让 AI 知道 “要做到什么程度”;
  • 输出格式:让 AI 知道 “按什么格式输出”;
  • 否定条件:让 AI 知道 “不要做什么”;
  • 优先级:让 AI 知道 “先满足什么”。

结合这个公式,无论是生活化场景(菜谱、文案),还是技术场景(代码生成、接口文档),都能写出精准的 Prompt,让 Spring AI 的交互效果翻倍。

Response

Response(响应)是 AI 模型接收到 Prompt 指令后返回的结果集合,也是 Spring AI 封装大模型输出的核心数据结构 , 它并非简单的文本字符串,而是包含核心响应内容、元数据、对话上下文、状态信息的完整对象,是开发者获取 AI 输出、解析交互结果的唯一入口。
Alt

在 Spring AI 体系中,最核心的响应类型是 ChatResponse(对应对话场景),所有通过 ChatClient 发起的调用(同步 / 流式)最终都会落地为 ChatResponse 或其衍生形式,其核心价值在于:将大模型返回的原始 JSON 响应(如 OpenAI / 阿里百炼的 API 响应)封装为标准化、易解析的 Java 对象,屏蔽不同模型的响应格式差异,让开发者无需关注底层协议细节,直接通过统一 API 提取所需信息。

Response 的核心构成

以 ChatResponse 为例,一个完整的 ChatResponse 包含三大核心部分,覆盖从 “内容” 到 “元信息” 的全维度数据:

1**. 核心响应内容(最常用)**

即 AI 生成的文本结果,也是开发者最常提取的部分,可通过 content() 快速获取:

    // 简化调用:直接提取核心文本
    String text = chatClient.prompt("帮我写一句下午茶文案").call().content();
    
    // 完整获取 ChatResponse 后提取内容
    ChatResponse response = chatClient.prompt("帮我写一句下午茶文案").call().chatResponse();
    String text = response.getResult().getOutput().getContent();

2. 元数据(辅助信息)

包含 AI 生成响应的关键技术参数,用于监控、调试或优化交互效果,核心包括:

  • token 统计:生成响应消耗的令牌数(prompt_tokens/response_tokens/total_tokens),可用于成本核算;
  • 模型信息:生成响应的具体模型(如 qwen-plus/qwen-turbo);
  • 完成状态:响应是否完整生成(如 finish_reason,值为 stop 表示正常完成,length 表示因令牌数限制截断);
  • 响应 ID:唯一标识单次交互的 ID,用于问题排查。

示例提取元数据:

    ChatResponse response = chatClient.prompt("生成3天成都旅行攻略").call().chatResponse();
    // 获取 token 统计
    TokenUsage tokenUsage = response.getMetadata().get(TokenUsage.METADATA_KEY, TokenUsage.class);
    log.info("消耗提示词令牌:{},生成响应令牌:{}", tokenUsage.getPromptTokens(), tokenUsage.getCompletionTokens());
    // 获取完成状态
    String finishReason = response.getResult().getMetadata().get("finish_reason", String.class);

3. 对话上下文标识

包含 conversationId(会话 ID)、role(角色标识,如 assistant 表示 AI 响应)等信息,用于多轮对话中关联上下文,配合 ChatMemory 实现连续交互。

Response 的两种核心形态

同步 vs 流式

Spring AI 针对不同交互场景,将 Response 封装为两种易用形态,适配不同业务需求:

  1. 同步响应(完整结果)

适用于 “一次性获取完整结果” 的场景(如生成菜谱、旅行攻略),通过 call() 触发,返回完整的 ChatResponse:

    // 同步获取完整响应(包含所有内容+元数据)
    ChatResponse fullResponse = chatClient.prompt("用土豆牛肉做微辣家常菜").call().chatResponse();
    // 提取核心文本
    String recipe = fullResponse.getResult().getOutput().getContent();
    // 提取元数据
    String model = fullResponse.getMetadata().get("model", String.class);
  1. 流式响应(逐段结果)

适用于 “实时展示生成过程” 的场景(如聊天机器人打字机效果、长文本生成),通过 stream() 触发,返回 Flux(响应式流),每一个流元素都是一段碎片化的响应:

    // 流式获取响应(逐字/逐句返回)
    Flux<String> streamContent = chatClient.prompt("讲一个50字的暖心小故事")
                                          .stream()
                                          .content(); // 直接提取流式文本
    // 消费流式响应
    streamContent.subscribe(
        chunk -> log.info("收到流式片段:{}", chunk), // 每收到一段内容触发
        error -> log.error("流式响应异常:{}", error), // 异常处理
        () -> log.info("流式响应完成") // 完成回调
    );

流式响应的核心特点是 “边生成、边返回”,每个片段都是一个迷你 ChatResponse,最终拼接为完整文本,适合前端实时渲染场景。

与 Prompt 的关联逻辑

Prompt 是 “输入指令”,Response 是 “输出结果”,二者形成完整的交互闭环:

  1. 开发者通过 Prompt 传递意图(如 “生成 3 人份土豆牛肉菜谱”);
  2. AI 模型基于 Prompt 生成原始响应(包含文本 + 元数据);
  3. Spring AI 将原始响应封装为 ChatResponse;
  4. 开发者从 ChatResponse 中提取核心内容、元数据,完成业务逻辑处理。
    Alt

AiException

AiException 是 Spring AI 框架为大模型交互场景量身定制的核心异常体系,是所有 AI 相关错误的统一封装载体 —— 它并非单一异常类,而是以 AiException 为父类,涵盖模型调用失败、参数非法、响应解析错误、限流超时等细分场景的异常家族,核心作用是将底层复杂的错误(如 HTTP 连接失败、模型 API 报错、JSON 解析异常)转化为开发者可理解、可处理的标准化异常,屏蔽不同 AI 模型的错误格式差异。
Alt

AiException 的核心定位与价值

在 Spring AI 调用流程中(ChatClient → ChatModel → 大模型 API),任何环节的错误都会被封装为 AiException 及其子类,而非抛出原生的 RestClientException、JsonParseException 等底层异常,其核心价值体现在:

  1. 统一异常范式:无论对接 OpenAI、阿里百炼还是百度文心一言,所有 AI 交互错误都以 AiException 体系抛出,开发者无需针对不同模型适配不同异常类型;
  2. 错误语义化:异常信息中包含 “错误类型 + 业务上下文”(如 “模型调用失败:API 密钥无效 | 会话 ID:user001”),而非单纯的 “连接超时”,便于快速定位问题;
  3. 可定制化处理:支持捕获细分异常(如 AiResponseException、AiConnectionException),实现差异化的错误处理逻辑(如密钥错误提示用户更换、限流错误触发重试)。
AiException 的核心分类与典型场景

Spring AI 的 AiException 体系包含多个细分子类,覆盖 AI 交互全链路错误,以下是开发中最常见的类型及触发场景:

  1. 基础父类:AiException

所有 AI 相关异常的根类,可作为 “兜底捕获” 的异常类型,适用于无需区分具体错误场景的通用处理(如统一返回 “AI 服务暂不可用”)。

  1. 细分核心子类(高频场景)
    在这里插入图片描述

AiException 的捕获与处理 【待定】

在 Spring AI 开发中,通常通过「全局异常处理器 + 局部 try-catch」结合的方式处理 AiException,以下是贴合实际业务的示例:

  1. 局部捕获(针对性处理)

适用于需要对特定 AI 调用做差异化处理的场景(如旅行攻略生成失败时提示用户简化需求):

    @Service
    public class PromptTemplateService {
        private final ChatClient chatClient;
    
        // 生成旅行攻略:捕获细分异常并差异化处理
        public String generateTravelGuide(String destination, Integer days, String travelType, String budget) {
            String template = "你是资深旅行规划师...";
            PromptTemplate promptTemplate = new PromptTemplate(template);
            Prompt prompt = promptTemplate.create(Map.of("destination", destination, "days", days, "travelType", travelType, "budget", budget));
            
            try {
                return chatClient.prompt(prompt).call().content();
            } catch (AiAuthenticationException e) {
                // 鉴权错误:提示管理员检查 API 密钥
                log.error("AI 鉴权失败:{}", e.getMessage(), e);
                return "服务异常:AI 密钥无效,请联系管理员处理";
            } catch (AiResourceExhaustedException e) {
                // 限流/配额不足:提示用户稍后重试
                log.error("AI 资源耗尽:{}", e.getMessage(), e);
                return "当前调用量过大,请5分钟后重试";
            } catch (AiInvalidArgumentException e) {
                // 参数错误:提示用户修正输入(如天数为负数、预算非数字)
                log.error("AI 参数错误:{}", e.getMessage(), e);
                return "输入参数无效:" + e.getMessage() + ",请检查后重新提交";
            } catch (AiException e) {
                // 兜底捕获所有 AI 异常
                log.error("AI 交互失败:{}", e.getMessage(), e);
                return "旅行攻略生成失败,请稍后重试";
            }
        }
    }

2. 全局异常处理器(统一返回格式)

适用于整个项目的 AI 异常统一封装,避免重复编写 try-catch 逻辑(结合 Spring Boot 全局异常处理):

    @RestControllerAdvice
    public class AiGlobalExceptionHandler {
    
        // 捕获所有 AiException 及其子类
        @ExceptionHandler(AiException.class)
        public ResponseEntity<Result<String>> handleAiException(AiException e) {
            // 构建标准化错误响应
            Result<String> errorResult = Result.<String>builder()
                    .code(500)
                    .msg("AI 服务异常:" + e.getMessage())
                    .data(null)
                    .build();
            
            // 针对细分异常调整响应码(如鉴权错误返回 401,参数错误返回 400)
            if (e instanceof AiAuthenticationException) {
                return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(errorResult);
            } else if (e instanceof AiInvalidArgumentException) {
                return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResult);
            } else {
                return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResult);
            }
        }
    
        // 兜底捕获所有未处理异常
        @ExceptionHandler(Exception.class)
        public ResponseEntity<Result<String>> handleException(Exception e) {
            Result<String> errorResult = Result.<String>builder()
                    .code(500)
                    .msg("系统异常:" + e.getMessage())
                    .data(null)
                    .build();
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResult);
        }
    }
    // 配套的统一响应体
    @Data
    @Builder
    public class Result<T> {
        private Integer code;
        private String msg;
        private T data;
    }

会话记忆

ChatMemory(对话记忆)是 Spring AI 为多轮对话场景设计的核心组件,本质是 “对话消息的持久化 / 缓存管理器”—— 它自动存储、维护、检索历史 Message 集合,让 AI 能够 “记住” 之前的对话内容,实现连续、上下文关联的交互,而非每次调用都 “从零开始”。

在无 ChatMemory 的情况下,AI 每次调用都是独立的(“失忆式交互”):比如先问 “推荐大理小众景点”,再问 “这些景点附近有便宜住宿吗?”,AI 无法关联前序问题;而接入 ChatMemory 后,它会自动将历史消息(用户提问 + AI 回复)传入新的 Prompt,让 AI 理解上下文,实现 “有记忆的对话”。

基于Redis的会话记忆

导入依赖

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>

Redis配置类

    import com.fasterxml.jackson.annotation.JsonAutoDetect;
    import com.fasterxml.jackson.annotation.PropertyAccessor;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import org.springframework.cache.annotation.CachingConfigurerSupport;
    import org.springframework.cache.annotation.EnableCaching;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.data.redis.connection.RedisConnectionFactory;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
    import org.springframework.data.redis.serializer.StringRedisSerializer;
    
    @EnableCaching
    @Configuration
    public class RedisConfiguration extends CachingConfigurerSupport {
    
        @Bean
        public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
            RedisTemplate<String, Object> template  = new RedisTemplate<>();
            //设置工厂
            template.setConnectionFactory(factory);
            //创建对象映射
            ObjectMapper mapper = new ObjectMapper();
            //使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值
            //(默认使用JDK的序列化方式)
            Jackson2JsonRedisSerializer jackson2JsonRedisSerializer =
                    new Jackson2JsonRedisSerializer<>(mapper, Object.class);
            //指定要序列化的域,field,get和set,以及修饰符范围,ANY是都有包括private和public
            mapper.setVisibility(PropertyAccessor.ALL,JsonAutoDetect.Visibility.ANY);
            //指定序列化输入的类型,类必须是非final修饰的,final修饰的类,比如String,Integer
            //等会抛出异常
            mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
            //
            //字符串序列化器
            StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
            //key采用String的序列化方式
            template.setKeySerializer(stringRedisSerializer);
            //hash的key也采用String的方式
            template.setHashKeySerializer(stringRedisSerializer);
            //value采用jackson的方式
            template.setValueSerializer(jackson2JsonRedisSerializer);
            //hash的value也采用Jackson
            template.setHashValueSerializer(jackson2JsonRedisSerializer);
    
            template.afterPropertiesSet();
            return template;
        }
    }

ChatMemory实现类

    import com.fasterxml.jackson.core.type.TypeReference;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import lombok.Data;
    import lombok.RequiredArgsConstructor;
    import org.springframework.ai.chat.messages.AssistantMessage;
    import org.springframework.ai.chat.messages.Message;
    import org.springframework.ai.chat.messages.MessageType;
    import org.springframework.ai.chat.messages.UserMessage;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.stereotype.Component;
    
    import java.time.Duration;
    import java.util.ArrayList;
    import java.util.List;
    import java.util.Objects;
    import java.util.stream.Collectors;
    
    @Component
    @RequiredArgsConstructor
    public class RedisChatMemory {
        private final RedisTemplate<String, Object> redisTemplate;
        private final ObjectMapper objectMapper;
        // 统一角色常量为大写(匹配MessageType)
        public static final String ROLE_USER = MessageType.USER.name(); // "USER"
        public static final String ROLE_ASSISTANT = MessageType.ASSISTANT.name(); // "ASSISTANT"
        public static final int DEFAULT_LAST_N = 20;
    
        /**
         * 添加消息
         */
        public void add(String conversationId, String message, String role) {
            // 1. 角色校验(使用统一的常量)
            if (!ROLE_USER.equals(role) && !ROLE_ASSISTANT.equals(role)) {
                throw new IllegalArgumentException("角色必须是'" + ROLE_USER + "'或'" + ROLE_ASSISTANT + "'");
            }
            // 2. 获取【完整】历史数据(新增方法,不截断)
            List<Message> fullHistory = getFullHistory(conversationId);
            // 3. 追加新消息
            fullHistory.add(ROLE_USER.equals(role) ? new UserMessage(message) : new AssistantMessage(message));
            // 4. 转换为DTO(
            List<RedisChatMsgItem> RedisChatMsgItemS = fullHistory.stream()
                    .map(msg -> new RedisChatMsgItem(msg.getMessageType().name(), msg.getText()))
                    .collect(Collectors.toList());
            // 5. 写入Redis(覆盖为完整的新历史)
            try {
                redisTemplate.opsForValue().set(
                        conversationId,
                        objectMapper.writeValueAsString(RedisChatMsgItemS),
                        Duration.ofDays(1)
                );
            } catch (Exception e) {
                throw new RuntimeException("存储聊天历史失败", e); // 不建议只打印,需抛出异常
            }
        }
    
        /**
         * 读取完整的历史消息
         */
        private List<Message> getFullHistory(String conversationId) {
            String messages = (String) redisTemplate.opsForValue().get(conversationId);
            if (Objects.isNull(messages)) {
                return new ArrayList<>();
            }
            try {
                List<RedisChatMsgItem> RedisChatMsgItemS = objectMapper.readValue(messages, new TypeReference<>() {});
                return RedisChatMsgItemS.stream()
                        .map(dto -> {
                            if (ROLE_USER.equals(dto.getRole())) {
                                return new UserMessage(dto.getContent());
                            } else if (ROLE_ASSISTANT.equals(dto.getRole())) {
                                return new AssistantMessage(dto.getContent());
                            } else {
                                throw new IllegalArgumentException("未知角色:" + dto.getRole());
                            }
                        })
                        .collect(Collectors.toList());
            } catch (Exception e) {
                throw new RuntimeException("读取聊天历史失败", e);
            }
        }
    
        /**
         * 返回截断后的历史(仅用于展示,不影响追加逻辑)
         */
        public List<Message> get(String conversationId, int lastN) {
            List<Message> fullHistory = getFullHistory(conversationId);
            // 仅在查询时截断,不修改完整历史
            if (fullHistory.size() > lastN) {
                return fullHistory.subList(fullHistory.size() - lastN, fullHistory.size())
                        .stream()
                        .collect(Collectors.toList()); // 转换为普通List,避免SubList视图问题
            }
            return new ArrayList<>(fullHistory); // 返回新List,防止外部修改
        }
    
        public void clear(String conversationId) {
            redisTemplate.delete(conversationId);
        }
    
        public List<Message> get(String conversationId) {
            return get(conversationId, DEFAULT_LAST_N);
        }
    
        // 内部类:Redis存储的消息项(角色+内容)
        @Data
        public static class RedisChatMsgItem {
            private String role;
            private String content;
    
            public RedisChatMsgItem() {}
    
            public RedisChatMsgItem(String role, String content) {
                this.role = role;
                this.content = content;
            }
        }
    }

聊天参数对象:封装会话id

    @Data
    public class SessionMessage {
        private String conversationId;
        private String message;
    }

聊天服务实现

    // 注入redis聊天对象
    private final RedisChatMemory redisChatMemory;
    
    public Flux<String> chatStream(SessionMessage sessionMessage) {
        log.info("发送流式消息到阿里百炼: {}", sessionMessage.getMessage());
        if (sessionMessage.getConversationId() == null) {
            sessionMessage.setConversationId("default");
        }
    
        // 步骤1:读取Redis中的历史消息
        List<Message> historyMessages = redisChatMemory.get(sessionMessage.getConversationId(), RedisChatMemory.DEFAULT_LAST_N);
    
        // 步骤2:构建完整的消息列表(历史 + 本次新用户消息)
        List<Message> allMessages = new ArrayList<>(historyMessages);
        allMessages.add(new UserMessage(sessionMessage.getMessage())); // 核心:把本次新消息加入列表
    
        // 步骤3:将本次新消息存入Redis(持久化)
        redisChatMemory.add(sessionMessage.getConversationId(), sessionMessage.getMessage(), MessageType.USER.name());
    
        // 步骤4:调用AI时传入完整的消息列表(历史+新消息)
        ChatClient.StreamResponseSpec streamResponseSpec = chatClient.prompt()
            .user(sessionMessage.getMessage()) // 满足ChatClient校验
            .messages(allMessages) // 核心:传入包含本次新消息的完整列表
            .stream();
    
        return handleStreamingChat(sessionMessage.getConversationId(), streamResponseSpec);
    }

处理流式对话

    // 提取的公共方法 - 处理流式对话的核心逻辑
    private Flux<String> handleStreamingChat(String conversationId, ChatClient.StreamResponseSpec streamResponseSpec) {
        // 临时收集AI回复片段
        List<String> aiResponseSegments = new ArrayList<>();
    
        Flux<String> contentFlux = streamResponseSpec.content()
            .filter(content -> content != null && !content.trim().isEmpty())
            .doOnNext(segment -> aiResponseSegments.add(segment)) // 收集片段
            .onErrorResume(e -> {
                log.error("流式对话异常", e);
                return Flux.just("对话出错:" + e.getMessage());
            });
    
        // 最后拼接完整回复存入Redis(仅存一次)
        return contentFlux.concatWith(Mono.fromRunnable(() -> {
            if (!aiResponseSegments.isEmpty()) {
                String fullResponse = String.join("", aiResponseSegments);
                redisChatMemory.add(conversationId, fullResponse, RedisChatMemory.ROLE_ASSISTANT);
            }
        }))
            .concatWith(Mono.just("[DONE]"));
    }
消息持久化

导入依赖

    <mybatis-plus.version>3.5.9</mybatis-plus.version>
    <mysql.version>8.0.33</mysql.version>
    
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
        <version>${mybatis-plus.version}</version>
    </dependency>
    
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>${mysql.version}</version>
    </dependency>

数据库、mybatis-plus配置

    spring:
      datasource:
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/mall_116?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
        username: root
        password: root1234
    
    mybatis-plus:
      mapper-locations: classpath:mapper/*.xml
      configuration:
        map-underscore-to-camel-case: true # 开启下划线转驼峰命名

建表sql

    create table chat_record(
    	id bigint primary key,
    	conversation_id varchar(64),
    	role varchar(32),
    	content text,
    	create_time datetime
    );

实体类

    @Data
    @TableName("chat_record")
    public class ChatRecord {
        @TableId(type = IdType.ASSIGN_ID)
        private Long id; // 主键
        private String conversationId; // 会话ID
        private String role; // 角色:USER/ASSISTANT
        private String content; // 消息内容
        private LocalDateTime createTime; // 创建时间
    }

线程池异步

    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.scheduling.annotation.EnableAsync;
    import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
    import java.util.concurrent.ThreadPoolExecutor;
    
    @Configuration
    @EnableAsync // 必须开启异步注解支持
    public class AsyncThreadPoolConfig  {
    
        /**
         * 对话记录落库专用线程池(指定bean名称,供@Async引用)
         */
        @Bean(name = "chatTaskExecutor") // 命名为chatTaskExecutor(WebMvc默认优先识别这个名称)
        public ThreadPoolTaskExecutor chatTaskExecutor() {
            ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
            // 核心线程数(根据服务器配置调整,比如4核8G设为8)
            // 获取到当前服务器CPU核心数
            int cpus = Runtime.getRuntime().availableProcessors();
            executor.setCorePoolSize(cpus);
            // 最大线程数(核心线程忙不过来时,最多扩容到这个数)
            executor.setMaxPoolSize(cpus * 2);
            // 队列容量(核心线程满了,任务先入队列,避免直接创建新线程)
            executor.setQueueCapacity(1000);
            // 线程空闲时间(超过60秒空闲的非核心线程会被销毁)
            executor.setKeepAliveSeconds(60);
            // 线程命名前缀(便于日志排查问题)
            executor.setThreadNamePrefix("chat-async-");
            // 拒绝策略(队列满+最大线程数满时,让提交任务的线程执行,避免任务丢失)
            executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
            // 初始化线程池(必须调用,否则线程池不生效)
            executor.initialize();
            return executor;
        }
    }

配置到mvc

    import lombok.RequiredArgsConstructor;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
    import org.springframework.web.servlet.config.annotation.AsyncSupportConfigurer;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
    
    /**
     * Spring MVC 异步请求配置(解决默认SimpleAsyncTaskExecutor告警)
     */
    @Configuration
    @RequiredArgsConstructor
    public class WebMvcAsyncConfig implements WebMvcConfigurer {
    
        // 注入自定义的线程池(指定bean名称)
        private final ThreadPoolTaskExecutor chatTaskExecutor;
    
        /**
         * 配置MVC异步请求的线程池
         */
        @Override
        public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
            // 1. 指定MVC异步请求使用的线程池
            configurer.setTaskExecutor(chatTaskExecutor);
            // 2. 可选:设置异步请求超时时间(默认30秒,根据业务调整)
            configurer.setDefaultTimeout(60 * 1000); // 60秒
        }
    }

mapper

    @Mapper
    public interface ChatRecordMapper extends BaseMapper<ChatRecord> {
    }

service

    public interface ChatRecordService extends IService<ChatRecord> {
        public void asyncSaveChatRecord(String conversationId, String role, String content);
    }

实现类

    @Slf4j
    @Service
    @RequiredArgsConstructor
    public class ChatRecordServiceImpl extends ServiceImpl<ChatRecordMapper, ChatRecord> implements ChatRecordService {
        private final ChatRecordMapper chatRecordMapper;
    
        /**
         * 异步保存对话记录到数据库(核心:@Async指定线程池)
         * @param conversationId 会话ID
         * @param role 角色(USER/ASSISTANT)
         * @param content 消息内容
         */
        @Override
        @Async("chatTaskExecutor") // 绑定自定义线程池
        public void asyncSaveChatRecord(String conversationId, String role, String content) {
            try {
                ChatRecord record = new ChatRecord();
                record.setConversationId(conversationId);
                record.setRole(role);
                record.setContent(content);
                record.setCreateTime(LocalDateTime.now());
                chatRokenException e) {
                log.error("异步落库失败 线程id:{} | 会话ID:{} | 角色:{}", Thread.currentThread().getId(), conversationId, role, e);
            }
        }
    }

修改聊天服务,注入ChatRecordService,新增落库代码调用

    private final ChatRecordService chatRecordService;
    
    @Override
    public Flux<String> chatStream(SessionMessage sessionMessage) {
        log.info("发送流式消息到阿里百炼: {}", sessionMessage.getMessage());
        if (sessionMessage.getConversationId() == null) {
            sessionMessage.setConversationId("default");
        }
        // 步骤1:读取Redis中的历史消息
        List<Message> historyMessages = redisChatMemory.get(sessionMessage.getConversationId(), RedisChatMemory.DEFAULT_LAST_N);
    
        // 步骤2:构建完整的消息列表(历史 + 本次新用户消息)
        List<Message> allMessages = new ArrayList<>(historyMessages);
        allMessages.add(new UserMessage(sessionMessage.getMessage())); // 核心:把本次新消息加入列表
    
        // 步骤3:将本次新消息存入Redis(持久化)
        redisChatMemory.add(sessionMessage.getConversationId(), sessionMessage.getMessage(), MessageType.USER.name());
    
        // 持久化到数据库
        chatRecordService.asyncSaveChatRecord(sessionMessage.getConversationId(), RedisChatMemory.ROLE_USER, sessionMessage.getMessage());
    
        // 步骤4:调用AI时传入完整的消息列表(历史+新消息)
        ChatClient.StreamResponseSpec streamResponseSpec = chatClient.prompt()
            .user(sessionMessage.getMessage()) // 满足ChatClient校验
            .messages(allMessages) // 核心:传入包含本次新消息的完整列表
            .stream();
    
        return handleStreamingChat(sessionMessage.getConversationId(), streamResponseSpec);
    }
    
    
    // 提取的公共方法 - 处理流式对话的核心逻辑
    private Flux<String> handleStreamingChat(String conversationId, ChatClient.StreamResponseSpec streamResponseSpec) {
        // 临时收集AI回复片段
        List<String> aiResponseSegments = new ArrayList<>();
    
        Flux<String> contentFlux = streamResponseSpec.content()
            .filter(content -> content != null && !content.trim().isEmpty())
            .doOnNext(segment -> aiResponseSegments.add(segment)) // 收集片段
            .onErrorResume(e -> {
                log.error("流式对话异常", e);
                return Flux.just("对话出错:" + e.getMessage());
            });
    
        // 最后拼接完整回复存入Redis(仅存一次)
        return contentFlux.concatWith(Mono.fromRunnable(() -> {
            if (!aiResponseSegments.isEmpty()) {
                String fullResponse = String.join("", aiResponseSegments);
                redisChatMemory.add(conversationId, fullResponse, RedisChatMemory.ROLE_ASSISTANT);
                // 持久化到数据库
                chatRecordService.asyncSaveChatRecord(conversationId, RedisChatMemory.ROLE_ASSISTANT, fullResponse);
            }
        }))
            .concatWith(Mono.just("[DONE]"));
    }

测试

问题1:介绍一下成都,50字以内
Alt

问题二:有哪些美食,50字以内
Alt

问题三:推荐5家火锅,50字以内
Alt

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐