Administrator
2025-08-25 4efee316c1411c596a4d333740c3921742f08116
feat(ai): 添加流式回答接口 V2 版本

- 新增 answerStreamV2 方法,支持更复杂的对话场景
- 添加消息记录功能,基于会话 ID 获取历史对话内容
- 优化系统消息和用户消息的处理逻辑
- 新增 AiTalkAnswerStream 类作为请求参数
- 在 AiTalkItemService 中添加 getListByTalkId 方法
- 在 AiTalkService 中添加 answerStreamV2 方法
- 在 ApiAiTalkController 中添加 answerStreamV2 接口
7 files modified
1 files added
150 ■■■■■ changed files
src/main/java/cc/mrbird/febs/ai/controller/talk/ApiAiTalkController.java 11 ●●●●● patch | view | raw | blame | history
src/main/java/cc/mrbird/febs/ai/req/talk/AiTalkAnswerStream.java 24 ●●●●● patch | view | raw | blame | history
src/main/java/cc/mrbird/febs/ai/service/AiService.java 3 ●●●●● patch | view | raw | blame | history
src/main/java/cc/mrbird/febs/ai/service/AiTalkItemService.java 4 ●●●● patch | view | raw | blame | history
src/main/java/cc/mrbird/febs/ai/service/AiTalkService.java 3 ●●●●● patch | view | raw | blame | history
src/main/java/cc/mrbird/febs/ai/service/impl/AiServiceImpl.java 85 ●●●●● patch | view | raw | blame | history
src/main/java/cc/mrbird/febs/ai/service/impl/AiTalkItemServiceImpl.java 14 ●●●●● patch | view | raw | blame | history
src/main/java/cc/mrbird/febs/ai/service/impl/AiTalkServiceImpl.java 6 ●●●●● patch | view | raw | blame | history
src/main/java/cc/mrbird/febs/ai/controller/talk/ApiAiTalkController.java
@@ -3,6 +3,7 @@
import cc.mrbird.febs.ai.req.memberTalk.ApiMemberTalkAnswerDto;
import cc.mrbird.febs.ai.req.memberTalk.ApiMemberTalkDto;
import cc.mrbird.febs.ai.req.memberTalk.ApiMemberTalkItemPageDto;
import cc.mrbird.febs.ai.req.talk.AiTalkAnswerStream;
import cc.mrbird.febs.ai.req.talk.ApiTalkDto;
import cc.mrbird.febs.ai.req.talk.ApiTalkItemPageDto;
import cc.mrbird.febs.ai.req.talk.ApiTalkPageDto;
@@ -77,4 +78,14 @@
        return aiTalkService.answerStream(question);
    }
    @ApiOperation("提问AI(流式)V2")
    @ApiResponses({
            @ApiResponse(code = 200, message = "流式响应", response = ApiMemberTalkStreamVo.class),
    })
    @PostMapping("/answer-streamV2")
    public Flux<FebsResponse> answerStreamV2(@RequestBody @Validated AiTalkAnswerStream dto) {
        return aiTalkService.answerStreamV2(dto);
    }
}
src/main/java/cc/mrbird/febs/ai/req/talk/AiTalkAnswerStream.java
New file
@@ -0,0 +1,24 @@
package cc.mrbird.febs.ai.req.talk;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotBlank;
/**
 * @author Administrator
 */
