❤ 作者主页:李奕赫揍小邰的博客
❀ 个人介绍:大家好,我是李奕赫!( ̄▽ ̄)~*
🍊 记得点赞、收藏、评论⭐️⭐️⭐️
📣 认真学习!!!🎉🎉
文章目录
最近做的一个AI应用答题平台,其中有应用类型分为两种,一种是测评类例如MBIT测试,一种是打分类例如科普知识得分等。并且其中评分策略也有两种,一种直接计算得出结论,一种用AI生成结论评分。因此这种生成评分结果的方法正好可以使用策略模式来实现,接下来将会用策略模式逐渐解析这个问题
策略接口
需求:针对不同的应用类别和评分策略,编写不同的实现逻辑。
策略模式是一种行为设计模式,它定义了一系列算法,并将每个算法封装到独立的类中,使得它们可以相互替换。在本项目的场景中,输入的参数是一致的(应用和用户的答案列表),并且每种实现逻辑区别较大,比较适合使用策略模式。
java
/**
*评分策略接口
*/
public interface ScoringStrategy {
/**
* 执行评分
*/
UserAnswer doScore(List<String> choices, App app) throws Exception;
}
三种策略实现
之前讲过,应用分两种,测评类和打分类,题目评分分为两种,自定义评分和AI评分。因此我们可以组成三种策略形式,自定义测评,自定义打分,AI测评三种类型,当然后续要是有更多类型,可以依次累加。
java
/**
*自定义测评策略
*/
public class CustomTestScoringStrategy 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)
);
// 2. 统计用户每个选择对应的属性个数,如 I = 10 个,E = 5 个
// 初始化一个Map,用于存储每个选项的计数
Map<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();
// 如果result属性不在optionCount中,初始化为0
if (!optionCount.containsKey(result)) {
optionCount.put(result, 0);
}
// 在optionCount中增加计数
optionCount.put(result, optionCount.get(result) + 1);
}
}
}
}
// 3. 遍历每种评分结果,计算哪个结果的得分更高
// 初始化最高分数和最高分数对应的评分结果
int maxScore = 0;
ScoringResult maxScoringResult = scoringResultList.get(0);
// 遍历评分结果列表
for (ScoringResult scoringResult : scoringResultList) {
List<String> resultProp = JSONUtil.toList(scoringResult.getResultProp(), String.class);
int score = resultProp.stream()
.mapToInt(prop -> optionCount.getOrDefault(prop, 0))
.sum();
if (score > maxScore) {
maxScore = score;
maxScoringResult = scoringResult;
}
}
// 4. 构造返回值,填充答案对象的属性
UserAnswer userAnswer = new UserAnswer();
userAnswer.setAppId(appId); //赋值省略
return userAnswer;
}
}
java
/**
*自定义打分策略
*/
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 (int i = 0; i < questionContent.size(); i++) {
QuestionContentDTO questionContentDTO = questionContent.get(i);
String answer = choices.get(i);
// 遍历题目中的选项
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); //赋值略
return userAnswer;
}
}
java
/**
*AI测评策略
*/
public class AiTestScoringStrategy implements ScoringStrategy {
@Resource
private QuestionService questionService;
@Resource
private AiManager aiManager;
@Resource
private RedissonClient redissonClient;
@Override
public UserAnswer doScore(List<String> choices, App app) throws Exception {
Long appId = app.getId();
String jsonStr = JSONUtil.toJsonStr(choices);
String cacheKey = buildCacheKey(appId, jsonStr);
String answerJson = answerCacheMap.getIfPresent(cacheKey);
// 如果有缓存,直接返回
if (StrUtil.isNotBlank(answerJson)) {
// 构造返回值,填充答案对象的属性
UserAnswer userAnswer = JSONUtil.toBean(answerJson, UserAnswer.class);
userAnswer.setAppId(appId);
userAnswer.setAppType(app.getAppType());
userAnswer.setScoringStrategy(app.getScoringStrategy());
userAnswer.setChoices(jsonStr);
return userAnswer;
}
// 定义锁
RLock lock = redissonClient.getLock(AI_ANSWER_LOCK + cacheKey);
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(cacheKey, json);
// 3. 构造返回值,填充答案对象的属性
UserAnswer userAnswer = JSONUtil.toBean(json, UserAnswer.class);
userAnswer.setAppId(appId);
userAnswer.setAppType(app.getAppType());
userAnswer.setScoringStrategy(app.getScoringStrategy());
userAnswer.setChoices(jsonStr);
return userAnswer;
} finally {
if (lock != null && lock.isLocked()) {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
}
以上是三种继承策略接口的doScore实现类
全局执行器
为了简化外部调用,需要根据不同的应用类别和评分策略,选择对应的策略执行,因此需要一个全局执行器。
2 种实现方式:
1)编程式,在内部计算选用何种策略:
java
@Service
@Deprecated
public class ScoringStrategyContext {
@Resource
private CustomScoreScoringStrategy customScoreScoringStrategy;
@Resource
private CustomTestScoringStrategy customTestScoringStrategy;
/**
* 评分
*/
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:
break;
}
break;
}
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "应用配置有误,未找到匹配的策略");
}
}
优点是直观清晰,缺点是不利于扩展和维护。直接使用switch和case进行判断,适用于少量且类型不怎么更改的样式。
2)声明式,在每个策略类中通过接口声明对应的生效条件,适合比较规律的策略选取场景。
接口:
java
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Component
public @interface ScoringStrategyConfig {
/**
* 应用类型
* @return
*/
int appType();
/**
* 评分策略
* @return
*/
int scoringStrategy();
}
给策略实现类补充注解,将不同的策略实现类添加注解对应用类型和评分方式进行赋值:
java
@ScoringStrategyConfig(appType = 0, scoringStrategy = 0)
全局执行器
java
/**
* 评分策略执行器
*/
@Service
public class ScoringStrategyExecutor {
// 策略列表
@Resource
private List<ScoringStrategy> scoringStrategyList;
/**
* 评分
*/
public UserAnswer doScore(List<String> choiceList, App app) throws Exception {
Integer appType = app.getAppType();
Integer appScoringStrategy = app.getScoringStrategy();
if (appType == null || appScoringStrategy == null) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "应用配置有误,未找到匹配的策略");
}
// 根据注解获取策略
for (ScoringStrategy strategy : scoringStrategyList) {
if (strategy.getClass().isAnnotationPresent(ScoringStrategyConfig.class)) {
ScoringStrategyConfig scoringStrategyConfig = strategy.getClass().getAnnotation(ScoringStrategyConfig.class);
if (scoringStrategyConfig.appType() == appType && scoringStrategyConfig.scoringStrategy() == appScoringStrategy) {
return strategy.doScore(choiceList, app);
}
}
}
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "应用配置有误,未找到匹配的策略");
}
}
因为用了ScoringStrategyConfig注解,所以这个实现类被加上了component注解,因此可以被spring管理扫描到。然后@Resoure注入的时候,会通过ScoringStrategy类型找到所有实现ScoringStrategy接口的实现类
之后直接调用策略全局执行器即可调用不同doScore方法。
java
UserAnswer userAnswerWithResult = scoringStrategyExecutor.doScore(choices, app);
这样之后就算新增加应用类型和评分方式,我们代码中执行器和调用方法也不用修改,只需要添加继承策略接口的实现类,给策略实现类补充注解即可