🤵‍♂️ 个人主页:@rain雨雨编程

😄微信公众号:rain雨雨编程

✍🏻作者简介:持续分享机器学习,爬虫,数据分析
🐋 希望大家多多支持,我们一起进步!
如果文章对你有帮助的话,
欢迎评论 💬点赞👍🏻 收藏 📂加关注+

目录

AI答题应用平台开发实践

一、项目介绍

二、技术栈指点总结

1. 策略模式

2. 编写和调试Prompt

三、后端开发

3.1 应用审核功能

3.2 评分模块

策略接口

两种策略实现

3.3 回答模块

四、平台智能化

4.1 接入质谱AI模块

引入maven

在application.yaml中定义ai配置

定义AI配置类,加载配置文件,初始化调用质谱的Client并将其注册为bean

封装通用调用方法

4.2 AI生成题目

AI生成题目请求类

定义模板常量和构造用户模板的方法

AI生成接口

4.3 AI智能评分

编写题目答案封装类

定义模板常量和构造用户模板的方法

实现应用评分策略

五、性能优化

5.1 RxJava响应式编程介绍

观察者模式

事件

5.2 AI生成题目(RxJava响应式编程)

封装通用的流式调用AI接口

AI生成题目的SSE接口

5.3 AI评分优化(Redis缓存查询)

引入所需依赖

配置Redission客户端

配置文件补充Redis配置

完整代码

5.4 分库分表实战(SphereSharding划分用户答题表)

新建表

引入Sharding-JDBC依赖

在application.yaml中配置参数

总结


AI答题应用平台开发实践

在当今数字化时代,AI技术的飞速发展为教育和学习领域带来了诸多变革。AI答题应用平台便是其中的一个典型应用,它结合了人工智能、大数据和响应式编程等前沿技术,为用户提供了一个高效、智能的在线答题体验。本文将详细介绍该平台的开发过程,涵盖项目架构设计、技术栈选择、后端开发、智能化应用以及性能优化等多个方面。

一、项目介绍

AI答题应用平台是一个基于Vue 3 + Spring Boot + Redis + ChatGLM + RxJava + SSE技术栈构建的全栈应用。它允许用户快速创建、检索和分享答题应用,并通过AI技术实现题目生成和智能评分。平台的核心功能依赖于以下数据库表结构:

### 1. 应用表
存储用户创建的答题应用信息,包括应用名称、描述、创建时间等。每个应用可以关联多个题目和答题记录。

### 2. 题目表
包含题目的详细信息,如题目内容、题型(选择题、简答题等)、所属应用ID等。题目表是答题应用的核心数据结构。

### 3. 用户表
记录用户的基本信息,如用户名、密码、角色(普通用户或管理员)等。管理员可以审核应用和管理平台内容。

### 4. 评分结果表
存储用户的答题评分结果,包括答题得分、评分时间、答题ID等。该表用于统计和分析用户的答题表现。

### 5. 答题记录表
记录用户的答题行为,包括答题时间、答题内容、所属应用ID等。通过该表可以追踪用户的答题历史。

二、技术栈指点总结

1. 策略模式

在AI答题应用平台中,策略模式被用于实现不同题型的评分策略。通过定义一个评分策略接口,每种题型(如选择题、简答题)实现具体的评分逻辑。例如,选择题可以通过比对答案与标准答案来评分,而简答题则可能需要调用AI模型进行语义分析。

2. 编写和调试Prompt

为了充分利用AI模型(如ChatGLM)的能力,编写高质量的Prompt至关重要。Prompt是用户向AI模型提出的问题或指令,其质量直接影响AI的回答效果。例如,在生成题目时,Prompt可以是“生成一道关于人工智能的简答题”,而在评分时,Prompt可以是“根据标准答案评估用户答案的质量”。

三、后端开发

3.1 应用审核功能

管理员可以通过后端管理界面审核用户提交的答题应用。审核功能的核心在于验证应用内容是否符合平台规范。

/**
 * 应用审核
 * @param reviewRequest
 * @param request
 * @return
 */