@Data
@ApiModel(value = "AiTalkAnsStream", description = "参数")
public class AiTalkAnswerStream {
    @NotBlank(message = "会话ID不能为空")
    @ApiModelProperty(value = "会话ID", example = "10")
    private String talkId;
    @NotBlank(message = "内容不能为空")
    @ApiModelProperty(value = "内容", example = "10")
    private String question;
}
src/main/java/cc/mrbird/febs/ai/service/AiService.java
@@ -2,6 +2,7 @@
import cc.mrbird.febs.ai.req.ai.AiMessage;
import cc.mrbird.febs.ai.req.ai.AiRequest;
import cc.mrbird.febs.ai.req.talk.AiTalkAnswerStream;
import cc.mrbird.febs.ai.res.ai.AiResponse;
import cc.mrbird.febs.ai.res.ai.Report;
import cc.mrbird.febs.common.entity.FebsResponse;
@@ -30,4 +31,6 @@
    Report extractReportData(String modelOutput);
    Flux<FebsResponse> answerStream(String question);
    Flux<FebsResponse> answerStreamV2(AiTalkAnswerStream dto);
}
src/main/java/cc/mrbird/febs/ai/service/AiTalkItemService.java
@@ -6,9 +6,13 @@
import cn.hutool.core.date.DateTime;
import com.baomidou.mybatisplus.extension.service.IService;
import java.util.List;
public interface AiTalkItemService extends IService<AiTalkItem> {
    void add(String id, int code, String context, String memberUuid, DateTime date);
    FebsResponse historyPage(ApiTalkItemPageDto dto);
    List<AiTalkItem> getListByTalkId(String talkId);
}
src/main/java/cc/mrbird/febs/ai/service/AiTalkService.java
@@ -1,6 +1,7 @@
package cc.mrbird.febs.ai.service;
import cc.mrbird.febs.ai.entity.AiTalk;
import cc.mrbird.febs.ai.req.talk.AiTalkAnswerStream;
import cc.mrbird.febs.ai.req.talk.ApiTalkDto;
import cc.mrbird.febs.ai.req.talk.ApiTalkItemPageDto;
import cc.mrbird.febs.ai.req.talk.ApiTalkPageDto;
@@ -21,4 +22,6 @@
    FebsResponse talkList(ApiTalkPageDto dto);
    FebsResponse historyPage(ApiTalkItemPageDto dto);
    Flux<FebsResponse> answerStreamV2(AiTalkAnswerStream dto);
}
src/main/java/cc/mrbird/febs/ai/service/impl/AiServiceImpl.java
@@ -1,15 +1,18 @@
package cc.mrbird.febs.ai.service.impl;
import cc.mrbird.febs.ai.entity.AiTalkItem;
import cc.mrbird.febs.ai.enumerates.AiTypeEnum;
import cc.mrbird.febs.ai.entity.AiProductRole;
import cc.mrbird.febs.ai.req.ai.AiMessage;
import cc.mrbird.febs.ai.req.ai.AiRequest;
import cc.mrbird.febs.ai.req.talk.AiTalkAnswerStream;
import cc.mrbird.febs.ai.res.ai.AiResponse;
import cc.mrbird.febs.ai.res.ai.RadarDataItem;
import cc.mrbird.febs.ai.res.ai.Report;
import cc.mrbird.febs.ai.res.memberTalk.ApiMemberTalkStreamVo;
import cc.mrbird.febs.ai.service.AiProductRoleService;
import cc.mrbird.febs.ai.service.AiService;
import cc.mrbird.febs.ai.service.AiTalkItemService;
import cc.mrbird.febs.common.entity.FebsResponse;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.json.JSONUtil;
@@ -69,6 +72,7 @@
    private final AiProductRoleService aiProductRoleService;
    private final ObjectMapper objectMapper;
    private final AiTalkItemService aiTalkItemService;
    @Value("${ai.service.ak}")
    private String ak;
