Deploy / deploy (build, debian, 22) (push) Canceled after 0s
- 生成回复分“做了什么 / 我替你定的 / 想一想”三项;只问问题时直接回答、不给代码 - 没有代码的回复发 code: null,不再清空网页;上下文保留最近一份带代码的回复 - 教练带上题目要求、最近的提问和 AI 上一次的说明,照搬题目不算写清楚 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
127 lines
6.8 KiB
TypeScript
127 lines
6.8 KiB
TypeScript
import { all, get, run } from "../db"
|
||
import { complete, llmConfigured } from "./llm"
|
||
import { briefOf } from "./prompt"
|
||
|
||
// 学生在这里是“指挥 AI 做网页”,看的是这句提问里学生自己想了多少,不是要 AI 做的东西有多大:
|
||
// 把题目要求原样贴过去,功能列得再多也只是照着做(L3)
|
||
const CLASSIFY_SYSTEM_PROMPT = `你是一个教育评估专家。学生在网页制作课上让 AI 生成或修改网页,下面给出题目要求、学生上一轮的提问和这一轮的提问。请按布鲁姆认知分类学,判断“这一轮提问”体现的学生自己的思考层级。
|
||
|
||
按下面的顺序判断,命中哪一条就是哪一级:
|
||
1. L1 记忆:没有任何具体要求、解释或问题。只给作品名称、只报结果或只让 AI 修(例:"帮我做一个象棋游戏"、"制作一个吃豆人"、"继续"、"预览代码"、"还是没有显示"、"点击开始游戏进不去"、"帮我修复代码bug")
|
||
2. L2 理解:想弄懂原理、代码或概念(问为什么、是什么意思、怎么做到的),或者用自己的话解释现有代码在做什么(例:"为什么点了按钮没反应?"、"grid 和 flex 有什么区别"、"这段 js 里的 setInterval 是干什么的")
|
||
3. L4 分析:说清在哪个操作、哪个位置出了什么具体问题,或者自己推测原因、指出问题出在哪部分(例:"拖动绘制时松开鼠标还在画,是不是没监听 mouseup"、"缩小窗口后网格就不显示了"、"围棋我刚下一颗黑棋就判我赢了")
|
||
4. L5 评价:明确给出判断理由来评价现有结果,或者比较两种做法再选择;必须有“因为……所以……”“A 比 B 好”这样的理由或比较(例:"现在按钮太多,小学生看不懂,改成只留三个主要按钮"、"用 canvas 画比用一堆 div 流畅,改成 canvas")
|
||
5. L6 创造:在题目要求之外提出自己构思的设计,而且几部分互相配合成一个整体,如原创的主题、玩法、规则或交互(例:"改成星空主题:转盘是星座,抽中稀有奖励时流星划过并解锁对应星座的图鉴")
|
||
6. L3 应用:其余给出具体、可执行要求的,如颜色、风格、布局、要加或要改的功能(例:"方块大一点,旁边加一个橡皮擦和清空按钮"、"页面风格可爱,浅粉加浅蓝的渐变"、"加上撤销和保存为图片")
|
||
|
||
注意:
|
||
- 只看学生自己想出来的部分。照搬或改写题目要求里的功能清单是 L3,要求写得长、功能多不等于层级高。
|
||
- 只说想要什么风格、加什么功能,没有理由,是 L3 不是 L5;只列很多功能,没有自己的整体构思,是 L3 不是 L6。
|
||
- 只说“没反应”“进不去”“不能玩”“有 bug”,哪怕提到了是哪个按钮,也是 L1;要说出更具体的现象(如“打字速度始终是 0”)或推测原因才是 L4。
|
||
- 说明要改成什么样(如“弧线应该对着中圈”“球门要比禁区小”)是 L3,不是 L4。
|
||
- 整段贴了题目要求或一长串功能清单,不是 L1,是 L3。
|
||
- 结合上一轮看:重复上一轮的提问,层级不高于上一轮。
|
||
|
||
只返回一个数字(1-6),不要解释。`
|
||
|
||
const MIN_PROMPT_CHARS = 4
|
||
const PROMPT_CHARS = 1500
|
||
|
||
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
|
||
|
||
async function callLlm(content: string): Promise<number | null> {
|
||
if (!llmConfigured()) return null
|
||
try {
|
||
const text = await complete(
|
||
[
|
||
{ role: "system", content: CLASSIFY_SYSTEM_PROMPT },
|
||
{ role: "user", content },
|
||
],
|
||
// 只回一个数字,不用思考;关了思考 temperature 才生效,同一句提示词得到同一个层级
|
||
{ thinking: false, maxTokens: 10, temperature: 0, timeoutMs: 30_000 },
|
||
)
|
||
const match = /[1-6]/.exec(text)
|
||
if (!match) {
|
||
console.warn(`classify: unexpected LLM response '${text}'`)
|
||
return null
|
||
}
|
||
return Number(match[0])
|
||
} catch (e) {
|
||
console.error("classify LLM call failed:", e)
|
||
return null
|
||
}
|
||
}
|
||
|
||
function clip(text: string) {
|
||
return text.length > PROMPT_CHARS ? text.slice(0, PROMPT_CHARS) + "……" : text
|
||
}
|
||
|
||
// 发给模型的:题目要求、同一对话里上一轮的提问(引导对话只和引导对话比)、这一轮的提问
|
||
function classifyInput(msg: { id: number; content: string; source: string; conversation_id: string; created: string }) {
|
||
const task = get<{ title: string; content: string }>(
|
||
`SELECT t.title, t.content FROM prompt_conversation c JOIN task_task t ON t.id = c.task_id WHERE c.id = $conv`,
|
||
{ conv: msg.conversation_id },
|
||
)
|
||
const prev = get<{ content: string }>(
|
||
`SELECT content FROM prompt_message
|
||
WHERE conversation_id = $conv AND role = 'user' AND source != 'edit'
|
||
AND (source = 'guidance') = $guidance
|
||
AND (created < $created OR (created = $created AND id < $id))
|
||
ORDER BY created DESC, id DESC LIMIT 1`,
|
||
{ conv: msg.conversation_id, guidance: msg.source === "guidance", created: msg.created, id: msg.id },
|
||
)
|
||
return [
|
||
`题目:${task?.title ?? "(未知)"}`,
|
||
`题目要求:${task ? briefOf(task.content) : "(无)"}`,
|
||
`上一轮提问:${prev ? clip(prev.content) : "(这是第一轮)"}`,
|
||
`这一轮提问:${clip(msg.content)}`,
|
||
].join("\n\n")
|
||
}
|
||
|
||
export async function classifyMessage(messageId: number): Promise<number | null> {
|
||
const msg = get<{ id: number; content: string; source: string; conversation_id: string; created: string }>(
|
||
`SELECT id, content, source, conversation_id, created FROM prompt_message
|
||
WHERE id = $id AND role = 'user' AND source != 'edit'`,
|
||
{ id: messageId },
|
||
)
|
||
if (!msg) return null
|
||
// “1”“,”“可以”这类太短,判断不出层级;清掉以前打的标签(--force 重标时)
|
||
if ([...msg.content.trim()].length < MIN_PROMPT_CHARS) {
|
||
run(`UPDATE prompt_message SET prompt_level = NULL WHERE id = $id`, { id: messageId })
|
||
return null
|
||
}
|
||
const level = await callLlm(classifyInput(msg))
|
||
if (level !== null) {
|
||
run(`UPDATE prompt_message SET prompt_level = $level WHERE id = $id`, {
|
||
level,
|
||
id: messageId,
|
||
})
|
||
}
|
||
return level
|
||
}
|
||
|
||
export async function classifyConversationMessages(conversationId: string, force = false) {
|
||
const rows = all<{ id: number }>(
|
||
`SELECT id FROM prompt_message
|
||
WHERE conversation_id = $id AND role = 'user' AND source != 'edit' ${force ? "" : "AND prompt_level IS NULL"}
|
||
ORDER BY created, id`,
|
||
{ id: conversationId },
|
||
)
|
||
for (const { id } of rows) {
|
||
await classifyMessage(id)
|
||
await sleep(300)
|
||
}
|
||
}
|
||
|
||
export async function classifyMessagesBatch(ids: number[]) {
|
||
for (const id of ids) {
|
||
await classifyMessage(id)
|
||
await sleep(500)
|
||
}
|
||
}
|
||
|
||
// 后台执行,不阻塞请求
|
||
export function inBackground(p: Promise<unknown>) {
|
||
p.catch((e) => console.error("background task failed:", e))
|
||
}
|