@PostMapping("/review")
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
public BaseResponse<Boolean> doAppReview(@RequestBody ReviewRequest reviewRequest, HttpServletRequest request) {
    ThrowUtils.throwIf(reviewRequest == null, ErrorCode.PARAMS_ERROR);
    Long id = reviewRequest.getId();
    Integer reviewStatus = reviewRequest.getReviewStatus();
    // 校验
    ReviewStatusEnum reviewStatusEnum = ReviewStatusEnum.getEnumByValue(reviewStatus);
    if (id == null || reviewStatusEnum == null) {
        throw new BusinessException(ErrorCode.PARAMS_ERROR);
    }
    // 判断是否存在
    App oldApp = appService.getById(id);
    ThrowUtils.throwIf(oldApp == null, ErrorCode.NOT_FOUND_ERROR);
    // 已是该状态
    if (oldApp.getReviewStatus().equals(reviewStatus)) {
        throw new BusinessException(ErrorCode.PARAMS_ERROR, "请勿重复审核");
    }
    // 更新审核状态
    User loginUser = userService.getLoginUser(request);
    App app = new App();
    app.setId(id);
    app.setReviewStatus(reviewStatus);
    app.setReviewerId(loginUser.getId());
    app.setReviewTime(new Date());
    boolean result = appService.updateById(app);
    ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);
    return ResultUtils.success(true);
}

3.2 评分模块

评分模块是平台的核心功能之一。它根据题型调用不同的评分策略。例如,测试题和和得分题可使用不同的评分策略,如自定义评分,AI智能评分。此外,评分模块还利用全局执行器(Executor)来异步处理评分任务,提高系统性能。

策略接口
public interface ScoringStrategy {

    /**
     * 执行评分
     *
     * @param choices
     * @param app
     * @return
     * @throws Exception
     */
    UserAnswer doScore(List<String> choices, App app) throws Exception;
}
两种策略实现
@Service
public class CustomScoreScoringStrategy implements ScoringStrategy {

    @Resource
    private QuestionService questionService;

    @Resource
    private ScoringResultService scoringResultService;

    @Override
    public UserAnswer doScore(List<String> choices, App app) throws Exception {
        Long appId = app.getId();
        // 1. 根据 id 查询到题目和题目结果信息(按分数降序排序)
        Question question = questionService.getOne(
                Wrappers.lambdaQuery(Question.class).eq(Question::getAppId, appId)
        );
        List<ScoringResult> scoringResultList = scoringResultService.list(
                Wrappers.lambdaQuery(ScoringResult.class)
                        .eq(ScoringResult::getAppId, appId)
                        .orderByDesc(ScoringResult::getResultScoreRange)
        );

        // 2. 统计用户的总得分
        int totalScore = 0;
        QuestionVO questionVO = QuestionVO.objToVo(question);
        List<QuestionContentDTO> questionContent = questionVO.getQuestionContent();

        // 遍历题目列表
        for (QuestionContentDTO questionContentDTO : questionContent) {
            // 遍历答案列表
            for (String answer : choices) {
                // 遍历题目中的选项
                for (QuestionContentDTO.Option option : questionContentDTO.getOptions()) {
                    // 如果答案和选项的key匹配
                    if (option.getKey().equals(answer)) {
                        int score = Optional.of(option.getScore()).orElse(0);
                        totalScore += score;
                    }
                }
            }
        }

        // 3. 遍历得分结果,找到第一个用户分数大于得分范围的结果,作为最终结果
        ScoringResult maxScoringResult = scoringResultList.get(0);
        for (ScoringResult scoringResult : scoringResultList) {
            if (totalScore >= scoringResult.getResultScoreRange()) {
                maxScoringResult = scoringResult;
                break;
            }
        }

        // 4. 构造返回值,填充答案对象的属性
        UserAnswer userAnswer = new UserAnswer();
        userAnswer.setAppId(appId);
        userAnswer.setAppType(app.getAppType());
        userAnswer.setScoringStrategy(app.getScoringStrategy());
        userAnswer.setChoices(JSONUtil.toJsonStr(choices));
        userAnswer.setResultId(maxScoringResult.getId());
        userAnswer.setResultName(maxScoringResult.getResultName());
        userAnswer.setResultDesc(maxScoringResult.getResultDesc());
        userAnswer.setResultPicture(maxScoringResult.getResultPicture());
        userAnswer.setResultScore(totalScore);
        return userAnswer;
    }
}

@Service
public class CustomTestScoringStrategy implements ScoringStrategy {
    @Resource
    private QuestionService questionService;