@@ -418,6 +422,87 @@
                });
    }
    @Override
    public Flux<FebsResponse> answerStreamV2(AiTalkAnswerStream dto) {
        String question = dto.getQuestion();
        //获取消息记录
        List<AiTalkItem> aiTalkItems = aiTalkItemService.getListByTalkId(dto.getTalkId());
        log.info("----- standard request -----");
        final ChatMessage systemMessage = ChatMessage.builder()
                .role(ChatMessageRole.SYSTEM)
                .content("你是豆包,是由字节跳动开发的 AI 人工智能助手")
                .build();
        List<ChatMessage> messages = Arrays.asList(systemMessage);
        if(CollUtil.isNotEmpty(aiTalkItems)){
            for (AiTalkItem aiTalkItem : aiTalkItems){
                if (aiTalkItem.getType() == 1){
                    ChatMessage userMessage = ChatMessage.builder()
                            .role(ChatMessageRole.USER)
                            .content(aiTalkItem.getContext())
                            .build();
                    messages.add(userMessage);
                }
                if (aiTalkItem.getType() == 2){
                    ChatMessage assistantMessage = ChatMessage.builder()
                            .role(ChatMessageRole.ASSISTANT)
                            .content(aiTalkItem.getContext())
                            .build();
                    messages.add(assistantMessage);
                }
            }
        }
        final ChatMessage userMessage = ChatMessage.builder()
                .role(ChatMessageRole.USER)
                .content(question)
                .build();
        messages.add(userMessage);
        ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest.builder()
                .model("ep-20250805124033-lhxbf")
                .messages(messages)
                .stream(true)
                .thinking(new ChatCompletionRequest.ChatCompletionRequestThinking("enabled"))
                .temperature(0.7)
                .topP(0.9)
                .maxTokens(2048)
                .frequencyPenalty(0.0)
                .build();
        return Flux.from(service.streamChatCompletion(chatCompletionRequest))
                .map(response -> {
                    if (response == null || response.getChoices() == null || response.getChoices().isEmpty()) {
                        return new FebsResponse().success().data("END");
                    }
                    ChatCompletionChoice choice = response.getChoices().get(0);
                    if (choice == null || choice.getMessage() == null) {
                        return new FebsResponse().success().data("END");
                    }
                    ApiMemberTalkStreamVo apiMemberTalkStreamVo = new ApiMemberTalkStreamVo();
                    // 判断是否触发深度思考,触发则打印模型输出的思维链内容
                    ChatMessage message = choice.getMessage();
                    if (message.getReasoningContent()!= null &&!message.getReasoningContent().isEmpty()) {
                        apiMemberTalkStreamVo.setReasoningContent(message.getReasoningContent());
                        System.out.print(message.getReasoningContent());
                    }
                    String content = message.getContent() == null ? "" : message.getContent().toString();
                    apiMemberTalkStreamVo.setContent(content);
                    System.out.print(content);
                    return new FebsResponse().success().data(apiMemberTalkStreamVo);
                })
                .onErrorResume(throwable -> {
                    log.error("流式调用AI服务失败,问题输入: {}", question, throwable);
                    FebsResponse errorResponse = new FebsResponse().message("AI服务调用失败");
                    return Flux.just(errorResponse);
                });
    }
    private Report tryRepairTruncatedJson(String truncatedJson) {
        String[] repairAttempts = {
src/main/java/cc/mrbird/febs/ai/service/impl/AiTalkItemServiceImpl.java
@@ -9,11 +9,15 @@
import cc.mrbird.febs.ai.utils.UUID;
import cc.mrbird.febs.common.entity.FebsResponse;
import cn.hutool.core.date.DateTime;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.List;
@Slf4j
@Service
@@ -40,4 +44,14 @@
        Page<ApiTalkItemPageVo> pageListByQuery = aiTalkItemMapper.getPageListByQuery(page, dto);
        return new FebsResponse().success().data(pageListByQuery);
    }
    @Override
    public List<AiTalkItem> getListByTalkId(String talkId) {
        LambdaQueryWrapper<AiTalkItem> queryWrapper = Wrappers.lambdaQuery(AiTalkItem.class);
        queryWrapper.eq(AiTalkItem::getTalkId, talkId);
        queryWrapper.orderByDesc(AiTalkItem::getCreatedTime);
        queryWrapper.last("limit 10");
        List<AiTalkItem> list = aiTalkItemMapper.selectList(queryWrapper);
        return list;
    }
}
src/main/java/cc/mrbird/febs/ai/service/impl/AiTalkServiceImpl.java
@@ -2,6 +2,7 @@
import cc.mrbird.febs.ai.entity.AiTalk;
import cc.mrbird.febs.ai.mapper.AiTalkMapper;
import cc.mrbird.febs.ai.req.talk.AiTalkAnswerStream;
import cc.mrbird.febs.ai.req.talk.ApiTalkDto;
import cc.mrbird.febs.ai.req.talk.ApiTalkItemPageDto;
import cc.mrbird.febs.ai.req.talk.ApiTalkPageDto;
@@ -88,4 +89,9 @@
        return aiTalkItemService.historyPage(dto);
    }
    @Override
    public Flux<FebsResponse> answerStreamV2(AiTalkAnswerStream dto) {
        return aiService.answerStreamV2(dto);
    }
}