笔者目前正在挑战基于国内的大模型创建 10个AI应用,相关的连载教程 + Demo体验地址会在Github仓库持续更新。
这是基于国内大模型创建 AI 应用的第 2 期。AI 应用的开发过程会同步到 B 站: @卡皮 AI,欢迎大家关注。
前言
本期开发的应用是一个用智谱清言的 API 接口,开发的一个让 AI 来完成今年的高考作文的写作,并实现 AI 批改评分的一个 AI 应用。
整个应用的 UI 部分,有 3 个部分:
- 头部:试卷的切换功能,可以切换选择不同省份的试卷详情。主体部分采用左右分布的一个布局。
- 左侧部分:对应各个省份的作文题目,以及由 AI 根据该作为题目写作的作文内容。
- 右侧部分:展示了 AI 基于作文题目和作文内容,给出的具体评分,包括分数、作文等级和打分的依据。
大模型 Prompt 开发
首先我们直接进入到大模型的后端开发,这个 AI 应用的核心的接口就 2 个:让 AI 写作、让 AI 判分。
智谱清言的 API 调用是兼容了 OpenAI 的 npm 库,所以在开发后端应用之前,记得提前在智谱清言的后台获取到你的 API_KEY 和对应的 BASE_URL,然后实例化 OpenAI。
import OpenAI from "openai";
const openai = new OpenAI({
apiKey: process.env["GLM_API_KEY"],
baseURL: process.env["GLM_BASE_URL"],
});
AI 写作
首先我们进入到 AI 写作的开发,用户点击【AI 写作】按钮后,会调用/api/gaokao/write接口,接口接收两个参数:
city: 城市名称question:作文题目信息
接口的 Controller 代码如下:
@Post('/write')
async getWrite(@Body() body) {
const { city, question } = body;
const data = await this.gaokaoService.getAIEssay({ city, question });
return {
success: !!data,
data,
info: !!data ? null : '生成失败',
};
}
我们的核心调用智谱清言的核心 Prompt 的逻辑,封装在对应的 Service 中。
async getAIEssay({ question, city }) {
// 构建 Prompt
const prompt = `你现在是一名正在参加${city}高考的考生,你正在写作语文高考作文。要求:
1. 文章要符合题目要求,要有明确的中心思想,要有引子、承接、转折、结尾等要素。
2. 文章需要满足高考作文的字数要求,字数不够会扣分。
3. 文章需要有较好的语言表达,不要出现语法错误。
4. 文章需要有较好的文采,不要出现重复、啰嗦等问题。
5. 文章需要有较好的逻辑性,不要出现逻辑混乱等问题。
6. 文章需要有较好的内容,不要出现内容空洞等问题。
7. 文章需要有较好的结构,不要出现结构混乱等问题。
8. 文章需要有较好的主题,不要偏离主题等问题。
9. 文章需要有较好的观点,不要出现观点不明确等问题。
10. 文章需要有一定的思想、并且有感情真实
10. 请直接开始从题目开始写,题目使用<title><title/>进行包裹,无需其他的回答
请根据以下题目撰写一篇高考作文:
题目:${question}`;
try {
const chatCompletion: OpenAI.Chat.ChatCompletion =
await openai.chat.completions.create({
model: 'glm-4',
messages: [
{ role: 'system', content: '你是一个高考考生。' },
{ role: 'user', content: prompt },
],
temperature: 0.7,
});
// 返回结果
return chatCompletion?.choices[0]?.message?.content || '';
} catch (error) {
return error.message;
}
}
为了让生成的文章的内容更加出彩,我们这边调用的是智谱清言 GLM-4 的模型。在智谱清言官网中,GLM-4 的介绍如下:
新一代基座大模型 GLM-4,整体性能相比 GLM3 全面提升 60%,逼近 GPT-4;支持更长上下文;更强的多模态;支持更快推理速度,更多并发,大大降低推理成本;同时 GLM-4 增强了智能体能力。
在 Prompt 中,我们在system中让它假设自己是一个高考生,然后在user中,让它告知了大模型的高考作文题目。以及写文章需要注意的要点和注意事项。
在这个 Prompt 中需要注意的是,在第 10 条中,我们让大模型直接给出回答,无需其他的描述,这样确保了大模型给出的回答只有标题和作文的内容。我们还让大模型将作文的标题使用<title><title/>进行报告包裹,方便之后对标题如果有特殊样式的前端处理,方便实现正则的一个替换工作。
以新课标 I 卷为测试,如果调用成功,大模型返回的数据结果如下:
<title>智慧的探求:问题与答案的辩证法</title>
在这个互联网和人工智能迅猛发展的时代,知识的获取变得前所未有地便捷。一个问题只需轻轻敲击键盘,就能在瞬间得到答案。然而,这并不意味着我们的问题会越来越少,相反,我认为,问题和答案之间存在着一种辩证的关系,它们在探求智慧的道路上相辅相成。
科技的进步确实在很大程度上解决了我们的疑问,但同时也引发了更多的问题。互联网像一个无尽的宝库,让我们在探索中不断发现新的知识领域。当我们对一个答案感到满意时,新的问题往往接踵而至。这种循环往复的过程促使我们不断进步,不断挑战自己的认知边界。
在我看来,问题并非负担,而是推动我们前进的动力。每一个问题都代表着一个新的思考方向,引领我们走向更深层次的认知。正如古人所说:“学起于思,思源于疑。”疑问是我们探求真理的起点,也是我们成长过程中不可或缺的元素。
然而,我们不能仅仅满足于获取答案,更要学会独立思考。在这个信息爆炸的时代,过多的答案往往会让人陷入迷茫。我们需要具备辨别真伪、筛选信息的能力,从而形成自己的观点。这就要求我们在探求答案的过程中,不仅要关注结果,更要关注方法,培养自己的思维能力。
在这个背景下,我认为我们的问题不会越来越少,而是会越来越多。因为随着认知水平的提高,我们会站在更高的角度去审视这个世界,发现更多未知的问题。同时,人工智能和互联网的发展也在不断拓宽我们的视野,让我们有机会接触到更多领域的知识,从而产生更多的问题。
那么,如何在这个充满问题的世界找到自己的方向呢?我认为,首先要保持好奇心。好奇心是我们探索未知的驱动力,它能激发我们的求知欲,让我们在面对问题时保持积极的态度。其次,要勇于质疑。不盲从权威,不满足于表面的答案,敢于挖掘问题的本质。最后,要善于交流与合作。在探求答案的过程中,与他人分享观点,取长补短,共同成长。
总之,在这个互联网和人工智能的时代,我们的问题不会越来越少,而是会越来越多。这是因为问题与答案之间存在一种辩证的关系,它们相互促进,共同推动我们探求智慧。让我们在问题的引领下,不断拓展认知边界,追求真理,成为更好的自己。
AI 判分
在用户点击 AI 判分按钮时,会调用/api/gaokao/write接口,该接口接收两个参数:
question:作文的题目信息,用于大模型作文判分的依据content:作文的内容
接口的 Controller 代码如下:
@Post('/score')
async getScore(@Body() body) {
const { content, question } = body;
const data = await this.gaokaoService.getScore({ content, question });
return {
success: !!data,
data,
info: !!data ? null : '生成失败',
};
}
判分逻辑对应的 Service 逻辑如下:
// 获取判分
async getScore({ question, content }) {
try {
const chatCompletion: OpenAI.Chat.ChatCompletion =
await openai.chat.completions.create({
model: 'glm-4',
messages: [
{
role: 'system',
content: `你是一名高考的语文阅卷组老师,正在进行高考作文的批改,总分60分。你需要按照高考的作文评分标准进行给分,评分标准如下:
\`\`\`json
${JSON.stringify(GAOKAO_RULE)}
\`\`\`。
评分标准中有不同的等级,你需要判定内容、表达、特征等方面的对应等级,并在对应等级内的最高分和最低分之间评分。评分时需考虑以下几点:
1. 你需要先看内容的对应等级,再看表达和特征的对应等级,后面两个的等级不能高于内容的等级。
2. 等级对应type字段,1代表最高档,4代表最低档。
3. 每个等级的分数需要参考minScore和maxScore两个字段,你需要在这两个字段之间给出分数,对应的等级不允许高于其maxScore,也不允许低于minScore。
4. 如果作文字数不够、偏离题意、白卷或者有严重的思想问题,你需要给出最低档的分数甚至0分。
5. 你务必返回类似下面的格式的JSON数据:\`\`\`json
${JSON.stringify(GAOKAO_SOCRE_RESPONSE_EXAMPLE)}
\`\`\`,包含score/type/rules三个字段,否则学生的考试成绩可能会受到影响,后果非常严重!
`,
},
{
role: 'user',
content: `你是一名高考阅卷组老师,正在进行高考作文的批改,
作文的题目的题干:{${question}}。学生对应该题干的作文内容是:{${content}}。
请给出你评分结果的JSON数据。如果内容为空、字数不够或偏离题意,请按照最低档评分。
请确保每个项目(内容、表达、特征)的分数在对应的等级的minScore和maxScore之间,1等不超过20分,2等不超过15分,3等不超过10分,4等不超过5分,并且总分不超过60分。
`,
},
],
temperature: 0.2,
});
// 返回解析后的结果
const data = chatCompletion?.choices[0]?.message?.content;
const response = parseAIJSON(data) || null;
return response;
} catch (error) {
return error.message;
}
}
前端开发
前端开发部分比较简单,我们采用的是Nuxt3 + TailwindCSS + Vant的技术栈,并且基于TailwindCSS,前端也能视线很好的响应式。
数据存储部分采用的是Pinia,方便作文数据和批改数据的存储和管理。
前端代码
前端主页的代码如下:
<template>
<div class="flex flex-col">
<header class="px-6 py-3 flex justify-between items-center border-b">
<div class="flex flex-col lg:flex-row lg:items-center">
<span class="font-bold text-base md:text-xl">2024高考+AI</span>
<span
class="lg:ml-2 text-sm cursor-pointer underline hover:text-indigo-600 text-indigo-700"
@click="showRules = true"
>高考作文评分细则</span
>
</div>
<div class="flex items-center">
<span class="mr-4">{{ city }}</span>
<van-button
size="small"
@click="
() => {
showCityPicker = true;
}
"
>切换城市</van-button
>
<HelpButton class="relative !right-0 !top-0 ml-2 lg:ml-8" />
</div>
</header>
<div class="flex flex-col p-5 lg:flex-row lg:px-8 pb-12">
<!-- 作文题 + 作文 -->
<div class="lg:w-1/2 lg:flex-none">
<!-- 题目 -->
<QuestionDesc />
<!-- 写作 -->
<div class="flex justify-end mt-4">
<van-button
type="primary"
class="!bg-indigo-500 !border-none"
@click="onAIWrite"
>
<span class="flex items-center"
><SparklesIcon class="w-5 mr-1" />AI写作
</span>
</van-button>
</div>
<!-- 写作的内容 -->
<div>
<EssayContent
:data="content"
@change="
(value) => {
content = value;
}
"
/>
</div>
<!-- AI批改 -->
<div class="flex justify-end mt-4">
<van-button
type="primary"
class="!bg-indigo-500 !border-none"
:disabled="!content"
@click="onAICheck"
>
<span class="flex items-center"
><SparklesIcon class="w-5 mr-1" />AI判分
</span>
</van-button>
</div>
</div>
<!-- 批改结果 -->
<CheckResult class="lg:w-1/2 lg:mt-0 lg:ml-8" />
</div>
<!-- 选择器 -->
<van-popup v-model:show="showCityPicker" position="top">
<van-picker title="省份" :columns="cityOptions" @confirm="onConfirm"
/></van-popup>
<!-- loading -->
<van-overlay :show="loading">
<CubeLoading class="mt-[20vh]" textCSS="text-white" :text="loadingText" />
</van-overlay>
<van-overlay :show="showRules" @click="showRules = false">
<div class="pt-[15vh]">
<div class="px-8 max-w-5xl m-auto" @click.stop>
<img
src="https://static-main.aiyeshi.cn/images/gaokao-202406/gaokao-zuowen-rule.jpeg"
/>
</div>
</div>
</van-overlay>
</div>
</template>
<script setup>
import { ref, toRefs, computed } from "vue";
import useProjectGaokaoStore from "@/store/gaokao";
import { SparklesIcon } from "@heroicons/vue/24/outline";
// gaokaoStore
const gaokaoStore = useProjectGaokaoStore();
const { content, cityList, city, loading, loadingText } = toRefs(gaokaoStore);
const showCityPicker = ref(false);
const showRules = ref(false);
const cityOptions = computed(() => {
return cityList.value.map((i) => {
return {
text: i.city,
value: i.city,
};
});
});
function onAIWrite() {
gaokaoStore.onWrite();
}
function onAICheck() {
gaokaoStore.onGetScore();
}
function onConfirm(city) {
showCityPicker.value = false;
gaokaoStore.onCityChange(city.selectedValues[0]);
}
</script>
Pinia逻辑
核心的逻辑封装到了对应的 Store 中,@/store/gaokao的代码如下:
import { ref, onMounted, computed } from "vue";
import { showFailToast } from "vant";
import useAxios from "@/composables/useAxios";
import cityData from "@/components/data";
/**
* AI舌苔项目的store
*/
const useProjectGaokaoStore = defineStore("project-gaokao", () => {
const axios = useAxios();
const loading = ref(false);
const loadingText = ref("正在创作中...");
// 当前的城市
const city = ref("新课标I卷");
// 城市列表
const cityList = ref(cityData);
// 当前的内容
const content = ref("");
// 当前的题干
const question = computed(() => {
return cityList.value.find((item) => item.city === city.value).question;
});
// 开始进行AI写作
async function onWrite() {
loading.value = true;
loadingText.value = "正在写作中...";
result.value = null;
try {
const res = await axios.post("/api/gaokao/write", {
city: city.value,
question: question.value,
});
if (res.success) {
content.value = res.data;
} else {
showFailToast(res.info);
}
loading.value = false;
} catch (error) {
loading.value = false;
}
}
/**
interface IResult {
score: number;
type: number;
rules: {
name: string,
score: number,
type: number,
desc: string,
}[];
}
*/
const result = ref(null);
// 开始进行AI的批改
async function onGetScore() {
loading.value = true;
loadingText.value = "正在批改中...";
try {
const res = await axios.post("/api/gaokao/score", {
question: question.value,
content: content.value,
});
if (res.success) {
result.value = res.data;
} else {
showFailToast(res.info);
}
loading.value = false;
} catch (error) {
loading.value = false;
}
}
function onCityChange(value) {
city.value = value;
cityData.forEach((item) => {
if (item.city === value) {
content.value = item.content;
result.value = item.result;
}
});
}
onMounted(() => {
// 初始化数据为北京
onCityChange("新课标I卷");
});
return {
loading,
loadingText,
city,
question,
cityList,
content,
result,
onCityChange,
onWrite,
onGetScore,
};
});
export default useProjectGaokaoStore;
最后
整个项目是一个比较简单的AI应用,没有涉及到太多AI应用的复杂逻辑,但是对于高考作文的写作和批改,是一个比较有意义(趁热点)的一个应用。
该项目的源码可以在B站工房中获取,获取的源码可以进行商业化的二次开发,增加更加高效的Prompt和大模型的微调逻辑。