目录
一、接入AI模块
在智谱 AI 开放平台页面 进行题目生成的演示,然后后端 sdk 引入。
1.智谱 AI 介绍
智谱 AI 开放平台:智谱AI开放平台
创建应用:应用 = 预定义了一些规则的大模型
在线体验:可用来测试不同大模型和参数的效果,支持 AI 绘画
2.使用智谱 AI SDK
需要阅读官方文档
使用指南:智谱AI开放平台
可以看到很多能力,比如知识库、网络搜索等
接口文档:智谱AI开放平台
有 3 种调用方式,SDK、HTTP 和第三方框架。
比较推荐 SDK,方便,不用自己构造 HTTP 请求并构造响应对象。
跟着官方文档去学习使用即可:智谱AI开放平台
Java 中使用 SDK
1)引入 maven:
<dependency>
<groupId>cn.bigmodel.openapi</groupId>
<artifactId>oapi-java-sdk</artifactId>
<version>release-V4-2.0.2</version>
</dependency>
2)创建单元测试来跑通 Demo
注意配置 apiKey,在智谱开放平台查看:智谱AI开放平台
package com.yupi.yudada;
import com.zhipu.oapi.ClientV4;
import com.zhipu.oapi.Constants;
import com.zhipu.oapi.service.v4.model.ChatCompletionRequest;
import com.zhipu.oapi.service.v4.model.ChatMessage;
import com.zhipu.oapi.service.v4.model.ChatMessageRole;
import com.zhipu.oapi.service.v4.model.ModelApiResponse;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
@SpringBootTest
public class ZhiPuAiTest {
@Resource
private ClientV4 clientV4;
@Test
public void test() {
// 初始化客户端
// ClientV4 client = new ClientV4.Builder(KeyConstant.KEY).build();
// 构建请求
List<ChatMessage> messages = new ArrayList<>();
ChatMessage chatMessage = new ChatMessage(ChatMessageRole.USER.value(), "作为一名营销专家,请为智谱开放平台创作一个吸引人的slogan");
messages.add(chatMessage);
// String requestId = String.format(requestIdTemplate, System.currentTimeMillis());
ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest.builder()
.model(Constants.ModelChatGLM4)
.stream(Boolean.FALSE)
.invokeMethod(Constants.invokeMethod)
.messages(messages)
.build();
ModelApiResponse invokeModelApiResp = clientV4.invokeModelApi(chatCompletionRequest);
System.out.println("model output:" + invokeModelApiResp.getData().getChoices().get(0));
}
}
每个参数的含义可以参考官方文档:智谱AI开放平台
其他用法
异步调用:相当于提交任务,可以之后查询任务状态并获取结果。
SSE 调用:流式、实时地获取到结果直到结束,可以优化用户体验。
3、封装 AI 模块
目的是更快速灵活地调用 AI 的能力,各项目都可以复用。
1)在 application.yml 中定义 AI 配置
ai:
apiKey: xxx
2)定义 AI 配置类,加载配置文件,初始化调用智谱的 Client 并将其注册为 bean。
@Configuration
@ConfigurationProperties(prefix = "ai")
@Data
public class AiConfig {
/**
* apiKey,需要从开放平台获取
*/
private String apiKey;
@Bean
public ClientV4 getClientV4() {
return new ClientV4.Builder(apiKey).build();
}
}
可以自己定制客户端的一些配置,后续 service 即可注入使用,不用再重复创建 Client 了。
3)智谱 SDK 提供的客户端已经很方便了,但我们仍然可以根据实际业务需求,封装本项目内通用的 AI 请求构建模块。
比如将复杂的消息、请求构建、从返回值中获取结果的过程进行封装。
一般建议先封装通用的方法,然后提供不同参数默认值的简化调用方法,层层简化。
最通用的方法,可自定义消息、是否流式、随机数:
在manager包下(区别于工具类给整个项目提供一种能力)
/**
* 通用请求
*
* @param messages
* @param stream
* @param temperature
* @return
*/
public String doRequest(List<ChatMessage> messages, Boolean stream, Float temperature) {
// 构建请求
ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest.builder()
.model(Constants.ModelChatGLM4)
.stream(stream)
.temperature(temperature)
.invokeMethod(Constants.invokeMethod)
.messages(messages)
.build();
try {
ModelApiResponse invokeModelApiResp = clientV4.invokeModelApi(chatCompletionRequest);
return invokeModelApiResp.getData().getChoices().get(0).toString();
} catch (Exception e) {
e.printStackTrace();
throw new BusinessException(ErrorCode.SYSTEM_ERROR, e.getMessage());
}
}
通用请求,简化了消息传递:
/**
* 通用请求(简化消息传递)
*
* @param systemMessage
* @param userMessage
* @param stream
* @param temperature
* @return
*/
public String doRequest(String systemMessage, String userMessage, Boolean stream, Float temperature) {
List<ChatMessage> chatMessageList = new ArrayList<>();
ChatMessage systemChatMessage = new ChatMessage(ChatMessageRole.SYSTEM.value(), systemMessage);
chatMessageList.add(systemChatMessage);
ChatMessage userChatMessage = new ChatMessage(ChatMessageRole.USER.value(), userMessage);
chatMessageList.add(userChatMessage);
return doRequest(chatMessageList, stream, temperature);
}
通用同步请求:
/**
* 同步请求
*
* @param systemMessage
* @param userMessage
* @param temperature
* @return
*/
public String doSyncRequest(String systemMessage, String userMessage, Float temperature) {
return doRequest(systemMessage, userMessage, Boolean.FALSE, temperature);
}
预置两种不同稳定性的方法,以满足我们之后的需求。
- AI 生成题目不应该稳定(需提高随机性),让思维发散
- AI 评分需要稳定(降低随机性)避免相同的答案每次得到不一样的分析结果
// 稳定的随机数
private static final float STABLE_TEMPERATURE = 0.05f;
// 不稳定的随机数
private static final float UNSTABLE_TEMPERATURE = 0.99f;
/**
* 同步请求(答案不稳定)
*
* @param systemMessage
* @param userMessage
* @return
*/
public String doSyncUnstableRequest(String systemMessage, String userMessage) {
return doRequest(systemMessage, userMessage, Boolean.FALSE, UNSTABLE_TEMPERATURE);
}
/**
* 同步请求(答案较稳定)
*
* @param systemMessage
* @param userMessage
* @return
*/
public String doSyncStableRequest(String systemMessage, String userMessage) {
return doRequest(systemMessage, userMessage, Boolean.FALSE, STABLE_TEMPERATURE);
}
这样做之后,不仅满足了本项目的需求,其他项目也可以复用部分方法。
整个项目可以引入 AI 的地方有很多,核心使用场景是 “AI 生成题目” 和 “AI 智能评分”。
二、AI 生成题目
1.需求分析
原本创建题目需要人工一个个添加标题和选项,比较麻烦。
可以使用 AI,根据已经填写的应用信息,自动生成题目,然后再由人工进行编辑确认,提高创建题目的效率。
2.方案设计
AI 生成内容的核心是编写 Prompt,好的、精准的 Prompt 才能帮助我们得到预期的结果。
首先明确我们能提供或者需要输入给 AI 的参数,然后构建 Prompt 并输入给 AI,让 AI 生成题目并处理成我们需要的格式。
1、明确输入参数
明确需要提供给 AI 的参数列表:
- 应用信息,包括应用名称、描述、类别(得分 / 测评)
- 题目信息,包括要生成的题目数、每题选项数
当然,还可以有其他信息,比如限定单个选项的得分范围、选项对应的属性集合等。
不过因为 AI 给出的结果存在随机性,建议大家先一切从简,只提供核心参数,保证 AI 输出的结果尽可能地稳定。
2、明确AI返回结果
需要 JSON 格式的题目列表,比如:
3、编写和调试 Prompt
推荐阅读智谱 AI 官方提供的 Prompt 工程指南:智谱AI开放平台
对于精准度要求不高的生成需求,可以先通过智谱 AI 体验中心进行简单调试。但如果要追求更准确的结果,最好使用 SDK 调用来调试,因为 web 端和通过 SDK 调用的效果可能有细微差别。
遵循 Prompt 工程指南中的技巧,编写出系统 Prompt 和用户 Prompt。
系统 Prompt 如下:
你是一位严谨的出题专家,我会给你如下信息:
```
应用名称,
【【【应用描述】】】,
应用类别,
要生成的题目数,
每个题目的选项数
```
请你根据上述信息,按照以下步骤来出题:
1. 要求:题目和选项尽可能地短,题目不要包含序号,每题的选项数以我提供的为主,题目不能重复
2. 严格按照下面的 json 格式输出题目和选项
```
[{"options":[{"value":"选项内容","key":"A"},{"value":"","key":"B"}],"title":"题目标题"}]
```
title 是题目,options 是选项,每个选项的 key 按照英文字母序(比如 A、B、C、D)以此类推,value 是选项内容
3. 检查题目是否包含序号,若包含序号则去除序号
4. 返回的题目列表格式必须为 JSON 数组
用户 Prompt 按照顺序提供信息即可,示例 Prompt 如下。
测评类应用:
MBTI 性格测试,
【【【快来测测你的 MBTI 性格】】】,
测评类,
10,
3
得分类应用:
小学数学测验,
【【【小学三年级的数学题】】】,
得分类,
10,
3
用到的技巧:
- 定义 System Prompt
- 让 GLM 进行角色扮演
- 使用分隔符标示不同的输入部分
- 少样本学习
- 指定输出长度的示例
- 将复杂任务分解为简单的子任务
- 指定固定的输出格式
- 通过按顺序输入参数来节约空间。
- 对于描述这种多行内容,可以通过特殊字符括起来,防止用户的输入干扰生成结果。
先利用界面进行初步调试,分别调试测评类和得分类应用:
然后编写单元测试(调用之前封装的方法并查看其返回的结果)来进一步调试,得到的结果可能会包含额外的开头和结尾字符串。
对应的解决方案是,利用字符串查找,找到开头的 '[' 和结尾的 ']',进行截取。
最后,将 JSON 字符串转换为题目对象,返回给前端即可。
4、注意事项
任何大模型都有单次提问 token 限制,注意题目和选项数不能太多,太多的话可以分批生成。
经过尝试,建议最多每 20 道题生成调用一次 AI 接口。
Token:是模型用来表示自然语言文本的基本单位,可以直观的理解为“字”或“词”;通常1个中文词语、1个英文单词、1个数字或1个符号计为 1 个token。
ChatGLM 的最大单次调用的 token 量和对应的汉字数是多少?大概单次消耗多少 token?
一般情况下 ChatGLM 模型中 token 和字数的换算比例约为 1:1.6,但因为不同模型的分词不同,所以换算比例也存在差异,每一次实际处理 token 数量以模型返回为准,可以从返回结果的 usage 中查看。
经过测试,生成 10 道题目导致需要消耗 1700 左右的 token,10 道题的智能评分大致 600 左右的 token。
上下文最大 tokens 量是 128k(不同的模型不一样,要以官方文档为准),输出 token 数量还受到请求参数 max_tokens 的限制,默认1024,最大 8192,不建议超过 4096,超 4096 会显著降低接口响应速度。
3.后端开发
按照上述方案,进行编码实现。
本节先使用同步的方式调用 AI,后续会进行优化。
1)AI 生成题目请求类参数:
/**
* AI 生成题目请求
*/
@Data
public class AiGenerateQuestionRequest implements Serializable {
/**
* 应用 id
*/
private Long appId;
/**
* 题目数
*/
int questionNumber = 10;
/**
* 选项数
*/
int optionNumber = 2;
private static final long serialVersionUID = 1L;
}
2)定义模板常量和构造用户模板的方法(controller):
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 数组";
/**
* 生成题目的用户消息
*
* @param app
* @param questionNumber
* @param optionNumber
* @return
*/
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();
}
3)AI 生成接口(不写在service层的接口,业务不复杂):
@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.doSyncRequest(GENERATE_QUESTION_SYSTEM_MESSAGE, userMessage, null);
// 截取需要的 JSON 信息
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.前端开发
效果如图:
1.抽屉组件
允许用户填写生成参数并执行,生成成功后回调题目页面(父组件)的函数,插入题目。
代码如下:
<template>
<a-button type="outline" @click="handleClick">AI 生成题目</a-button>
<a-drawer
:width="340"
:visible="visible"
@ok="handleOk"
@cancel="handleCancel"
unmountOnClose
>
<template #title>AI 生成题目</template>
<div>
<a-form
:model="form"
label-align="left"
auto-label-width
@submit="handleSubmit"
>
<a-form-item label="应用 id">
{{ appId }}
</a-form-item>
<a-form-item field="questionNumber" label="题目数量">
<a-input-number
min="0"
max="20"
v-model="form.questionNumber"
placeholder="请输入题目数量"
/>
</a-form-item>
<a-form-item field="optionNumber" label="选项数量">
<a-input-number
min="0"
max="6"
v-model="form.optionNumber"
placeholder="请输入选项数量"
/>
</a-form-item>
<a-form-item>
<a-button
:loading="submitting"
type="primary"
html-type="submit"
style="width: 120px"
>
{{ submitting ? "生成中" : "一键生成" }}
</a-button>
</a-form-item>
</a-form>
</div>
</a-drawer>
</template>
<script setup lang="ts">
import { defineProps, reactive, ref, withDefaults } from "vue";
import API from "@/api";
import { aiGenerateQuestionUsingPost } from "@/api/questionController";
import message from "@arco-design/web-vue/es/message";
interface Props {
appId: string;
onSuccess?: (result: API.QuestionContentDTO[]) => void;
}
const props = withDefaults(defineProps<Props>(), {
appId: () => {
return "";
},
});
const form = reactive({
optionNumber: 2,
questionNumber: 10,
} as API.AiGenerateQuestionRequest);
const visible = ref(false);
const submitting = ref(false);
const handleClick = () => {
visible.value = true;
};
const handleOk = () => {
visible.value = false;
};
const handleCancel = () => {
visible.value = false;
};
/**
* 提交
*/
const handleSubmit = async () => {
if (!props.appId) {
return;
}
submitting.value = true;
const res = await aiGenerateQuestionUsingPost({
appId: props.appId as any,
...form,
});
if (res.data.code === 0 && res.data.data.length > 0) {
if (props.onSuccess) {
props.onSuccess(res.data.data);
} else {
message.success("生成题目成功");
}
// 关闭抽屉
handleCancel();
} else {
message.error("操作失败," + res.data.message);
}
submitting.value = false;
};
</script>
编写 AI 生成题目成功的处理,插入题目:
/**
* AI 生成题目成功后执行
*/
const onAiGenerateSuccess = (result: API.QuestionContentDTO[]) => {
message.success(`AI 生成题目成功,生成 ${result.length} 道题目`);
questionContent.value = [...questionContent.value, ...result];
};
2.引入组件
设置题目页面引入抽屉组件:
5.验证测试
1)AddAppPage 创建应用成功后无法正确跳转到详情页 Bug
因为 id 的默认值为空字符串导致,需将 "??" 改为 "||",代码如下:
2)AI 生成中断,需要调整请求配置中的超时时间
3)实际测试的生成结果有点太飘(随机)了,如图:
还是将随机性调整为默认值:
// AI 生成
String result = aiManager.doSyncRequest(GENERATE_QUESTION_SYSTEM_MESSAGE, userMessage, null);
调整参数后,生成的结果正常多了:
6.扩展思路
1)可以将历史生成的题目标题关联到上下文中,让 AI 排除掉重复题目的生成。建议做一个通用的支持上下文自动关联并调用的组件。
示例代码仅供参考:
private String AICreateTitle(int number) {
String appAiSysMessageConfig = "系统提示词";
String appAiUserMessageConfig = "用户提示词";
// 经过测试,需要每 20 道题一组,ChatGLM 拒绝一次性输出大量题目
List<ChatMessage> assistantChatMessageList = new ArrayList<>();
while (number >= 10) {
invokeAiLoop(number, appAiSysMessageConfig, appAiUserMessageConfig, assistantChatMessageList);
number = number - 10;
}
if (number > 0) {
invokeAiLoop(number, appAiSysMessageConfig, appAiUserMessageConfig, assistantChatMessageList);
}
// 将结果合并成一个 List 返回
JSONArray result = new JSONArray();
for (ChatMessage message : assistantChatMessageList) {
JSONArray jsonArray = JSONArray.parseArray(String.valueOf(message.getContent()));
result.addAll(jsonArray);
}
return JSON.toJSONString(result);
}
private void invokeAiLoop(int number, String appAiSysMessageConfig, String appAiUserMessageConfig, List<ChatMessage> assistantChatMessages) {
// 每次需要重建request,因为一次调用后socket通道已经被关闭,复用老的 request 会复用老链接会报错
ChatCompletionRequest chatCompletionRequest = ChatGLMUtil.buildSyncUnStableRequest(appAiSysMessageConfig, String.format(appAiUserMessageConfig, Math.min(number, 10)));
chatCompletionRequest.getMessages().addAll(assistantChatMessages);
ModelApiResponse invokeModelApiResp = client.invokeModelApi(chatCompletionRequest);
String s = invokeModelApiResp.getData().getChoices().get(0).getMessage().getContent().toString();
int start = s.indexOf("[");
int end = s.lastIndexOf("]");
String content = s.substring(start, end + 1);
// 保存上下文
assistantChatMessages.add(new ChatMessage(ChatMessageRole.ASSISTANT.value(), content));
}
2)对于测评类应用,给定属性集,生成题目选项对应的属性。
3)对于得分类应用,给定得分范围,生成题目选项对应的得分。
4)思考如何进一步优化 Prompt,比如通过简化 JSON key 的长度来节约 token 数。
三、AI 智能评分
原本题目评分需要让应用创建者自己创建评分结果,并且给题目选项设置得分和对应的属性,比较麻烦。
可以使用 AI,根据应用信息、题目和用户的答案进行评分,直接返回评分结果。
这种评分策略更适用于测评类应用,提高创建应用效率的同时,给结果更多的可能性。
所以下面我们主要实现测评类应用的 AI 评分策略,暂时不关注得分类应用的 AI 评分结果,
也可以在前端做一些控制,如果是得分类应用,不支持选择 AI 评分策略等。
1.方案设计
1、明确输入参数
明确需要提供给 AI 的参数列表:
- 应用信息,包括应用名称、描述
- 题目信息
- 用户答案
但是需要注意,题目数量可能会很多,如果将完整的题目结构(包括选项列表)输入给 AI,可能会超出最大 token 限制,所以可以进行优化。只存储用户答案对应的选项。
示例结构如下:
2、明确返回结果
需要 JSON 格式的评分结果对象,比如:
3、编写和调试 Prompt
遵循 Prompt 工程指南中的技巧,编写出系统 Prompt 和用户 Prompt。
系统 Prompt 如下:
用户 Prompt 按照顺序提供信息即可,示例 Prompt 如下。
测评类应用:
用到的技巧:
- 定义 System Prompt
- 让 GLM 进行角色扮演
- 使用分隔符标示不同的输入部分
- 少样本学习
- 指定输出长度的示例
- 将复杂任务分解为简单的子任务
- 指定固定的输出格式
- 通过按顺序输入参数来节约空间。
- 对于描述这种多行内容,可以通过特殊字符括起来,防止用户的输入干扰生成结果。
利用界面进行初步调试,可以尝试修改随机性:
然后可以通过编写单元测试来调试。
得到结果后,仍然需要利用字符串查找,找到开头的 '{' 和结尾的 '}',截取到实际内容。
2.后端开发
1)编写题目答案封装类:
/**
* 题目答案封装类(用于 AI 评分)
*/
@Data
public class QuestionAnswerDTO {
/**
* 题目
*/
private String title;
/**
* 用户答案
*/
private String userAnswer;
}
2)编写 AI 测评类应用评分策略类,需要指定对应的注解:
@ScoringStrategyConfig(appType = 1, scoringStrategy = 1)
定义模板常量和构造用户模板的方法:
/**
* AI 评分系统消息
*/
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 对象";
/**
* AI 评分用户消息封装
*
* @param app
* @param questionContentDTOList
* @param choices
* @return
*/
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);
// 截取需要的 JSON 信息
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;
}
3.前端开发
前端不需要开发新的页面,给做题页面的“查看结果”按钮补充一个提交等待的 loading 效果即可。
4.验证测试
效果如图:
可以多次测试,调试 Prompt 和模型参数,以便获得更好的结果。
5.扩展思路
1)实现得分类应用的 AI 评分,可以给 AI 多传递每道题目正确的得分参数。
2)支持用户额外定义结果集,将 AI 生成的结果限定在一个范围内,更精确。