    @Resource
    private ScoringResultService scoringResultService;
    @Override
    public UserAnswer doScore(List<String> choices, App app) {
        // 1. 根据id查询题目,题目结果信息
        Question question = questionService.getOne(Wrappers.lambdaQuery(Question.class)
                .eq(Question::getAppId, app.getId()));
        List<ScoringResult> scoringResultList = scoringResultService.list(Wrappers.lambdaQuery(ScoringResult.class)
                .eq(ScoringResult::getAppId, app.getId()));
        //  2. 统计用户每个选择对应的属性个数,如 I = 10 个,E = 5 个
        HashMap<String, Integer> optionCount = new HashMap<>();
        QuestionVO questionVO = QuestionVO.objToVo(question);
        List<QuestionContentDTO> questionContent = questionVO.getQuestionContent();
        for(QuestionContentDTO questionContentDto : questionContent){
            for(String answer:choices){
                for(QuestionContentDTO.Option option:questionContentDto.getOptions()){
                    if(option.getKey().equals(answer)){
                        String result = option.getResult();
                        if(!optionCount.containsKey(result)){
                            optionCount.put(result,0);
                        }
                        optionCount.put(result,optionCount.get(result)+1);
                    }
                }
            }
        }
        int maxScore=0;
        ScoringResult maxScoringResult=scoringResultList.get(0);
        // 3. 遍历每种评分结果,计算哪个结果的得分更高
        for(ScoringResult scoringResult:scoringResultList){
            List<String> scoreProps = JSONUtil.toList(scoringResult.getResultProp(), String.class);
            int score = scoreProps.stream().mapToInt(prop -> optionCount.getOrDefault(prop, 0)).sum();
            if(score>maxScore){
                maxScoringResult=scoringResult;
                maxScore=score;
            }
        }

        // 4. 构造返回值,填充答案对象的属性
        UserAnswer userAnswer = new UserAnswer();
        userAnswer.setAppId(app.getId());
        userAnswer.setAppType(app.getAppType());
        userAnswer.setScoringStrategy(app.getScoringStrategy());
        userAnswer.setChoices(JSONUtil.toJsonStr(choices));
        userAnswer.setResultId(maxScoringResult.getId());
        userAnswer.setResultName(maxScoringResult.getResultName());
        userAnswer.setResultDesc(maxScoringResult.getResultDesc());
        userAnswer.setResultPicture(maxScoringResult.getResultPicture());

        return userAnswer;
    }
}
@Service
public class ScoringStrategyContext {

    @Resource
    private CustomScoreScoringStrategy customScoreScoringStrategy;

    @Resource
    private CustomTestScoringStrategy customTestScoringStrategy;

    @Resource
    private AiTestScoringStrategy aiTestScoringStrategy;

    /**
     * 评分
     *
     * @param choiceList
     * @param app
     * @return
     * @throws Exception
     */
    public UserAnswer doScore(List<String> choiceList, App app) throws Exception {
        AppTypeEnum appTypeEnum = AppTypeEnum.getEnumByValue(app.getAppType());
        AppScoringStrategyEnum appScoringStrategyEnum = AppScoringStrategyEnum.getEnumByValue(app.getScoringStrategy());
        if (appTypeEnum == null || appScoringStrategyEnum == null) {
            throw new BusinessException(ErrorCode.SYSTEM_ERROR, "应用配置有误,未找到匹配的策略");
        }
        // 根据不同的应用类别和评分策略,选择对应的策略执行
        switch (appTypeEnum) {
            case SCORE:
                switch (appScoringStrategyEnum) {
                    case CUSTOM:
                        return customScoreScoringStrategy.doScore(choiceList, app);
                    case AI:
                        break;
                }
                break;
            case TEST:
                switch (appScoringStrategyEnum) {
                    case CUSTOM:
                        return customTestScoringStrategy.doScore(choiceList, app);
                    case AI:
                        return aiTestScoringStrategy.doScore(choiceList, app);
                }
                break;
        }
        throw new BusinessException(ErrorCode.SYSTEM_ERROR, "应用配置有误,未找到匹配的策略");
    }
}

3.3 回答模块

回答模块负责接收用户的答题内容,并调用评分模块进行评分。评分完成后,模块会更新用户的答题记录表和评分结果表。

 /**
     * 创建用户答案
     *
     * @param userAnswerAddRequest
     * @param request
     * @return
     */
    @PostMapping("/add")
    public BaseResponse<Long> addUserAnswer(@RequestBody UserAnswerAddRequest userAnswerAddRequest, HttpServletRequest request) {
        ThrowUtils.throwIf(userAnswerAddRequest == null, ErrorCode.PARAMS_ERROR);
        // 在此处将实体类和 DTO 进行转换
        UserAnswer userAnswer = new UserAnswer();
        BeanUtils.copyProperties(userAnswerAddRequest, userAnswer);
        List<String> choices = userAnswerAddRequest.getChoices();
        userAnswer.setChoices(JSONUtil.toJsonStr(choices));
        // 数据校验
        userAnswerService.validUserAnswer(userAnswer, true);
        // 判断 app 是否存在
        Long appId = userAnswerAddRequest.getAppId();
        App app = appService.getById(appId);
        ThrowUtils.throwIf(app == null, ErrorCode.NOT_FOUND_ERROR);
        if (!ReviewStatusEnum.PASS.equals(ReviewStatusEnum.getEnumByValue(app.getReviewStatus()))) {
            throw new BusinessException(ErrorCode.NO_AUTH_ERROR, "应用未通过审核,无法答题");
        }
        // 填充默认值
        User loginUser = userService.getLoginUser(request);
        userAnswer.setUserId(loginUser.getId());
        // 写入数据库
        try {
            boolean result = userAnswerService.save(userAnswer);
            ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);
        } catch (DuplicateKeyException e) {
            // ignore error
        }
        // 返回新写入的数据 id
        long newUserAnswerId = userAnswer.getId();
        // 调用评分模块
        try {
            UserAnswer userAnswerWithResult = scoringStrategyContext.doScore(choices, app);
            userAnswerWithResult.setId(newUserAnswerId);
            userAnswerWithResult.setAppId(null);
            userAnswerService.updateById(userAnswerWithResult);
        } catch (Exception e) {
            e.printStackTrace();
            throw new BusinessException(ErrorCode.OPERATION_ERROR, "评分错误");
        }
        return ResultUtils.success(newUserAnswerId);
    }

