平台智能化

本节重点

平台智能化

  • 接入 AI 模块
    • 智谱 AI 介绍
    • 使用智谱 AI SDK
    • 封装 AI 模块
  • AI 生成题目
    • 需求分析
    • 方案设计(实现原理),包括使用智谱页面调试 Prompt
    • 后端开发
    • 前端开发
  • AI 智能评分
    • 需求分析
    • 方案设计(实现原理),包括使用智谱页面调试 Prompt
    • 后端开发
    • 前端开发
  • 扩展知识
    • Spring AI
    • 智谱 AI + Spring AI 整合应用

一、接入 AI 模块

在智谱 AI 开放平台页面 进行题目生成的演示,然后后端 sdk 引入

1、智谱 AI 介绍

智谱 AI 开放平台:https://open.bigmodel.cn/overview

大模型介绍

AI 的应用场景

知识库管理

创建应用:应用 = 预定义了一些规则的大模型

在线体验:可用来测试不同大模型和参数的效果,支持 AI 绘画

BWPSmrEYtmbf8Pvr-ed1e5fbe

2、使用智谱 AI SDK

需要阅读官方文档

使用指南:https://open.bigmodel.cn/dev/howuse/introduction

可以看到很多能力,比如知识库、网络搜索等

接口文档:https://open.bigmodel.cn/dev/api

有 3 种调用方式,SDK、HTTP 和第三方框架。

比较推荐 SDK,方便,不用自己构造 HTTP 请求并构造响应对象。

跟着官方文档去学习使用即可:https://open.bigmodel.cn/dev/api#sdk_install

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,在智谱开放平台查看:https://open.bigmodel.cn/usercenter/apikeys

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 java.util.ArrayList;
import java.util.List;

@SpringBootTest
public class ZhiPuAiTest {

    @Test
    public void test() {
        String apiKey = "";
        // 创建客户端
        ClientV4 client = new ClientV4.Builder(apiKey).build();
        // 构造请求
        List<ChatMessage> messages = new ArrayList<>();
        ChatMessage chatMessage = new ChatMessage(ChatMessageRole.USER.value(), "作为一名营销专家,请为智谱开放平台创作一个吸引人的slogan");
        messages.add(chatMessage);
        String requestId = String.valueOf(System.currentTimeMillis());
        ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest.builder()
                .model(Constants.ModelChatGLM4)
                .stream(Boolean.FALSE)
                .invokeMethod(Constants.invokeMethod)
                .messages(messages)
                .requestId(requestId)
                .build();
        // 调用
        ModelApiResponse invokeModelApiResp = client.invokeModelApi(chatCompletionRequest);
        System.out.println("model output:" + invokeModelApiResp.getMsg());
    }
}

每个参数的含义可以参考官方文档:https://open.bigmodel.cn/dev/api#glm-4

3)Debug 程序,如果无法 Debug,开启如下配置

UYDNioSbamwr973n-27cba69b

查看到 AI 返回的结果如图:

JTHBGA5JxLz3gGXN-87cdb924

其他用法

异步调用:相当于提交任务,可以之后查询任务状态并获取结果。

SSE 调用:流式、实时地获取到结果直到结束,可以优化用户体验。

3、封装 AI 模块

目的是更快速灵活地调用 AI 的能力,各项目都可以复用。

1)在 application.yml 中定义 AI 配置

ai:
  apiKey: xxx

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

@Configuration
@ConfigurationProperties(prefix = "ai")
@Data
public class AiConfig {

    private String apiKey;

    @Bean
    public ClientV4 getClientV4() {
        return new ClientV4.Builder(apiKey).build();
    }
}

可以自己定制客户端的一些配置,后续 service 即可注入使用,不用再重复创建 Client 了。

jAUhrSgYifFKt1qd-0a495761

3)智谱 SDK 提供的客户端已经很方便了,但我们仍然可以根据实际业务需求,封装本项目内通用的 AI 请求构建模块。

比如将复杂的消息、请求构建、从返回值中获取结果的过程进行封装。

一般建议先封装通用的方法,然后提供不同参数默认值的简化调用方法,层层简化。

