Commit b31e919d5235362a66e5a9bf7716b7a0990ad124

Authored by YunaiV
1 parent 20657cca

【新增】AI:流式发送消息的微调,统一成单接口

yudao-module-ai/yudao-module-ai-biz/src/main/resources/http/chat-message.http renamed to yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatMessageController.http
... ... @@ -10,13 +10,13 @@ Authorization: {{token}}
10 10 }
11 11  
12 12  
13   -### chat call
14   -POST {{baseUrl}}/admin-api/ai/chat/message/send-stream
  13 +### 发送消息(流式)
  14 +POST {{baseUrl}}/ai/chat/message/send-stream
15 15 Content-Type: application/json
16 16 Authorization: {{token}}
17 17  
18 18 {
19   - "conversationId": "1781604279872581649",
  19 + "conversationId": "1781604279872581651",
20 20 "content": "苹果是什么颜色?"
21 21 }
22 22  
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatMessageController.java
... ... @@ -17,6 +17,7 @@ import reactor.core.publisher.Flux;
17 17 import java.util.List;
18 18  
19 19 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  20 +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
20 21  
21 22 @Tag(name = "管理后台 - 聊天消息")
22 23 @RestController
... ... @@ -36,14 +37,8 @@ public class AiChatMessageController {
36 37 @Operation(summary = "发送消息(流式)", description = "流式返回,响应较快")
37 38 @PostMapping(value = "/send-stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
38 39 @PermitAll // 解决 SSE 最终响应的时候,会被 Access Denied 拦截的问题
39   - public Flux<AiChatMessageRespVO> sendMessageStream(@Validated @RequestBody AiChatMessageSendStreamReqVO sendReqVO) {
40   - return chatService.chatStream(sendReqVO);
41   - }
42   -
43   - @Operation(summary = "添加/提问", description = "先创建好 message 前端才好渲染")
44   - @PostMapping(value = "/add")
45   - public CommonResult<AiChatMessageRespVO> add(@Validated @RequestBody AiChatMessageAddReqVO req) {
46   - return success(chatService.add(req));
  40 + public Flux<AiChatMessageSendRespVO> sendChatMessageStream(@Validated @RequestBody AiChatMessageSendReqVO sendReqVO) {
  41 + return chatService.sendChatMessageStream(sendReqVO, getLoginUserId());
47 42 }
48 43  
49 44 @Operation(summary = "获得指定会话的消息列表")
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageRespVO.java
... ... @@ -44,4 +44,5 @@ public class AiChatMessageRespVO {
44 44  
45 45 @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2024-05-12 12:51")
46 46 private LocalDateTime createTime;
  47 +
47 48 }
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageSendRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.Data;
  5 +
  6 +import java.time.LocalDateTime;
  7 +
  8 +@Schema(description = "管理后台 - AI 聊天消息发送 Response VO")
  9 +@Data
  10 +public class AiChatMessageSendRespVO {
  11 +
  12 + @Schema(description = "发送消息", requiredMode = Schema.RequiredMode.REQUIRED)
  13 + private Message send;
  14 +
  15 + @Schema(description = "接收消息", requiredMode = Schema.RequiredMode.REQUIRED)
  16 + private Message receive;
  17 +
  18 + @Schema(description = "消息")
  19 + @Data
  20 + public static class Message {
  21 +
  22 + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  23 + private Long id;
  24 +
  25 + @Schema(description = "消息类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "role")
  26 + private String type; // 参见 MessageType 枚举类
  27 +
  28 + @Schema(description = "聊天内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "你好,你好啊")
  29 + private String content;
  30 +
  31 + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2024-05-12 12:51")
  32 + private LocalDateTime createTime;
  33 +
  34 + }
  35 +
  36 +}
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageSendStreamReqVO.java deleted
1   -package cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message;
2   -
3   -import io.swagger.v3.oas.annotations.media.Schema;
4   -import jakarta.validation.constraints.NotEmpty;
5   -import jakarta.validation.constraints.NotNull;
6   -import lombok.Data;
7   -
8   -@Schema(description = "管理后台 - AI 聊天消息发送 Request VO")
9   -@Data
10   -public class AiChatMessageSendStreamReqVO {
11   -
12   - @Schema(description = "提问的 messageId", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
13   - @NotNull(message = "提问的 messageId 不能为空")
14   - private Long id;
15   -
16   -}
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/convert/AiChatMessageConvert.java
... ... @@ -27,12 +27,4 @@ public interface AiChatMessageConvert {
27 27 */
28 28 List<AiChatMessageRespVO> convertAiChatMessageRespVOList(List<AiChatMessageDO> aiChatMessageDOList);
29 29  
30   - /**
31   - * 转换 - aiChatMessageDO
32   - *
33   - * @param aiChatMessageDO
34   - * @return
35   - */
36   - AiChatMessageRespVO convertAiChatMessageRespVO(AiChatMessageDO aiChatMessageDO);
37   -
38 30 }
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/AiChatService.java
... ... @@ -23,22 +23,6 @@ public interface AiChatService {
23 23 AiChatMessageRespVO chat(AiChatMessageSendReqVO sendReqVO);
24 24  
25 25 /**
26   - * chat stream
27   - *
28   - * @param sendReqVO
29   - * @return
30   - */
31   - Flux<AiChatMessageRespVO> chatStream(AiChatMessageSendStreamReqVO sendReqVO);
32   -
33   - /**
34   - * 添加 - message
35   - *
36   - * @param sendReqVO
37   - * @return
38   - */
39   - AiChatMessageRespVO add(AiChatMessageAddReqVO sendReqVO);
40   -
41   - /**
42 26 * 获取 - 获取对话 message list
43 27 *
44 28 * @param conversationId
... ... @@ -54,4 +38,13 @@ public interface AiChatService {
54 38 */
55 39 Boolean deleteMessage(Long id);
56 40  
  41 + /**
  42 + * 发送消息
  43 + *
  44 + * @param sendReqVO
  45 + * @param userId
  46 + * @return
  47 + */
  48 + Flux<AiChatMessageSendRespVO> sendChatMessageStream(AiChatMessageSendReqVO sendReqVO, Long userId);
  49 +
57 50 }
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/impl/AiChatServiceImpl.java
1 1 package cn.iocoder.yudao.module.ai.service.impl;
2 2  
3 3 import cn.hutool.core.exceptions.ExceptionUtil;
  4 +import cn.hutool.core.util.ObjUtil;
4 5 import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
  6 +import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageSendRespVO;
5 7 import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatConversationDO;
6 8 import org.springframework.ai.chat.ChatClient;
7 9 import org.springframework.ai.chat.ChatResponse;
8 10 import org.springframework.ai.chat.StreamingChatClient;
9 11 import org.springframework.ai.chat.messages.MessageType;
  12 +import org.springframework.ai.chat.prompt.ChatOptionsBuilder;
10 13 import org.springframework.ai.chat.prompt.Prompt;
11   -import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil;
12 14 import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
13   -import cn.iocoder.yudao.module.ai.ErrorCodeConstants;
14 15 import cn.iocoder.yudao.module.ai.config.AiChatClientFactory;
15   -import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.conversation.AiChatConversationRespVO;
16   -import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageAddReqVO;
17 16 import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageRespVO;
18 17 import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageSendReqVO;
19   -import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageSendStreamReqVO;
20 18 import cn.iocoder.yudao.module.ai.convert.AiChatMessageConvert;
21 19 import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatMessageDO;
22 20 import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO;
23 21 import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO;
24   -import cn.iocoder.yudao.module.ai.dal.mysql.chat.AiChatConversationMapper;
25 22 import cn.iocoder.yudao.module.ai.dal.mysql.AiChatMessageMapper;
26 23 import cn.iocoder.yudao.module.ai.service.chat.AiChatConversationService;
27 24 import cn.iocoder.yudao.module.ai.service.model.AiChatRoleService;
... ... @@ -33,13 +30,16 @@ import org.springframework.stereotype.Service;
33 30 import org.springframework.transaction.annotation.Transactional;
34 31 import reactor.core.publisher.Flux;
35 32  
  33 +import java.time.LocalDateTime;
36 34 import java.util.List;
37 35 import java.util.Map;
38 36 import java.util.Set;
39 37 import java.util.concurrent.atomic.AtomicInteger;
40   -import java.util.function.Consumer;
41 38 import java.util.stream.Collectors;
42 39  
  40 +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
  41 +import static cn.iocoder.yudao.module.ai.ErrorCodeConstants.CHAT_CONVERSATION_NOT_EXISTS;
  42 +
43 43 /**
44 44 * 聊天 service
45 45 *
... ... @@ -52,11 +52,11 @@ import java.util.stream.Collectors;
52 52 @AllArgsConstructor
53 53 public class AiChatServiceImpl implements AiChatService {
54 54  
55   - private final AiChatClientFactory aiChatClientFactory;
  55 + private final AiChatClientFactory chatClientFactory;
56 56  
57 57 private final AiChatMessageMapper aiChatMessageMapper;
58 58 private final AiChatConversationService chatConversationService;
59   - private final AiChatModelService aiChatModalService;
  59 + private final AiChatModelService chatModalService;
60 60 private final AiChatRoleService chatRoleService;
61 61  
62 62 @Transactional(rollbackFor = Exception.class)
... ... @@ -65,7 +65,7 @@ public class AiChatServiceImpl implements AiChatService {
65 65 // 查询对话
66 66 AiChatConversationDO conversation = chatConversationService.validateExists(req.getConversationId());
67 67 // 获取对话模型
68   - AiChatModelDO chatModel = aiChatModalService.validateChatModel(conversation.getModelId());
  68 + AiChatModelDO chatModel = chatModalService.validateChatModel(conversation.getModelId());
69 69 // 获取角色信息
70 70 AiChatRoleDO chatRoleDO = conversation.getRoleId() != null ? chatRoleService.validateChatRole(conversation.getRoleId()) : null;
71 71 // 获取 client 类型
... ... @@ -84,7 +84,7 @@ public class AiChatServiceImpl implements AiChatService {
84 84 // req.setTopP(req.getTopP());
85 85 // req.setTemperature(req.getTemperature());
86 86 // 发送 call 调用
87   - ChatClient chatClient = aiChatClientFactory.getChatClient(platformEnum);
  87 + ChatClient chatClient = chatClientFactory.getChatClient(platformEnum);
88 88 ChatResponse call = chatClient.call(prompt);
89 89 content = call.getResult().getOutput().getContent();
90 90 tokens = call.getResults().size();
... ... @@ -113,88 +113,72 @@ public class AiChatServiceImpl implements AiChatService {
113 113 .setModelId(modelId)
114 114 .setContent(content)
115 115 .setTokens(tokens)
116   -
117 116 .setTemperature(temperature)
118 117 .setMaxTokens(maxTokens)
119 118 .setMaxContexts(maxContexts);
  119 + insertChatMessageDO.setCreateTime(LocalDateTime.now());
120 120 // 增加 chat message 记录
121 121 aiChatMessageMapper.insert(insertChatMessageDO);
122 122 return insertChatMessageDO;
123 123 }
124 124  
125   - public Flux<AiChatMessageRespVO> chatStream(AiChatMessageSendStreamReqVO req) {
126   - Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
127   - // 查询提问的 message
128   - AiChatMessageDO aiChatMessageDO = aiChatMessageMapper.selectById(req.getId());
129   - if (aiChatMessageDO == null) {
130   - throw ServiceExceptionUtil.exception(ErrorCodeConstants.AI_CHAT_MESSAGE_NOT_EXIST);
  125 + @Override
  126 + public Flux<AiChatMessageSendRespVO> sendChatMessageStream(AiChatMessageSendReqVO sendReqVO, Long userId) {
  127 + // 1.1 校验对话存在
  128 + AiChatConversationDO conversation = chatConversationService.validateExists(sendReqVO.getConversationId());
  129 + if (ObjUtil.notEqual(conversation.getUserId(), userId)) {
  130 + throw exception(CHAT_CONVERSATION_NOT_EXISTS);
131 131 }
132   - // 查询对话
133   - AiChatConversationDO conversation = chatConversationService.validateExists(aiChatMessageDO.getConversationId());
134   - // 获取对话模型
135   - AiChatModelDO chatModel = aiChatModalService.validateChatModel(conversation.getModelId());
136   - // 获取角色信息
137   - AiChatRoleDO chatRoleDO = conversation.getRoleId() != null ? chatRoleService.validateChatRole(conversation.getRoleId()) : null;
138   - // 创建 chat 需要的 Prompt
139   - Prompt prompt = new Prompt(aiChatMessageDO.getContent());
140   - // 提前创建一个 system message
141   - AiChatMessageDO systemMessage = insertChatMessage(conversation.getId(), MessageType.SYSTEM, loginUserId, conversation.getRoleId(),
142   - chatModel.getModel(), chatModel.getId(), "",
  132 + // 1.2 校验模型
  133 + AiChatModelDO model = chatModalService.validateChatModel(conversation.getModelId());
  134 + AiPlatformEnum platform = AiPlatformEnum.validatePlatform(model.getPlatform());
  135 + StreamingChatClient chatClient = chatClientFactory.getStreamingChatClient(platform);
  136 +
  137 + // 2. 插入 user 发送消息 TODO tokens 计算
  138 + AiChatMessageDO userMessage = insertChatMessage(conversation.getId(), MessageType.USER, userId, conversation.getRoleId(),
  139 + conversation.getModel(), conversation.getId(), sendReqVO.getContent(),
  140 + null, conversation.getTemperature(), conversation.getMaxTokens(), conversation.getMaxContexts());
  141 +
  142 + // 3.1 插入 system 接收消息
  143 + AiChatMessageDO systemMessage = insertChatMessage(conversation.getId(), MessageType.SYSTEM, userId, conversation.getRoleId(),
  144 + conversation.getModel(), conversation.getId(), conversation.getSystemMessage(),
143 145 0, conversation.getTemperature(), conversation.getMaxTokens(), conversation.getMaxContexts());
144   -// req.setTopK(req.getTopK());
145   -// req.setTopP(req.getTopP());
146   -// req.setTemperature(req.getTemperature());
147   - // 获取 client 类型
148   - AiPlatformEnum platformEnum = AiPlatformEnum.validatePlatform(chatModel.getPlatform());
149   - StreamingChatClient streamingChatClient = aiChatClientFactory.getStreamingChatClient(platformEnum);
150   - Flux<ChatResponse> streamResponse = streamingChatClient.stream(prompt);
151   - // 转换 flex AiChatMessageRespVO
  146 + // 3.2 创建 chat 需要的 Prompt
  147 + // TODO 消息上下文
  148 + Prompt prompt = new Prompt(sendReqVO.getContent());
  149 +// ChatOptionsBuilder.builder().withTemperature(conversation.getTemperature().floatValue()).build()
  150 + Flux<ChatResponse> streamResponse = chatClient.stream(prompt);
  151 + // 3.3 转换 flex AiChatMessageRespVO
152 152 StringBuffer contentBuffer = new StringBuffer();
153   - AtomicInteger tokens = new AtomicInteger(0);
  153 + AtomicInteger tokens = new AtomicInteger(0); // TODO token 计算不对;
154 154 return streamResponse.map(res -> {
155   - AiChatMessageRespVO aiChatMessageRespVO =
156   - AiChatMessageConvert.INSTANCE.convertAiChatMessageRespVO(systemMessage);
157   - aiChatMessageRespVO.setContent(res.getResult().getOutput().getContent());
158   - contentBuffer.append(res.getResult().getOutput().getContent());
159   - tokens.incrementAndGet();
160   - return aiChatMessageRespVO;
161   - }
162   - ).doOnComplete(new Runnable() {
163   - @Override
164   - public void run() {
165   - log.info("发送完成!");
166   - // 保存 chat message
167   - aiChatMessageMapper.updateById(new AiChatMessageDO()
168   - .setId(systemMessage.getId())
169   - .setContent(contentBuffer.toString())
170   - .setTokens(tokens.get())
171   - );
172   - }
173   - }).doOnError(new Consumer<Throwable>() {
174   - @Override
175   - public void accept(Throwable throwable) {
176   - log.error("发送错误 {}!", throwable.getMessage());
177   - // 更新错误信息
178   - aiChatMessageMapper.updateById(new AiChatMessageDO()
179   - .setId(systemMessage.getId())
180   - .setContent(throwable.getMessage())
181   - .setTokens(tokens.get())
182   - );
183   - }
184   - });
185   - }
  155 + contentBuffer.append(res.getResult().getOutput().getContent());
  156 + tokens.incrementAndGet();
186 157  
187   - @Override
188   - public AiChatMessageRespVO add(AiChatMessageAddReqVO req) {
189   - Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
190   - // 查询对话
191   - AiChatConversationDO conversation = chatConversationService.validateExists(req.getConversationId());
192   - // 获取对话模型
193   - AiChatModelDO chatModel = aiChatModalService.validateChatModel(conversation.getModelId());
194   - AiChatMessageDO userMessage = insertChatMessage(conversation.getId(), MessageType.USER, loginUserId, conversation.getRoleId(),
195   - chatModel.getModel(), chatModel.getId(), req.getContent(),
196   - null, conversation.getTemperature(), conversation.getMaxTokens(), conversation.getMaxContexts());
197   - return AiChatMessageConvert.INSTANCE.convertAiChatMessageRespVO(userMessage);
  158 + AiChatMessageSendRespVO.Message send = new AiChatMessageSendRespVO.Message().setId(userMessage.getId())
  159 + .setType(MessageType.USER.getValue()).setCreateTime(userMessage.getCreateTime())
  160 + .setContent(sendReqVO.getContent());
  161 + AiChatMessageSendRespVO.Message receive = new AiChatMessageSendRespVO.Message().setId(systemMessage.getId())
  162 + .setType(MessageType.SYSTEM.getValue()).setCreateTime(systemMessage.getCreateTime())
  163 + .setContent(res.getResult().getOutput().getContent());
  164 + return new AiChatMessageSendRespVO().setSend(send).setReceive(receive);
  165 + }).doOnComplete(() -> {
  166 + log.info("发送完成!");
  167 + // 保存 chat message
  168 + aiChatMessageMapper.updateById(new AiChatMessageDO()
  169 + .setId(systemMessage.getId())
  170 + .setContent(contentBuffer.toString())
  171 + .setTokens(tokens.get())
  172 + );
  173 + }).doOnError(throwable -> {
  174 + log.error("发送错误 {}!", throwable.getMessage());
  175 + // 更新错误信息 TODO 貌似不应该更新异常
  176 + aiChatMessageMapper.updateById(new AiChatMessageDO()
  177 + .setId(systemMessage.getId())
  178 + .setContent(throwable.getMessage())
  179 + .setTokens(tokens.get())
  180 + );
  181 + });
198 182 }
199 183  
200 184 @Override
... ... @@ -205,7 +189,7 @@ public class AiChatServiceImpl implements AiChatService {
205 189 List<AiChatMessageDO> aiChatMessageDOList = aiChatMessageMapper.selectByConversationId(conversationId);
206 190 // 获取模型信息
207 191 Set<Long> modalIds = aiChatMessageDOList.stream().map(AiChatMessageDO::getModelId).collect(Collectors.toSet());
208   - List<AiChatModelDO> modalList = aiChatModalService.getModalByIds(modalIds);
  192 + List<AiChatModelDO> modalList = chatModalService.getModalByIds(modalIds);
209 193 Map<Long, AiChatModelDO> modalIdMap = modalList.stream().collect(Collectors.toMap(AiChatModelDO::getId, o -> o));
210 194 // 转换 AiChatMessageRespVO
211 195 List<AiChatMessageRespVO> aiChatMessageRespList = AiChatMessageConvert.INSTANCE.convertAiChatMessageRespVOList(aiChatMessageDOList);
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/org/springframework/ai/models/yiyan/YiYanChatClient.java
... ... @@ -94,7 +94,10 @@ public class YiYanChatClient implements ChatClient, StreamingChatClient {
94 94 String a = ";";
95 95 }
96 96 });
97   - return response.map(res -> new ChatResponse(List.of(new Generation(res.getResult()))));
  97 + return response.map(res -> {
  98 + // TODO @fan:这里缺少了 usage 的封装
  99 + return new ChatResponse(List.of(new Generation(res.getResult())));
  100 + });
98 101 }
99 102  
100 103 private YiYanChatCompletionRequest createRequest(Prompt prompt, boolean stream) {
... ...