From 3d762c09e6b25a87c8f4c5e026ed6de255b43377 Mon Sep 17 00:00:00 2001 From: Administrator <15274802129@163.com> Date: Tue, 02 Sep 2025 14:54:37 +0800 Subject: [PATCH] feat(ai): 实现 AI 流式回答功能 --- src/main/java/cc/mrbird/febs/ai/req/memberTalkStream/AiTalkAnswerStreamDto.java | 27 ++++ src/main/java/cc/mrbird/febs/ai/req/memberTalkStream/ApiMemberTalkAnswerSavaDto.java | 35 +++++ src/main/java/cc/mrbird/febs/ai/enumerates/AiTalkOutputEnum.java | 49 ++++++++ src/main/java/cc/mrbird/febs/ai/req/memberTalkStream/AiPromptJsonReq.java | 19 +++ src/main/java/cc/mrbird/febs/ai/service/ApiMemberTalkStreamService.java | 7 + src/main/java/cc/mrbird/febs/ai/service/impl/ApiMemberTalkStreamServiceImpl.java | 135 +++++++++++++++++++++- src/main/java/cc/mrbird/febs/ai/controller/memberTalk/ApiMemberTalkStreamController.java | 30 +++++ src/main/java/cc/mrbird/febs/ai/enumerates/AiPromptEnum.java | 2 8 files changed, 298 insertions(+), 6 deletions(-) diff --git a/src/main/java/cc/mrbird/febs/ai/controller/memberTalk/ApiMemberTalkStreamController.java b/src/main/java/cc/mrbird/febs/ai/controller/memberTalk/ApiMemberTalkStreamController.java index 277d56e..b482022 100644 --- a/src/main/java/cc/mrbird/febs/ai/controller/memberTalk/ApiMemberTalkStreamController.java +++ b/src/main/java/cc/mrbird/febs/ai/controller/memberTalk/ApiMemberTalkStreamController.java @@ -1,18 +1,29 @@ package cc.mrbird.febs.ai.controller.memberTalk; import cc.mrbird.febs.ai.req.memberTalk.ApiMemberTalkItemPageDto; +import cc.mrbird.febs.ai.req.memberTalkStream.AiTalkAnswerStreamDto; +import cc.mrbird.febs.ai.req.memberTalkStream.ApiMemberTalkAnswerSavaDto; import cc.mrbird.febs.ai.req.memberTalkStream.ApiMemberTalkReloadStreamDto; import cc.mrbird.febs.ai.req.memberTalkStream.ApiMemberTalkStreamDto; +import cc.mrbird.febs.ai.req.talk.AiTalkAnswerStream; import cc.mrbird.febs.ai.res.memberTalk.ApiMemberTalkItemVo; import cc.mrbird.febs.ai.res.memberTalkStream.ApiMemberTalkReloadStreamVo; import cc.mrbird.febs.ai.res.memberTalkStream.ApiMemberTalkStreamVo; import cc.mrbird.febs.ai.service.ApiMemberTalkStreamService; +import cc.mrbird.febs.ai.strategy.enumerates.LlmStrategyEnum; +import cc.mrbird.febs.ai.strategy.param.LlmStrategyDto; import cc.mrbird.febs.common.entity.FebsResponse; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.dashscope.common.Role; import io.swagger.annotations.*; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; +import reactor.core.publisher.Flux; + +import java.util.ArrayList; /** * @author Administrator @@ -56,4 +67,23 @@ return apiMemberTalkStreamService.historyPage(dto); } + @ApiOperation("回答(流式)") + @ApiResponses({ + @ApiResponse(code = 200, message = "流式响应", response = cc.mrbird.febs.ai.res.memberTalk.ApiMemberTalkStreamVo.class), + }) + @PostMapping("/answer") + public Flux<FebsResponse> answer(@RequestBody @Validated AiTalkAnswerStreamDto dto) { + if (StrUtil.isEmpty(dto.getId()) || StrUtil.isEmpty(dto.getReqContext())|| StrUtil.isEmpty(dto.getReqContext())){ + return Flux.just(new FebsResponse().fail().message("参数异常")); + } + return apiMemberTalkStreamService.answer(dto); + } + + @ApiOperation(value = "保存答案", notes = "保存答案") + @PostMapping(value = "/saveAnswer") + public FebsResponse saveAnswer(@RequestBody @Validated ApiMemberTalkAnswerSavaDto dto) { + + return apiMemberTalkStreamService.saveAnswer(dto); + } + } diff --git a/src/main/java/cc/mrbird/febs/ai/enumerates/AiPromptEnum.java b/src/main/java/cc/mrbird/febs/ai/enumerates/AiPromptEnum.java index 9244aa0..8386811 100644 --- a/src/main/java/cc/mrbird/febs/ai/enumerates/AiPromptEnum.java +++ b/src/main/java/cc/mrbird/febs/ai/enumerates/AiPromptEnum.java @@ -9,6 +9,8 @@ @Getter public enum AiPromptEnum { + HIGH_LIGHT(2,"请输出亮点"), + STREAM_NORMAL(1,"你是人工智能,请回答我提出的问题"); private final int code; diff --git a/src/main/java/cc/mrbird/febs/ai/enumerates/AiTalkOutputEnum.java b/src/main/java/cc/mrbird/febs/ai/enumerates/AiTalkOutputEnum.java new file mode 100644 index 0000000..417fd9b --- /dev/null +++ b/src/main/java/cc/mrbird/febs/ai/enumerates/AiTalkOutputEnum.java @@ -0,0 +1,49 @@ +package cc.mrbird.febs.ai.enumerates; + +import lombok.Getter; + +/** + * @author Administrator + */ + +@Getter +public enum AiTalkOutputEnum { + + KEY_KNOWLEDGE(4,"只输出知识点总结","KEY_KNOWLEDGE"), + + REFERENCE_ANSWER(3,"只输出参考答案","REFERENCE_ANSWER"), + + SUGGESTION(2,"只输出建议","SUGGESTION"), + + HIGH_LIGHT(1,"只输出亮点","HIGH_LIGHT"); + + private final int type; + private final String content; + private final String code; + + AiTalkOutputEnum(int type, String content, String code) { + + this.type = type; + this.content = content; + this.code = code; + } + + public static String getContentByType(int type) { + for (AiTalkOutputEnum value : AiTalkOutputEnum.values()) { + if (value.getType() == type) { + return value.getContent(); + } + } + return null; + } + + public static String getCodeByType(int type) { + for (AiTalkOutputEnum value : AiTalkOutputEnum.values()) { + if (value.getType() == type) { + return value.getCode(); + } + } + return null; + } + +} diff --git a/src/main/java/cc/mrbird/febs/ai/req/memberTalkStream/AiPromptJsonReq.java b/src/main/java/cc/mrbird/febs/ai/req/memberTalkStream/AiPromptJsonReq.java new file mode 100644 index 0000000..364e9a0 --- /dev/null +++ b/src/main/java/cc/mrbird/febs/ai/req/memberTalkStream/AiPromptJsonReq.java @@ -0,0 +1,19 @@ +package cc.mrbird.febs.ai.req.memberTalkStream; + +import io.swagger.annotations.ApiModel; +import lombok.Data; + +/** + * @author Administrator + */ +@Data +@ApiModel(value = "AiPromptJsonReq", description = "参数") +public class AiPromptJsonReq { + + private String task; + + private String rule; + + private String outputFormat; + +} diff --git a/src/main/java/cc/mrbird/febs/ai/req/memberTalkStream/AiTalkAnswerStreamDto.java b/src/main/java/cc/mrbird/febs/ai/req/memberTalkStream/AiTalkAnswerStreamDto.java new file mode 100644 index 0000000..378c1f7 --- /dev/null +++ b/src/main/java/cc/mrbird/febs/ai/req/memberTalkStream/AiTalkAnswerStreamDto.java @@ -0,0 +1,27 @@ +package cc.mrbird.febs.ai.req.memberTalkStream; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +/** + * @author Administrator + */ +@Data +@ApiModel(value = "AiTalkAnswerStreamDto", description = "参数") +public class AiTalkAnswerStreamDto { + + + @ApiModelProperty(value = "类型 1:亮点 2:建议 3:参考答案 4:知识点总结", example = "10") + private Integer type; + + + @ApiModelProperty(value = "会话ID", example = "10") + private String id; + + + @ApiModelProperty(value = "回答", example = "10") + private String reqContext; + + +} diff --git a/src/main/java/cc/mrbird/febs/ai/req/memberTalkStream/ApiMemberTalkAnswerSavaDto.java b/src/main/java/cc/mrbird/febs/ai/req/memberTalkStream/ApiMemberTalkAnswerSavaDto.java new file mode 100644 index 0000000..6333555 --- /dev/null +++ b/src/main/java/cc/mrbird/febs/ai/req/memberTalkStream/ApiMemberTalkAnswerSavaDto.java @@ -0,0 +1,35 @@ +package cc.mrbird.febs.ai.req.memberTalkStream; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +/** + * @author Administrator + */ +@Data +@ApiModel(value = "ApiMemberTalkAnswerSavaDto", description = "参数") +public class ApiMemberTalkAnswerSavaDto { + + + /** + * 用户对话ID (UUID) + */ + @NotBlank(message = "会话ID不能为空") + @ApiModelProperty(value = "会话ID", example = "10") + private String memberTalkId; + + @ApiModelProperty(value = "会话ItemID", example = "10") + private String memberTalkItemId; + + @NotBlank(message = "回复内容不能为空") + @ApiModelProperty(value = "回复内容", example = "10") + private String content; + + @NotNull(message = "类型ID不能为空") + @ApiModelProperty(value = "类型 1:亮点 2:建议 3:参考答案 4:知识点总结", example = "10") + private Integer type; +} diff --git a/src/main/java/cc/mrbird/febs/ai/service/ApiMemberTalkStreamService.java b/src/main/java/cc/mrbird/febs/ai/service/ApiMemberTalkStreamService.java index ee5e950..9994afd 100644 --- a/src/main/java/cc/mrbird/febs/ai/service/ApiMemberTalkStreamService.java +++ b/src/main/java/cc/mrbird/febs/ai/service/ApiMemberTalkStreamService.java @@ -2,10 +2,13 @@ import cc.mrbird.febs.ai.entity.AiMemberTalk; import cc.mrbird.febs.ai.req.memberTalk.ApiMemberTalkItemPageDto; +import cc.mrbird.febs.ai.req.memberTalkStream.AiTalkAnswerStreamDto; +import cc.mrbird.febs.ai.req.memberTalkStream.ApiMemberTalkAnswerSavaDto; import cc.mrbird.febs.ai.req.memberTalkStream.ApiMemberTalkReloadStreamDto; import cc.mrbird.febs.ai.req.memberTalkStream.ApiMemberTalkStreamDto; import cc.mrbird.febs.common.entity.FebsResponse; import com.baomidou.mybatisplus.extension.service.IService; +import reactor.core.publisher.Flux; public interface ApiMemberTalkStreamService extends IService<AiMemberTalk> { /** @@ -18,4 +21,8 @@ FebsResponse reload(ApiMemberTalkReloadStreamDto dto); FebsResponse historyPage(ApiMemberTalkItemPageDto dto); + + Flux<FebsResponse> answer(AiTalkAnswerStreamDto dto); + + FebsResponse saveAnswer(ApiMemberTalkAnswerSavaDto dto); } diff --git a/src/main/java/cc/mrbird/febs/ai/service/impl/ApiMemberTalkStreamServiceImpl.java b/src/main/java/cc/mrbird/febs/ai/service/impl/ApiMemberTalkStreamServiceImpl.java index eaae65c..3b1b327 100644 --- a/src/main/java/cc/mrbird/febs/ai/service/impl/ApiMemberTalkStreamServiceImpl.java +++ b/src/main/java/cc/mrbird/febs/ai/service/impl/ApiMemberTalkStreamServiceImpl.java @@ -1,29 +1,36 @@ package cc.mrbird.febs.ai.service.impl; -import cc.mrbird.febs.ai.entity.AiMemberTalk; -import cc.mrbird.febs.ai.entity.AiMemberTalkItem; -import cc.mrbird.febs.ai.entity.AiProductQuestion; -import cc.mrbird.febs.ai.entity.AiProductRoleLink; +import cc.mrbird.febs.ai.entity.*; +import cc.mrbird.febs.ai.enumerates.AiTalkOutputEnum; import cc.mrbird.febs.ai.enumerates.AiTypeEnum; import cc.mrbird.febs.ai.mapper.AiMemberTalkMapper; import cc.mrbird.febs.ai.req.memberTalk.ApiMemberTalkItemPageDto; -import cc.mrbird.febs.ai.req.memberTalkStream.ApiMemberTalkReloadStreamDto; -import cc.mrbird.febs.ai.req.memberTalkStream.ApiMemberTalkStreamDto; +import cc.mrbird.febs.ai.req.memberTalkStream.*; import cc.mrbird.febs.ai.res.memberTalkStream.ApiMemberTalkReloadStreamVo; import cc.mrbird.febs.ai.res.memberTalkStream.ApiMemberTalkStreamVo; import cc.mrbird.febs.ai.service.*; +import cc.mrbird.febs.ai.strategy.LlmStrategyFactory; +import cc.mrbird.febs.ai.strategy.enumerates.LlmStrategyEnum; +import cc.mrbird.febs.ai.strategy.param.LlmStrategyDto; import cc.mrbird.febs.common.entity.FebsResponse; import cc.mrbird.febs.common.exception.FebsException; import cc.mrbird.febs.common.utils.LoginUserUtil; import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONUtil; +import com.alibaba.dashscope.common.Role; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import reactor.core.publisher.Flux; +import java.util.ArrayList; import java.util.Date; +import java.util.HashMap; +import java.util.List; /** * AI用户对话训练记录 Service实现类 @@ -38,9 +45,11 @@ private final AiMemberTalkMapper aiMemberTalkMapper; private final AiProductRoleLinkService aiProductRoleLinkService; + private final AiProductRoleService aiProductRoleService; private final AiMemberTalkService aiMemberTalkService; private final AiMemberTalkItemService aiMemberTalkItemService; private final AiProductQuestionService aiProductQuestionService; + private final LlmStrategyFactory llmStrategyFactory; private final AiService aiService; @@ -108,4 +117,118 @@ dto.setMemberUuid(memberUuid); return new FebsResponse().success().data(aiMemberTalkItemService.historyPage(dto)); } + + @Override + public Flux<FebsResponse> answer(AiTalkAnswerStreamDto dto) { + + String memberUuid = LoginUserUtil.getLoginUser().getMemberUuid(); + Integer type = dto.getType(); + String memberTalkId = dto.getId(); + AiMemberTalk aiMemberTalk = this.getById(memberTalkId); + if (ObjectUtil.isNull(aiMemberTalk)){ + throw new FebsException("对话不存在"); + } + + LambdaQueryWrapper<AiProductRoleLink> productLinkQuery = Wrappers.lambdaQuery(AiProductRoleLink.class); + productLinkQuery.eq(AiProductRoleLink::getProductId,aiMemberTalk.getProductId()); + productLinkQuery.last("limit 1"); + AiProductRoleLink aiProductRoleLink = aiProductRoleLinkService.getByQuery(productLinkQuery); + if(ObjectUtil.isNull(aiProductRoleLink)){ + throw new FebsException("产品没有关联AI陪练"); + } + + String productRoleId = aiProductRoleLink.getProductRoleId(); + AiProductRole aiProductRole = aiProductRoleService.getById(productRoleId); + if (ObjectUtil.isNull(aiProductRole)){ + throw new FebsException("产品AI陪练不存在"); + } + + String reqContext = dto.getReqContext(); + LambdaQueryWrapper<AiMemberTalkItem> memberTalkItemQuery = Wrappers.lambdaQuery(AiMemberTalkItem.class); + memberTalkItemQuery.eq(AiMemberTalkItem::getMemberId,memberUuid); + memberTalkItemQuery.eq(AiMemberTalkItem::getMemberTalkId,memberTalkId); + memberTalkItemQuery.eq(AiMemberTalkItem::getType,1); + memberTalkItemQuery.orderByDesc(AiMemberTalkItem::getCreatedTime); + memberTalkItemQuery.last("limit 1"); + AiMemberTalkItem aiMemberTalkItem = aiMemberTalkItemService.getByQuery(memberTalkItemQuery); + aiMemberTalkItemService.add(memberUuid,aiMemberTalk.getId(),2,reqContext,new Date()); + + String prompt = this.buildPrompt(aiProductRole.getPromptHead(), aiProductRole.getPromptTemplate(), type); + + + List<LlmStrategyDto> llmStrategyDtoList = new ArrayList<>(); + LlmStrategyDto llmStrategyDto = this.buildLlmStrategyDtoList(prompt, 1); + llmStrategyDtoList.add(llmStrategyDto); + llmStrategyDto = this.buildLlmStrategyDtoList(reqContext, 2); + llmStrategyDtoList.add(llmStrategyDto); + String modelName = LlmStrategyEnum.getName(aiService.getSystemSetAiType()); + + return llmStrategyFactory.getCalculationStrategyMap().get(modelName).llmInvokeStreamingNoThink(llmStrategyDtoList); + } + + private String buildPrompt(String promptHead, String promptTemplate,Integer type){ + AiPromptJsonReq aiPromptJsonReq = new AiPromptJsonReq(); + aiPromptJsonReq.setTask(promptHead); + aiPromptJsonReq.setRule(promptTemplate); + String contentByCode = AiTalkOutputEnum.HIGH_LIGHT.getContentByType(type); + aiPromptJsonReq.setOutputFormat(contentByCode); + + return JSONUtil.toJsonStr(aiPromptJsonReq); + } + + private LlmStrategyDto buildLlmStrategyDtoList(String Str, Integer type){ + LlmStrategyDto llmStrategyDto = new LlmStrategyDto(); + if (type == 1){ + llmStrategyDto.setRole(Role.SYSTEM.getValue()); + } + if (type == 2){ + llmStrategyDto.setRole(Role.USER.getValue()); + } + if (type == 3){ + llmStrategyDto.setRole(Role.ASSISTANT.getValue()); + } + llmStrategyDto.setContent(Str); + + return llmStrategyDto; + } + + @Override + public FebsResponse saveAnswer(ApiMemberTalkAnswerSavaDto dto) { + + String memberUuid = LoginUserUtil.getLoginUser().getMemberUuid(); + String memberTalkId = dto.getMemberTalkId(); + String content = dto.getContent(); + + AiMemberTalk aiMemberTalk = this.getById(memberTalkId); + if (ObjectUtil.isNull(aiMemberTalk)){ + throw new FebsException("对话不存在"); + } + Integer type = dto.getType(); + String memberTalkItemId; + String contentByCode = AiTalkOutputEnum.HIGH_LIGHT.getCodeByType(type); + if(StrUtil.isEmpty(dto.getMemberTalkItemId())){ + HashMap<String, String> stringStringHashMap = new HashMap<>(); + stringStringHashMap.put(contentByCode,content); + AiMemberTalkItem add = aiMemberTalkItemService.add(memberUuid, memberTalkId, 3, JSONUtil.toJsonStr(stringStringHashMap), new Date()); + memberTalkItemId = add.getId(); + }else{ + memberTalkItemId = dto.getMemberTalkItemId(); + AiMemberTalkItem aiMemberTalkItem = aiMemberTalkItemService.getById(memberTalkItemId); + String context = aiMemberTalkItem.getContext(); + HashMap<String, String> stringStringHashMap = JSONUtil.toBean(context, HashMap.class); + stringStringHashMap.put(contentByCode,content); + aiMemberTalkItemService.update(null, + Wrappers.lambdaUpdate(AiMemberTalkItem.class) + .set(AiMemberTalkItem::getContext,JSONUtil.toJsonStr(stringStringHashMap)) + .set(AiMemberTalkItem::getUpdatedTime,new Date()) + .set(AiMemberTalkItem::getRevision,aiMemberTalkItem.getRevision()+1) + .eq(AiMemberTalkItem::getId,memberTalkItemId) + ); + } + + HashMap<String, String> stringStringHashMap = new HashMap<>(); + stringStringHashMap.put("memberTalkItemId",memberTalkItemId); + return new FebsResponse().success().data(stringStringHashMap); + } + } -- Gitblit v1.9.1