最通用的方法,可自定义消息、是否流式、随机数:

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();
}

通用请求,简化了消息传递:

public String doRequest(String systemMessage, String userMessage, Boolean stream, 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 doRequest(messages, stream, temperature);
}

通用同步请求:

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 doSyncStableRequest(String systemMessage, String userMessage) {
    return doSyncRequest(systemMessage, userMessage, STABLE_TEMPERATURE);
}

/**
 * 同步调用(答案较随机)
 *
 * @param systemMessage
 * @param userMessage
 * @return
 */
public String doSyncUnstableRequest(String systemMessage, String userMessage) {
    return doSyncRequest(systemMessage, userMessage, UNSTABLE_TEMPERATURE);
}

这样做之后,不仅满足了本项目的需求,其他项目也可以复用部分方法。


整个项目可以引入 AI 的地方有很多,核心使用场景是 “AI 生成题目” 和 “AI 智能评分”。

二、AI 生成题目

需求分析

原本创建题目需要人工一个个添加标题和选项,比较麻烦。

可以使用 AI,根据已经填写的应用信息,自动生成题目,然后再由人工进行编辑确认,提高创建题目的效率。

方案设计

AI 生成内容的核心是编写 Prompt,好的、精准的 Prompt 才能帮助我们得到预期的结果。

首先明确我们能提供或者需要输入给 AI 的参数,然后构建 Prompt 并输入给 AI,让 AI 生成题目并处理成我们需要的格式。

1、明确输入参数

明确需要提供给 AI 的参数列表:

  1. 应用信息,包括应用名称、描述、类别(得分 / 测评)
  2. 题目信息,包括要生成的题目数、每题选项数

当然,还可以有其他信息,比如限定单个选项的得分范围、选项对应的属性集合等。

不过因为 AI 给出的结果存在随机性,建议大家先一切从简,只提供核心参数,保证 AI 输出的结果尽可能地稳定。

2、明确返回结果

需要 JSON 格式的题目列表,比如:

[
  {
    "options": [
      {
        "result": "I",
        "value": "独自工作",
        "key": "A"
      },
      {
        "result": "E",
        "value": "与他人合作",
        "key": "B"
      }
    ],
    "title": "你通常更喜欢"
  }
]

3、编写和调试 Prompt

推荐阅读智谱 AI 官方提供的 Prompt 工程指南:https://open.bigmodel.cn/dev/howuse/prompt

6dTw42G0oDVaZBGD-9ae54021

对于精准度要求不高的生成需求,可以先通过智谱 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

用到的技巧:

  1. 定义 System Prompt
  2. 让 GLM 进行角色扮演
  3. 使用分隔符标示不同的输入部分
  4. 少样本学习
  5. 指定输出长度的示例
  6. 将复杂任务分解为简单的子任务
  7. 指定固定的输出格式
  8. 通过按顺序输入参数来节约空间。
  9. 对于描述这种多行内容,可以通过特殊字符括起来,防止用户的输入干扰生成结果。

先利用界面进行初步调试,分别调试测评类和得分类应用:

fNHNZa3JpzFVAyCl-c661c5e6 WNkTiYDIvaaQ1hiG-2d49faa0

然后编写单元测试来进一步调试,得到的结果可能会包含额外的开头和结尾字符串。

8hQH7FRCtuIxwXMD-301b8973

对应的解决方案是,利用字符串查找,找到开头的 '[' 和结尾的 ']',进行截取。

最后,将 JSON 字符串转换为题目对象,返回给前端即可。

lu8aT9Z8uKZtATEE-c6934e3a

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 会显著降低接口响应速度。

后端开发

按照上述方案,进行编码实现。

本节先使用同步的方式调用 AI,后续会进行优化。

1)AI 生成题目请求类:

@Data
public class AiGenerateQuestionRequest implements Serializable {

    /**
     * id
     */
    private Long appId;

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

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

    private static final long serialVersionUID = 1L;
}

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

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();
}

3)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);
}

前端开发

效果如图:

XpRL8m7lfx8adx00-181f18b4

抽屉组件

允许用户填写生成参数并执行,生成成功后回调题目页面(父组件)的函数,插入题目。