四、平台智能化

4.1 接入质谱AI模块

为了进一步提升平台的智能化水平,可以接入质谱AI模块。该模块可以用于对用户的答题进行评分。

引入maven
<dependency>
    <groupId>cn.bigmodel.openapi</groupId>
    <artifactId>oapi-java-sdk</artifactId>
    <version>release-V4-2.0.2</version>
</dependency>

在application.yaml中定义ai配置
ai:
  apiKey: xxx
定义AI配置类,加载配置文件,初始化调用质谱的Client并将其注册为bean
@Configuration
@ConfigurationProperties(prefix = "ai")
@Data
public class AiConfig {

    private String apiKey;

    @Bean
    public ClientV4 getClientV4() {
        return new ClientV4.Builder(apiKey).build();
    }
}
封装通用调用方法
public String doRequest(List<ChatMessage> messages, Boolean stream, Float temperature) {
    // 构造请求
    ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest.builder()
    .model(Constants.ModelChatGLM4)
    .stream(stream)
    .invokeMethod(Constants.invokeMethod)
    .temperature(temperature)
    .messages(messages)
    .build();
    ModelApiResponse invokeModelApiResp = clientV4.invokeModelApi(chatCompletionRequest);
    ChatMessage result = invokeModelApiResp.getData().getChoices().get(0).getMessage();
    return result.getContent().toString();
}

4.2 AI生成题目

通过调用AI模型(如ChatGLM),平台可以根据用户设定快速生成高质量的题目。这不仅提高了题目的多样性,还降低了题库建设的成本。

AI生成题目请求类
@Data
public class AiGenerateQuestionRequest implements Serializable {

    /**
     * id
     */
    private Long appId;

    /**
     * 题目数
     */
    int questionNumber = 10;

    /**
     * 选项数
     */
    int optionNumber = 2;

    private static final long serialVersionUID = 1L;
}
定义模板常量和构造用户模板的方法
private static final String GENERATE_QUESTION_SYSTEM_MESSAGE = "你是一位严谨的出题专家,我会给你如下信息:\n" +
        "```\n" +
        "应用名称,\n" +
        "【【【应用描述】】】,\n" +
        "应用类别,\n" +
        "要生成的题目数,\n" +
        "每个题目的选项数\n" +
        "```\n" +
        "\n" +
        "请你根据上述信息,按照以下步骤来出题:\n" +
        "1. 要求:题目和选项尽可能地短,题目不要包含序号,每题的选项数以我提供的为主,题目不能重复\n" +
        "2. 严格按照下面的 json 格式输出题目和选项\n" +
        "```\n" +
        "[{\"options\":[{\"value\":\"选项内容\",\"key\":\"A\"},{\"value\":\"\",\"key\":\"B\"}],\"title\":\"题目标题\"}]\n" +
        "```\n" +
        "title 是题目,options 是选项,每个选项的 key 按照英文字母序(比如 A、B、C、D)以此类推,value 是选项内容\n" +
        "3. 检查题目是否包含序号,若包含序号则去除序号\n" +
        "4. 返回的题目列表格式必须为 JSON 数组";

