SpringAI+SSE实现类似GPT功能

574 阅读2分钟

SpringAI+SSE实现类似GPT功能

SpringAI介绍

官网:docs.spring.io/spring-ai/r…

  • Spring AI项目旨在简化包含人工智能功能的应用程序的开发,避免不必要的复杂性。
  • Spring AI 提供了抽象,作为开发 AI 应用程序的基础。这些抽象有多种实现,可以通过最少的代码更改轻松进行组件交换。

关于ChatClient和ChatModel:

  • ChatClient是SpringAI 0.8.0版本的概念,到1.0.0版本变成了ChatModel,但同时保留了ChatClient,
  • ChatClient底层还是调用ChatModel,ChatClient支持Fluent Api,ChatModel不支持。两者都是表示某个模型,具体是什么模型,需要看配置。

可以理解为ChatClient是在ChatModel的基础上进一步封装,所以我们尽量使用ChatClient即可

模型接入

统一管理各大厂商的依赖:

 <properties>
   <java.version>21</java.version>
   <spring-ai.version>1.0.0-M2</spring-ai.version>
 </properties>
 ​
 <repositories>
   <repository>
     <id>central</id>
     <url>https://repo.maven.apache.org/maven2/</url>
     <snapshots>
       <enabled>false</enabled>
     </snapshots>
     <releases>
       <enabled>true</enabled>
     </releases>
   </repository>
   <repository>
     <id>spring-milestones</id>
     <name>Spring Milestones</name>
     <url>https://repo.spring.io/milestone</url>
     <snapshots>
       <enabled>false</enabled>
     </snapshots>
   </repository>
   <repository>
     <id>spring-snapshots</id>
     <name>Spring Snapshots</name>
     <url>https://repo.spring.io/snapshot</url>
     <releases>
       <enabled>false</enabled>
     </releases>
   </repository>
 </repositories>
 <dependencyManagement>
   <dependencies>
     <dependency>
       <groupId>org.springframework.ai</groupId>
       <artifactId>spring-ai-bom</artifactId>
       <version>1.0.0-SNAPSHOT</version>
       <type>pom</type>
       <scope>import</scope>
     </dependency>
   </dependencies>
 </dependencyManagement>

统一配置使用

比如我现在又有OpenAI的,又有通义千问的,那么可以去定义多个Client,按照名称来注入,这样使用的时候,也是按照名称去注入对应的Client

 package com.yunfei.ai.controller.config;
 ​
 import jakarta.annotation.Resource;
 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;
 ​
 /**
  * @author houyunfei
  */
 @Configuration
 public class ChatConfig {
 ​
     @Resource(name = "dashscopeChatModel")
     private ChatModel dashscopeChatModel;
 ​
 ​
     @Resource(name = "openAiChatModel")
     private ChatModel openAiChatModel;
 ​
     @Bean("openAiChatClient")
     public ChatClient chatClient() {
         return ChatClient.create(openAiChatModel);
     }
 ​
     @Bean("dashscopeChatClient")
     public ChatClient dashscopeChatClient() {
         return ChatClient.create(dashscopeChatModel);
     }
 }

整合OpenAI

官网:docs.spring.io/spring-ai/r…

免费Key网站:api.xty.app/register?af…

导入依赖
   <!--ai相关-->
   <dependency>
     <groupId>org.springframework.ai</groupId>
     <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
   </dependency>
导入配置
 spring.ai.openai.base-url=https://api.xty.app
 spring.ai.openai.api-key=xxx
 spring.ai.openai.chat.options.model=gpt-3.5-turbo
使用

使用的时候只需要注入,然后就可以使用里面提供的方法了

整合灵积(通义千问)

获取api-key:dashscope.console.aliyun.com/apiKey

文档:sca.aliyun.com/ai/get-star…

导入依赖
 <dependency>
   <groupId>com.alibaba.cloud.ai</groupId>
   <artifactId>spring-ai-alibaba-starter</artifactId>
   <version>${spring-ai.version}</version>
 </dependency>
导入配置
 spring.ai.dashscope.base-url=https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions
 spring.ai.dashscope.api-key=ccc
 spring.ai.dashscope.chat.options.model=qwen-long

实现聊天功能

阻塞式实现

构建一个Prompt,然后调用call即可实现阻塞式等待拿到结果,content就是输出的内容。这种方式非常简单,但是会等待很久拿不到结果

 @Resource(name = "dashscopeChatClient")
 private ChatClient chatClient;
 @GetMapping("/generate")
 public String generate(@RequestParam(value = "message", defaultValue = "给我讲个笑话") String message) {
     // 使用chatClient生成对话
     Prompt prompt = new Prompt(message);
     String res = chatClient.prompt(prompt).call().content();
     return res;
 }

也可以用下面的方式:

 chatClient.prompt().messages(new UserMessage(message)).call().content();