代码如下:

<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
            v-model="form.questionNumber"
            min="0"
            max="20"
            placeholder="请输入题目数量"
          />
        </a-form-item>
        <a-form-item field="optionNumber" label="选项数量">
          <a-input-number
            v-model="form.optionNumber"
            min="0"
            max="6"
            placeholder="请输入选项数量"
          />
        </a-form-item>
        <a-form-item>
          <a-button
            :loading="submitting"
            type="primary"
            html-type="submit"
            style="width: 120px"
          >
            一键生成
          </a-button>
        </a-form-item>
      </a-form>
    </div>
  </a-drawer>
</template>


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 visible = ref(false);
const submitting = ref(false);

const form = reactive({
  questionNumber: 10,
  optionNumber: 2,
} as API.UserLoginRequest);

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;
};

引入组件

设置题目页面引入抽屉组件:

<a-space size="medium">
  <a-button @click="addQuestion(questionContent.length)">
    底部添加题目
  </a-button>
  <!-- AI 生成题目抽屉 -->
  <AiGenerateQuestionDrawer
    :appId="appId"
    :onSuccess="onAiGenerateSuccess"
  />
</a-space>

编写 AI 生成题目成功的处理,插入题目:

const API.QuestionContentDTO[]) => {
  questionContent.value = [...questionContent.value, ...result];
  message.success(`AI 生成题目成功,已新增 ${result.length} 道题目`);
};

验证测试

1)AddAppPage 创建应用成功后无法正确跳转到详情页 Bug

因为 id 的默认值为空字符串导致,需将 "??" 改为 "||",代码如下:

if (res.data.code === 0) {
  message.success("操作成功,即将跳转到应用详情页");
  setTimeout(() => {
    router.push(`/app/detail/${props.id || res.data.data}`);
  }, 3000);
}

2)AI 生成中断,需要调整请求配置中的超时时间

const myAxios = axios.create({
  baseURL: "http://localhost:8101",
  timeout: 60000,
  withCredentials: true,
});

3)实际测试的生成结果有点太飘(随机)了,如图:

PZrB7Ny4cnWHbeVe-8075373c

还是将随机性调整为默认值:

// AI 生成
String result = aiManager.doSyncRequest(GENERATE_QUESTION_SYSTEM_MESSAGE, userMessage, null);

调整参数后,生成的结果正常多了:

pRNw2OMQ1eXwTK9L-9ac5431e

扩展思路

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,因为一次调用后sokcet通道已经被关闭,复用老的 request 会复用老链接会报错
    ChatCompletionRequest chatCompletionRequest = ChatGLMUtil.buildSyncUnStableRequest(appAiSysMessageConfig, String.format(appAiUserMessageConfig, Math.min(number, 10), 2));
    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、明确输入参数

明确需要提供给 AI 的参数列表:

  1. 应用信息,包括应用名称、描述
  2. 题目信息
  3. 用户答案

但是需要注意,题目数量可能会很多,如果将完整的题目结构(包括选项列表)输入给 AI,可能会超出最大 token 限制,所以可以进行优化。只存储用户答案对应的选项。

示例结构如下:

{
  "appName": "MBTI 性格测试",
  "appDesc": "测试你的 MBTI 性格",
  "question": [
    {
      "title": "你喜欢和人交流",
      "answer": "喜欢"
    }
  ]
}

2、明确返回结果

需要 JSON 格式的评分结果对象,比如:

{
  "resultName": "INTJ",
  "resultDesc": "INTJ被称为'策略家'或'建筑师',是一个高度独立和具有战略思考能力的性格类型"
}

3、编写和调试 Prompt

遵循 Prompt 工程指南中的技巧,编写出系统 Prompt 和用户 Prompt。

系统 Prompt 如下:

你是一位严谨的判题专家,我会给你如下信息:

应用名称, 【【【应用描述】】】, 题目和用户回答的列表:格式为 [{"title": "题目","answer": "用户回答"}]

请你根据上述信息,按照以下步骤来对用户进行评价:
1. 要求:需要给出一个明确的评价结果,包括评价名称(尽量简短)和评价描述(尽量详细,大于 200 字)
2. 严格按照下面的 json 格式输出评价名称和评价描述