private String getGenerateQuestionUserMessage(App app, int questionNumber, int optionNumber) {
    StringBuilder userMessage = new StringBuilder();
    userMessage.append(app.getAppName()).append("\n");
    userMessage.append(app.getAppDesc()).append("\n");
    userMessage.append(AppTypeEnum.getEnumByValue(app.getAppType()).getText() + "类").append("\n");
    userMessage.append(questionNumber).append("\n");
    userMessage.append(optionNumber);
    return userMessage.toString();
}
AI生成接口
@PostMapping("/ai_generate")
public BaseResponse<List<QuestionContentDTO>> aiGenerateQuestion(@RequestBody AiGenerateQuestionRequest aiGenerateQuestionRequest) {
    ThrowUtils.throwIf(aiGenerateQuestionRequest == null, ErrorCode.PARAMS_ERROR);
    // 获取参数
    Long appId = aiGenerateQuestionRequest.getAppId();
    int questionNumber = aiGenerateQuestionRequest.getQuestionNumber();
    int optionNumber = aiGenerateQuestionRequest.getOptionNumber();
    App app = appService.getById(appId);
    ThrowUtils.throwIf(app == null, ErrorCode.NOT_FOUND_ERROR);
    // 封装 Prompt
    String userMessage = getGenerateQuestionUserMessage(app, questionNumber, optionNumber);
    // AI 生成
    String result = aiManager.doSyncUnstableRequest(GENERATE_QUESTION_SYSTEM_MESSAGE, userMessage);
    // 结果处理
    int start = result.indexOf("[");
    int end = result.lastIndexOf("]");
    String json = result.substring(start, end + 1);
    List<QuestionContentDTO> questionContentDTOList = JSONUtil.toList(json, QuestionContentDTO.class);
    return ResultUtils.success(questionContentDTOList);
}

4.3 AI智能评分

AI智能评分模块可以对用户的答案进行语义分析和质量评估。例如,对于测评题(如人格测试),AI可以通过自然语言处理技术判断判断用户的特性。

编写题目答案封装类
public class QuestionAnswerDTO {

    /**
     * 题目
     */
    private String title;

    /**
     * 用户答案
     */
    private String userAnswer;
}

定义模板常量和构造用户模板的方法
private static final String AI_TEST_SCORING_SYSTEM_MESSAGE = "你是一位严谨的判题专家,我会给你如下信息:\n" +
        "```\n" +
        "应用名称,\n" +
        "【【【应用描述】】】,\n" +
        "题目和用户回答的列表:格式为 [{\"title\": \"题目\",\"answer\": \"用户回答\"}]\n" +
        "```\n" +
        "\n" +
        "请你根据上述信息,按照以下步骤来对用户进行评价:\n" +
        "1. 要求:需要给出一个明确的评价结果,包括评价名称(尽量简短)和评价描述(尽量详细,大于 200 字)\n" +
        "2. 严格按照下面的 json 格式输出评价名称和评价描述\n" +
        "```\n" +
        "{\"resultName\": \"评价名称\", \"resultDesc\": \"评价描述\"}\n" +
        "```\n" +
        "3. 返回格式必须为 JSON 对象";

private String getAiTestScoringUserMessage(App app, List<QuestionContentDTO> questionContentDTOList, List<String> choices) {
    StringBuilder userMessage = new StringBuilder();
    userMessage.append(app.getAppName()).append("\n");
    userMessage.append(app.getAppDesc()).append("\n");
    List<QuestionAnswerDTO> questionAnswerDTOList = new ArrayList<>();
    for (int i = 0; i < questionContentDTOList.size(); i++) {
        QuestionAnswerDTO questionAnswerDTO = new QuestionAnswerDTO();
        questionAnswerDTO.setTitle(questionContentDTOList.get(i).getTitle());
        questionAnswerDTO.setUserAnswer(choices.get(i));
        questionAnswerDTOList.add(questionAnswerDTO);
    }
    userMessage.append(JSONUtil.toJsonStr(questionAnswerDTOList));
    return userMessage.toString();
}

实现应用评分策略
@Override
public UserAnswer doScore(List<String> choices, App app) throws Exception {
    Long appId = app.getId();
    // 1. 根据 id 查询到题目
    Question question = questionService.getOne(
            Wrappers.lambdaQuery(Question.class).eq(Question::getAppId, appId)
    );
    QuestionVO questionVO = QuestionVO.objToVo(question);
    List<QuestionContentDTO> questionContent = questionVO.getQuestionContent();
    // 2. 调用 AI 获取结果
    // 封装 Prompt
    String userMessage = getAiTestScoringUserMessage(app, questionContent, choices);
    // AI 生成
    String result = aiManager.doSyncStableRequest(AI_TEST_SCORING_SYSTEM_MESSAGE, userMessage);
    // 结果处理
    int start = result.indexOf("{");
    int end = result.lastIndexOf("}");
    String json = result.substring(start, end + 1);
    // 3. 构造返回值,填充答案对象的属性
    UserAnswer userAnswer = JSONUtil.toBean(json, UserAnswer.class);
    userAnswer.setAppId(appId);
    userAnswer.setAppType(app.getAppType());
    userAnswer.setScoringStrategy(app.getScoringStrategy());
    userAnswer.setChoices(JSONUtil.toJsonStr(choices));
    return userAnswer;
}

五、性能优化

5.1 RxJava响应式编程介绍

RxJava是一种响应式编程框架,可以有效提高系统的并发处理能力和响应速度。在AI答题应用平台中,RxJava被用于优化AI生成题目和评分的流程。

观察者模式
  • RxJava基于观察者模式实现,其中包含观察者和被观察者两个角色。

  • 被观察者负责实时传输数据流,观察者观测这些数据流。

  • 用户可以通过一些操作方法对数据进行转换或其他处理。

  • 在RxJava中,观察者被称为Observer,而被观察者是Observable和Flowable。

  • Observable适合处理相对较小、可控、不会迅速产生大量数据的场景,不具备背压处理能力。

  • Flowable是针对背压(反向压力)问题设计的可观测类型,提供多种背压策略来处理数据生产速度超过数据消费速度的场景。

  • 被观察者通过subscribe(观察者)建立订阅关系,被观察者传输的数据或发出的事件会被观察者观察到。

事件

RxJava是一个基于事件驱动的框架

  1. onNext:被观察者每发送一次数据,就会触发此事件。

  2. onError:如果发送数据过程中产生意料之外的错误,那么被观察者可以发送此事件。

  3. onComplete:如果没有发生错误,那么被观察者在最后一次调用onNext之后发送此事件表示完成数据传输。

  • 对应的观察者得到这些事件后,可以进行一定处理。

flowable.observeOn(Schedulers.io())
    .doOnNext(item -> {
        System.out.println("来数据啦" + item.toString());
    })
    .doOnError(e -> {
        System.out.println("出错啦" + e.getMessage());
    })
    .doOnComplete(() -> {
        System.out.println("数据处理完啦");
    }).subscribe();

5.2 AI生成题目(RxJava响应式编程)

通过RxJava,平台可以异步处理AI生成题目的请求,避免阻塞主线程。这使得用户在等待题目生成时仍能流畅地使用平台的其他功能。

封装通用的流式调用AI接口
/**
 * 通用流式请求(简化消息传递)
 *
 * @param systemMessage
 * @param userMessage
 * @param temperature
 * @return
 */
public Flowable<ModelData> doStreamRequest(String systemMessage, String userMessage, Float temperature) {
    // 构造请求
    List<ChatMessage> messages = new ArrayList<>();
    ChatMessage systemChatMessage = new ChatMessage(ChatMessageRole.SYSTEM.value(), systemMessage);
    ChatMessage userChatMessage = new ChatMessage(ChatMessageRole.USER.value(), userMessage);
    messages.add(systemChatMessage);
    messages.add(userChatMessage);
    return doStreamRequest(messages, temperature);
}

/**
 * 通用流式请求
 *
 * @param messages
 * @param temperature
 * @return
 */
public Flowable<ModelData> doStreamRequest(List<ChatMessage> messages, Float temperature) {
    // 构造请求
    ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest.builder()
            .model(Constants.ModelChatGLM4)
            .stream(Boolean.TRUE)
            .invokeMethod(Constants.invokeMethod)
            .temperature(temperature)
            .messages(messages)
            .build();
    ModelApiResponse invokeModelApiResp = clientV4.invokeModelApi(chatCompletionRequest);
    return invokeModelApiResp.getFlowable();
}

AI生成题目的SSE接口
@GetMapping("/ai_generate/sse")
public SseEmitter aiGenerateQuestionSSE(AiGenerateQuestionRequest aiGenerateQuestionRequest) {
    ThrowUtils.throwIf(aiGenerateQuestionRequest == null, ErrorCode.PARAMS_ERROR);
    // 获取参数
    Long appId = aiGenerateQuestionRequest.getAppId();
    int questionNumber = aiGenerateQuestionRequest.getQuestionNumber();
    int optionNumber = aiGenerateQuestionRequest.getOptionNumber();
    // 获取应用信息
    App app = appService.getById(appId);
    ThrowUtils.throwIf(app == null, ErrorCode.NOT_FOUND_ERROR);

    // 封装 Prompt
    String userMessage = getGenerateQuestionUserMessage(app, questionNumber, optionNumber);
    // 建立 SSE 连接对象,0 表示不超时
    SseEmitter emitter = new SseEmitter(0L);
    // AI 生成,sse 流式返回
    Flowable<ModelData> modelDataFlowable = aiManager.doStreamRequest(GENERATE_QUESTION_SYSTEM_MESSAGE, userMessage, null);
    StringBuilder contentBuilder = new StringBuilder();
    AtomicInteger flag = new AtomicInteger(0);
    modelDataFlowable
            // 异步线程池执行
            .observeOn(Schedulers.io())
            .map(chunk -> chunk.getChoices().get(0).getDelta().getContent())
            .map(message -> message.replaceAll("\\s", ""))
            .filter(StrUtil::isNotBlank)
            .flatMap(message -> {
                // 将字符串转换为 List<Character>
                List<Character> charList = new ArrayList<>();
                for (char c : message.toCharArray()) {
                    charList.add(c);
                }
                return Flowable.fromIterable(charList);
            })
            .doOnNext(c -> {
                {
                    // 识别第一个 [ 表示开始 AI 传输 json 数据,打开 flag 开始拼接 json 数组
                    if (c == '{') {
                        flag.addAndGet(1);
                    }
                    if (flag.get() > 0) {
                        contentBuilder.append(c);
                    }
                    if (c == '}') {
                        flag.addAndGet(-1);
                        if (flag.get() == 0) {
                            // 累积单套题目满足 json 格式后,sse 推送至前端
                            // sse 需要压缩成当行 json,sse 无法识别换行
                            emitter.send(JSONUtil.toJsonStr(contentBuilder.toString()));
                            // 清空 StringBuilder
                            contentBuilder.setLength(0);
                        }
                    }
                }
            }).doOnComplete(emitter::complete).subscribe();
    return emitter;
}

5.3 AI评分优化(Redis缓存查询)

为了提高AI评分的效率,平台使用Redis缓存频繁查询的数据。例如,标准答案和评分规则可以存储在Redis中,从而减少数据库的访问次数。

为避免高频率访问的数据同时在缓存中失效,导致缓存击穿,采用分布式锁

引入所需依赖
<dependency>
  <groupId>org.redisson</groupId>
  <artifactId>redisson</artifactId>
  <version>3.21.0</version>
</dependency>
<dependency>
  <groupId>com.github.ben-manes.caffeine</groupId>
  <artifactId>caffeine</artifactId>
  <version>2.9.2</version>
</dependency>

配置Redission客户端
@Configuration
@ConfigurationProperties(prefix = "spring.redis")
@Data
public class RedissonConfig {

    private String host;

    private Integer port;

    private Integer database;

    private String password;

    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        config.useSingleServer()
        .setAddress("redis://" + host + ":" + port)
        .setDatabase(database)
        .setPassword(password);
        return Redisson.create(config);
    }
}

配置文件补充Redis配置
# Redis 配置
spring:
  redis:
    database: 0
    host: xxxx
    port: xxx
    timeout: 2000
    password: xxx

完整代码
 /**
     * 使用ai进行评分
     * @param choices
     * @param app
     * @return
     * @throws Exception
     */
    @Override
    public UserAnswer doScore(List<String> choices, App app) throws Exception {
        Long appId = app.getId();
        String choicesStr = JSONUtil.toJsonStr(choices);
        String key = buildKey(appId, choicesStr);
        String userAnswerJson = answerCacheMap.getIfPresent(key);
        // 先从本地缓存中获取结果
        if(StringUtil.isNotBlank(userAnswerJson)) {
            UserAnswer userAnswer = JSONUtil.toBean(userAnswerJson, UserAnswer.class);
            userAnswer.setAppId(appId);
            userAnswer.setAppType(app.getAppType());
            userAnswer.setScoringStrategy(app.getScoringStrategy());
            userAnswer.setChoices(choicesStr);
            return userAnswer;
        }
        // 从缓存中获取不到,再去调用 AI 评分,先获取锁
        RLock lock = redissonClient.getLock(AI_ANSWER_LOCK + key);
        try {
            boolean res = lock.tryLock(3, 15, TimeUnit.SECONDS);
            if(!res) {
                // 没有获取到锁,强行返回
               return null;
            }
                // 1. 根据 id 查询到题目
                Question question = questionService.getOne(
                        Wrappers.lambdaQuery(Question.class).eq(Question::getAppId, appId)
                );
                QuestionVO questionVO = QuestionVO.objToVo(question);
                List<QuestionContentDTO> questionContent = questionVO.getQuestionContent();

                // 2. 调用 AI 获取结果
                // 封装 Prompt
                String userMessage = getAiTestScoringUserMessage(app, questionContent, choices);
                // AI 生成
                String result = aiManager.doSyncStableRequest(AI_TEST_SCORING_SYSTEM_MESSAGE, userMessage);
                // 截取需要的 JSON 信息
                int start = result.indexOf("{");
                int end = result.lastIndexOf("}");
                String json = result.substring(start, end + 1);

                // 缓存结果
                answerCacheMap.put(key, json);
                // 3. 构造返回值,填充答案对象的属性
                UserAnswer userAnswer = JSONUtil.toBean(json, UserAnswer.class);
                userAnswer.setAppId(appId);
                userAnswer.setAppType(app.getAppType());
                userAnswer.setScoringStrategy(app.getScoringStrategy());
                userAnswer.setChoices(choicesStr);

            return userAnswer;
        }finally {
            // 释放锁
            if(lock!=null && lock.isLocked() && lock.isHeldByCurrentThread()){
                lock.unlock();
            }
        }
    }

    private String buildKey(long appId, String choices) {
        return DigestUtil.md5Hex(appId + ":"+ choices);
    }