流式实现

 @GetMapping("/generateStream")
 public Flux<String> generateStream(@RequestParam(value = "message", defaultValue = "给我讲个笑话") String message) {
     // 使用chatClient生成对话
     Prompt prompt = new Prompt(message);
     Flux<String> stream = chatClient.prompt(prompt).stream().content();
     return stream;
 }
  • Spring WebFlux 会等到 Flux<String> 流的数据完全生成后,将其合并为一个响应,并作为一个完整的 HTTP 响应返回给前端(类似于JSON数组)。
  • 这种行为对前端来说,无法实现逐条接收和实时显示流数据。
  • 想要前端实现ChatGPT官网的效果,就需要使用SSE推流

SSE推流

前端可以使用EventSource来实现SSE,但是这种方式只支持GET请求,由于用户一次性可能会输入很多内容,GET请求的URL有限制,所以我们最好要去找到一种支持Post请求的库,可以使用微软的库:

如果要使用html直接使用,可以使用别人改过的:

后端实现:

     @Resource(name = "dashscopeChatClient")
     private ChatClient chatClient;
 /**
  * 使用SSE(Server-Sent Events)实现聊天
  *
  * @param chatRequest 用户请求
  * @return 聊天响应
  */
 @PostMapping(value = "/sse", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
 public Flux<ServerSentEvent<ChatResponse>> sse(@RequestBody ChatRequest chatRequest) {
     // 使用chatClient生成对话
     Prompt prompt = new Prompt(chatRequest.getUserText());
     Flux<ServerSentEvent<ChatResponse>> stream = chatClient
             .prompt(prompt)
             .stream()
             .chatResponse()
             .map(chatResponse -> {
                 ServerSentEvent<ChatResponse> sentEvent = ServerSentEvent
                         .builder(chatResponse)
                         .event("message")
                         .build();
                 return sentEvent;
             });
     return stream;
 }

前端实现index.html

 <!DOCTYPE html>
 <html lang="en">
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>ChatGPT</title>
     <script src="https://cdn.tailwindcss.com"></script>
     <script src="https://unpkg.com/@sentool/fetch-event-source/dist/index.min.js"></script>
 </head>
 <body class="bg-gray-100 text-gray-800">
 <div class="max-w-4xl mx-auto p-6">
     <h1 class="text-4xl font-bold text-center mb-4">聊天响应流</h1>
     <div class="bg-white p-4 rounded-lg shadow-md">
         <h2 class="text-2xl font-semibold mb-4">用户输入:</h2>
         <input id="userText" type="text" class="w-full p-2 border rounded-lg mb-4" placeholder="请输入您的问题"
                value="hello"/>
         <button id="startButton" class="w-full bg-blue-500 text-white p-2 rounded-lg">开始聊天</button>
         <h2 class="text-2xl font-semibold mt-6 mb-4">聊天响应:</h2>
         <div id="chatOutput" class="h-60 overflow-y-scroll bg-gray-50 p-4 rounded-lg border border-gray-200"></div>
     </div>
 </div>
 ​
 <script>
     document.getElementById('startButton').addEventListener('click', async () => {
         const {fetchEventSource} = FetchEventSource;
         const userText = document.getElementById('userText');
         const chatOutput = document.getElementById('chatOutput');
         chatOutput.innerHTML = ''; // 清空聊天区域
         fetchEventSource("/ai/sse", {
             method: 'POST',
             body: JSON.stringify({userText: userText.value}),
             headers: {
                 'Content-Type': 'application/json'
             },
             onopen(response) {
                 console.log('Request has been opened.');
             },
             onmessage(event) {
                 if (event.data) {
                     const data = event.data //这是一个json格式的字符串 包括metadata result results
                     console.log('data', data);
                     console.log("data.content", data.result.output.content);
                     chatOutput.innerHTML += `${data.result.output.content}`;
                 }
             },
             onerror(error) {
                 console.error('Error:', error);
             },
             done() {
                 console.log('Stream has completed.');
             },
         });
     });
 </script>
 </body>
 </html>

对话存储

  • 可以自定义一些系统提示词system
  • 使用ChatMemory可以附带一些聊天的历史记录,但是不易过多,会浪费很多token
 /**
  * 使用SSE(Server-Sent Events)实现聊天 (可以添加上下文)
  * @param chatRequest 用户请求
  * @return 聊天响应
  */
 @PostMapping(value = "/sse", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
 public Flux<ServerSentEvent<ChatResponse>> sse2(@RequestBody ChatRequest chatRequest) {
     ChatMemory chatMemory = new InMemoryChatMemory();
     String sessionId = "1"; // todo 从请求中获取
     int maxMessages = 10;
     MessageChatMemoryAdvisor advisor = new MessageChatMemoryAdvisor(chatMemory, sessionId, maxMessages);
     Flux<ServerSentEvent<ChatResponse>> stream = chatClient
             .prompt()
             .user(chatRequest.getUserText())
             .system(chatRequest.getSystemText())
             .advisors(advisor)
             .stream()
             .chatResponse()
             .map(chatResponse -> {
                 ServerSentEvent<ChatResponse> sentEvent = ServerSentEvent
                         .builder(chatResponse)
                         .event("message")
                         .build();
                 return sentEvent;
             });
     return stream;
 }

参考资料