{"resultName": "评价名称", "resultDesc": "评价描述"}

3. 返回格式必须为 JSON 对象

用户 Prompt 按照顺序提供信息即可,示例 Prompt 如下。

测评类应用:

MBTI 性格测试,
【【【快来测测你的 MBTI 性格】】】,
[{"title": "你通常更喜欢","answer": "独自工作"}, {"title": "当安排活动时","answer": "更愿意随机应变"}]

用到的技巧:

  1. 定义 System Prompt
  2. 让 GLM 进行角色扮演
  3. 使用分隔符标示不同的输入部分
  4. 少样本学习
  5. 指定输出长度的示例
  6. 将复杂任务分解为简单的子任务
  7. 指定固定的输出格式
  8. 通过按顺序输入参数来节约空间。
  9. 对于描述这种多行内容,可以通过特殊字符括起来,防止用户的输入干扰生成结果。

利用界面进行初步调试,可以尝试修改随机性:

GLGzfK8XpeX23rLn-3efec567

然后可以通过编写单元测试来调试。

得到结果后,仍然需要利用字符串查找,找到开头的 '{' 和结尾的 '}',截取到实际内容。

后端开发

1)编写题目答案封装类:

public class QuestionAnswerDTO {

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

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

2)编写 AI 测评类应用评分策略类,需要指定对应的注解:

@ScoringStrategyConfig(appType = 1, scoringStrategy = 1)

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

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;
}

前端开发

前端不需要开发新的页面,给做题页面的“查看结果”按钮补充一个提交等待的 loading 效果即可。

验证测试

效果如图:

LSMqWk4SXtISU5Qv-bbca0887

可以多次测试,调试 Prompt 和模型参数,以便获得更好的结果。

扩展思路

1)实现得分类应用的 AI 评分,可以给 AI 多传递每道题目正确的得分参数。

2)支持用户额外定义结果集,将 AI 生成的结果限定在一个范围内,更精确。

四、扩展知识

Spring AI

简化 AI 应用程序开发的工具库。

官方文档地址:https://docs.spring.io/spring-ai/reference/

主要功能如下:

q8HcuBssj7DSZT0E-ea09d6b1

简单来说就是适配了市面上主流的 AI 模型,提供统一接口便于我们的开发。

注意,整个 Spring AI 是基于 jdk 17 构建的。

X0zmLa9Ncv7lepH0-147c7756

默认支持国外主流的 AI 大模型,也支持自己定制。

智谱 AI + Spring AI 整合应用

Spring AI 官方也整合了智谱 AI:https://github.com/spring-projects/spring-ai/blob/main/spring-ai-spring-boot-starters/spring-ai-starter-zhipuai/pom.xml

现在有两个版本可以选择:https://central.sonatype.com/artifact/io.springboot.ai/spring-ai-zhipu-ai-spring-boot-starter/versions

QIe6jWNWMcSzqV2b-dfa810c0

引入依赖:

<dependency>
  <groupId>io.springboot.ai</groupId>
  <artifactId>spring-ai-zhipu-ai-spring-boot-starter</artifactId>
  <version>1.0.3</version>
</dependency>

使用起来仅需在配置文件中加下对应模型的配置:

spring:
  ai:
    zhipuai:
      api-key: xxxx
      chat:
        enabled: true
        options:
          model: glm-4

然后引入对应模型的 client,即可直接使用:

@Resource
private ZhipuAiChatClient chatClient;

//简单调用
chatClient.call("给我讲一个笑话");

//也可以配置一些参数
ChatResponse response = chatClient.call(
    new Prompt(
        "帮我随机生成五个名字",
        ZhipuAiChatOptions.builder()
        .withModel("glm-4")
        .withTemperature(0.8)
        .build()
    ));

相比起来其实代码并没有简化很多,只不过项目同时接入多个 AI 平台,调用方式上会得到统一。


项目分区导航:⬅️ 06-用户模块RBAC模型搭建 | 01-平台智能化 | ➡️ 02-性能优化