5.4 分库分表实战(SphereSharding划分用户答题表)

随着用户数据的增长,分库分表成为优化性能的重要手段。平台通过SphereSharding将用户答题表按照插入的ID奇偶性分为两张表。这种设计可以有效分散数据存储压力,提高查询效率。

新建表
create table if not exists user_answer_xxx
(
    id              bigint auto_increment primary key,
    appId           bigint                             not null comment '应用 id',
    appType         tinyint  default 0                 not null comment '应用类型(0-得分类,1-角色测评类)',
    choiceJson      text                               not null comment '用户答案',
    resultId        bigint                             null comment '评分结果 id',
    resultName      varchar(128)                       null comment '结果名称,如物流师',
    resultDesc      text                               null comment '结果描述',
    resultPicture   varchar(1024)                      null comment '结果图标',
    resultScore     int                                null comment '得分',
    scoringStrategy tinyint  default 0                 not null comment '评分策略(0-自定义,1-AI)',
    userId          bigint                             not null comment '用户 id',
    createTime      datetime default CURRENT_TIMESTAMP not null comment '创建时间',
    updateTime      datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
    isDelete        tinyint  default 0                 not null comment '是否删除',
    index idx_appId (appId),
    index idx_userId (userId)
) comment '用户答题记录' collate = utf8mb4_unicode_ci;
引入Sharding-JDBC依赖
<dependency>
    <groupId>org.apache.shardingsphere</groupId>
    <artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
    <version>5.2.0</version>
</dependency>
在application.yaml中配置参数
spring:
  shardingsphere:
    #数据源配置
    datasource:
      # 多数据源以逗号隔开即可
      names: yudada
      yudada:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://localhost:3306/yudada?allowPublicKeyRetrieval=true&useSSL=false&autoReconnect=true&characterEncoding=utf8
        username: root
        password: 123456
    # 规则配置
    rules:
      sharding:
        # 分片算法配置
        sharding-algorithms:
          # 自定义分片规则名
          answer-table-inline:
            ## inline 类型是简单的配置文件里面就能写的类型,其他还有自定义类等等
            type: INLINE
            props:
              algorithm-expression: user_answer_$->{appId % 2}
        tables:
          user_answer:
            actual-data-nodes: yudada.user_answer_$->{0..1}
            # 分表策略
            table-strategy:
              standard:
                sharding-column: appId
                sharding-algorithm-name: answer-table-inline
  1. 数据源需要被配置在 shardingsphere 下。

  2. 需要设定数据源的名称和URL等配置信息。

  3. 自定义分片规则,命名为 answer-table-inline。分片算法定义为 user_answer_$->{appld % 2},这个算法的含义是根据 appld 值对2取余的结果来拼接表名,以此改写SQL。

  4. 设置对应的表使用分片规则,即 tables:user_answer:table-strategy,指定分片键为 appld,分片的规则是 answer-table-inline

总结

AI答题应用平台的开发过程涉及多个领域的技术应用,从数据库设计到后端开发,再到智能化应用和性能优化,每一步都至关重要。通过合理的技术选型和优化策略,平台不仅能够提供高效的答题体验,还能通过AI技术为用户提供个性化的学习支持。未来,随着AI技术的进一步发展,该平台有望在教育领域发挥更大的作用。

文章持续跟新,可以微信搜一搜公众号  rain雨雨编程 ],第一时间阅读,涉及数据分析,机器学习,Java编程,爬虫,实战项目等。

Logo

GitCode 天启AI是一款由 GitCode 团队打造的智能助手,基于先进的LLM(大语言模型)与多智能体 Agent 技术构建,致力于为用户提供高效、智能、多模态的创作与开发支持。它不仅支持自然语言对话,还具备处理文件、生成 PPT、撰写分析报告、开发 Web 应用等多项能力,真正做到“一句话,让 Al帮你完成复杂任务”。

更多推荐