Commit 99b6f240923b05357b5f31f37dd9907764d6012a

Authored by YunaiV
1 parent 041a00b2

【修复】AI:解决 spring-ai 与 webflux 集成时,SSE 存在乱序的问题

yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatMessageController.http
@@ -6,7 +6,7 @@ Authorization: {{token}} @@ -6,7 +6,7 @@ Authorization: {{token}}
6 6
7 { 7 {
8 "conversationId": "1781604279872581649", 8 "conversationId": "1781604279872581649",
9 - "content": "中国好看吗?" 9 + "content": "你是 OpenAI 么?"
10 } 10 }
11 11
12 12
@@ -16,11 +16,10 @@ Content-Type: application/json @@ -16,11 +16,10 @@ Content-Type: application/json
16 Authorization: {{token}} 16 Authorization: {{token}}
17 17
18 { 18 {
19 - "conversationId": "1781604279872581651",  
20 - "content": "苹果是什么颜色?" 19 + "conversationId": "1781604279872581690",
  20 + "content": "1+1=?"
21 } 21 }
22 22
23 -  
24 ### message list 23 ### message list
25 GET {{baseUrl}}/admin-api/ai/chat/message/list-by-conversation-id?conversationId=1781604279872581649 24 GET {{baseUrl}}/admin-api/ai/chat/message/list-by-conversation-id?conversationId=1781604279872581649
26 Authorization: {{token}} 25 Authorization: {{token}}
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/chat/AiChatConversationMapper.java
@@ -15,12 +15,11 @@ import java.util.List; @@ -15,12 +15,11 @@ import java.util.List;
15 @Mapper 15 @Mapper
16 public interface AiChatConversationMapper extends BaseMapperX<AiChatConversationDO> { 16 public interface AiChatConversationMapper extends BaseMapperX<AiChatConversationDO> {
17 17
  18 + // TODO @fan:建议这里不排序哈;交给他们前端排序
18 default List<AiChatConversationDO> selectListByUserId(Long userId) { 19 default List<AiChatConversationDO> selectListByUserId(Long userId) {
19 - return selectList(  
20 - new LambdaQueryWrapperX<AiChatConversationDO>()  
21 - .eq(AiChatConversationDO::getUserId, userId)  
22 - .orderByAsc(AiChatConversationDO::getCreateTime)  
23 - ); 20 + return selectList(new LambdaQueryWrapperX<AiChatConversationDO>()
  21 + .eq(AiChatConversationDO::getUserId, userId)
  22 + .orderByAsc(AiChatConversationDO::getCreateTime));
24 } 23 }
25 24
26 } 25 }
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/impl/AiChatServiceImpl.java
@@ -32,6 +32,7 @@ import lombok.extern.slf4j.Slf4j; @@ -32,6 +32,7 @@ import lombok.extern.slf4j.Slf4j;
32 import org.springframework.stereotype.Service; 32 import org.springframework.stereotype.Service;
33 import org.springframework.transaction.annotation.Transactional; 33 import org.springframework.transaction.annotation.Transactional;
34 import reactor.core.publisher.Flux; 34 import reactor.core.publisher.Flux;
  35 +import reactor.core.scheduler.Schedulers;
35 36
36 import java.time.LocalDateTime; 37 import java.time.LocalDateTime;
37 import java.util.*; 38 import java.util.*;
@@ -125,9 +126,10 @@ public class AiChatServiceImpl implements AiChatService { @@ -125,9 +126,10 @@ public class AiChatServiceImpl implements AiChatService {
125 Flux<ChatResponse> streamResponse = chatClient.stream(prompt); 126 Flux<ChatResponse> streamResponse = chatClient.stream(prompt);
126 127
127 // 3.3 流式返回 128 // 3.3 流式返回
  129 + // 注意:Schedulers.immediate() 目的是,避免默认 Schedulers.parallel() 并发消费 chunk 导致 SSE 响应前端会乱序问题
128 StringBuffer contentBuffer = new StringBuffer(); 130 StringBuffer contentBuffer = new StringBuffer();
129 - return streamResponse.map(response -> {  
130 - String newContent = response.getResult() != null ? response.getResult().getOutput().getContent() : null; 131 + return streamResponse.publishOn(Schedulers.immediate()).map(chunk -> {
  132 + String newContent = chunk.getResult() != null ? chunk.getResult().getOutput().getContent() : null;
131 newContent = StrUtil.nullToDefault(newContent, ""); // 避免 null 的 情况 133 newContent = StrUtil.nullToDefault(newContent, ""); // 避免 null 的 情况
132 contentBuffer.append(newContent); 134 contentBuffer.append(newContent);
133 // 响应结果 135 // 响应结果
yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/openAiImage/OpenAiImageClientTests.java
@@ -71,21 +71,4 @@ public class OpenAiImageClientTests { @@ -71,21 +71,4 @@ public class OpenAiImageClientTests {
71 } 71 }
72 } 72 }
73 73
74 - public static void main(String[] args) {  
75 -// OpenAiApi api = new OpenAiApi("https://api.gptsapi.net", "sk-yzKea6d8e8212c3bdd99f9f44ced1cae37c097e5aa3BTS7z");  
76 -// OpenAiApi api = new OpenAiApi("https://openkey.cloud", "sk-QmgIIPc5xiYd8lPb076b1b7774Ea49Af9eD2Ef172c8f7e43");  
77 - OpenAiApi api = new OpenAiApi("https://api.chatanywhere.tech", "sk-gkgfYxhX9FxyZJznwxRZSJwKeGQYNPDVWjhby2PRRf17GHeT");  
78 - OpenAiChatClient client = new OpenAiChatClient(api);  
79 -// String result = client.call("未来,英文是什么?");  
80 -// System.out.println(result);  
81 - Flux<String> result = client.stream("未来,英文是什么?");  
82 - result.map(new Function<String, String>() {  
83 - @Override  
84 - public String apply(String s) {  
85 - System.out.println(s);  
86 - return s;  
87 - }  
88 - }).blockLast();  
89 - }  
90 -  
91 } 74 }
yudao-server/src/main/resources/application.yaml
@@ -156,6 +156,9 @@ spring.ai: @@ -156,6 +156,9 @@ spring.ai:
156 # base-url: https://api.chatanywhere.tech 156 # base-url: https://api.chatanywhere.tech
157 api-key: sk-yzKea6d8e8212c3bdd99f9f44ced1cae37c097e5aa3BTS7z 157 api-key: sk-yzKea6d8e8212c3bdd99f9f44ced1cae37c097e5aa3BTS7z
158 base-url: https://api.gptsapi.net 158 base-url: https://api.gptsapi.net
  159 +# chat:
  160 +# options:
  161 +# model: gpt-4-0125-preview
159 162
160 yudao.ai: 163 yudao.ai:
161 yiyan: 164 yiyan: