feat: 挑战的题目要求发给 AI 当对照清单,学生没提到的不替他做,在“想一想”里问
Deploy / deploy (build, debian, 22) (push) Canceled after 0s
Deploy / deploy (build, debian, 22) (push) Canceled after 0s
学生不用再整段贴题目;AI 只做学生说了的,从没提到的要求里挑一项问他打算怎么做。 整段贴了题目的照做。自由创作的题目是学生自己写的,不发。输入框提示改成“用你自己的话说说”。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
43c18cdb88
commit
5bdacc438d
6 files changed
+55
-9
No files matched your search
@@ -39,6 +39,15 @@ document.getElementById('btn').onclick = function() {
|
||||
</script>
|
||||
\`\`\``
|
||||
|
||||
// 挑战题目要求只当对照清单:学生没说的不替他做,挑一项放进“想一想”问他打算怎么做,
|
||||
// 要做成什么样还是学生自己说;整段贴了题目的照做(不拦,拦了只会去用外部 AI)
|
||||
export const BRIEF_RULES = `下面是这道题的题目要求。它是对照清单,不是让你照着做——这是课堂练习,题目要求要学生自己一步步提出来,这正是这节课要练的:
|
||||
- 只做学生在对话里说了的内容,做出来要能打开、能用。学生说的话里包含了题目要求(比如整段贴了题目),就照着做
|
||||
- 题目要求里学生还没提到的功能、规则和细节(如计时方式、对错的颜色、计分、重来按钮),哪怕很常见、顺手就能做,也先不做;也不要照搬题目里的类名、变量名和数据
|
||||
- 从学生没提到的要求里挑一项最重要的放进“想一想”,问他打算怎么做(如“题目还要求标出打错的字,你希望打错的时候是什么样子?”)
|
||||
- 学生要的和题目要求冲突时,按学生说的做,在“想一想”里提醒他题目是怎么要求的
|
||||
- 题目要求都做到了,“想一想”就问他自己的想法,不再提题目`
|
||||
|
||||
// 判断标准和分类器一致:看学生自己想了什么,照搬题目写得再长也不算清楚
|
||||
export const GUIDANCE_SYSTEM_PROMPT = `你是一个提示词写作教练。学生在网页制作课上让 AI 生成或修改网页,你帮他把这一次要对 AI 说的话想清楚、说具体。后面会给你题目要求、他之前对 AI 说过的话和 AI 上一次的说明。
|
||||
|
||||
@@ -208,9 +217,10 @@ export function userFacingError(e: unknown) {
|
||||
return status ? `AI 服务出错(${status}),请稍后重试` : "AI 服务出错,请稍后重试"
|
||||
}
|
||||
|
||||
// 生成整页代码:开着思考,质量优先
|
||||
export function streamChat(history: ChatMessage[], model?: string, signal?: AbortSignal) {
|
||||
return streamCompletion([{ role: "system", content: SYSTEM_PROMPT }, ...trimHistory(history)], {
|
||||
// 生成整页代码:开着思考,质量优先。brief 是挑战的题目要求(见 challengeBrief),自由创作没有
|
||||
export function streamChat(history: ChatMessage[], model?: string, signal?: AbortSignal, brief?: string | null) {
|
||||
const system = brief ? `${SYSTEM_PROMPT}\n\n${BRIEF_RULES}\n\n${brief}` : SYSTEM_PROMPT
|
||||
return streamCompletion([{ role: "system", content: system }, ...trimHistory(history)], {
|
||||
model: chatModel(model),
|
||||
thinking: true,
|
||||
signal,
|
||||
|
||||
@@ -102,15 +102,28 @@ export function codeAsMarkdown(code: {
|
||||
// 题目要求去掉图片、压成一段,太长的截掉
|
||||
const BRIEF_CHARS = 800
|
||||
|
||||
export function briefOf(content: string) {
|
||||
export function briefOf(content: string, max = BRIEF_CHARS) {
|
||||
const text = content
|
||||
.replace(/!\[[^\]]*\]\([^)]*\)/g, "")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim()
|
||||
return text.length > BRIEF_CHARS ? text.slice(0, BRIEF_CHARS) + "……" : text
|
||||
return clipTo(text, max)
|
||||
}
|
||||
|
||||
const clipTo = (text: string, n: number) => (text.length > n ? text.slice(0, n) + "……" : text)
|
||||
function clipTo(text: string, n: number) {
|
||||
return text.length > n ? text.slice(0, n) + "……" : text
|
||||
}
|
||||
|
||||
// 挑战的题目要求,给生成网页的 AI 当对照清单(见 llm.ts 的 BRIEF_RULES);要逐条对照,不能截得太短。
|
||||
// 自由创作的题目是学生自己写的介绍,不用对照,返回 null
|
||||
export function challengeBrief(taskId: number) {
|
||||
const task = get<{ title: string; content: string }>(
|
||||
`SELECT title, content FROM task_task WHERE id = $id AND task_type = 'challenge'`,
|
||||
{ id: taskId },
|
||||
)
|
||||
const brief = task && briefOf(task.content, 4000)
|
||||
return brief ? `题目:${task.title}\n\n题目要求:${brief}` : null
|
||||
}
|
||||
|
||||
// “帮我写清楚”的教练要知道题目和学生已经做到哪了,才分得清照搬题目和自己的想法:
|
||||
// 题目要求、学生最近几次对 AI 说的话、AI 上一次的说明(代码去掉)
|
||||
|
||||
+3
-1
@@ -3,6 +3,7 @@ import { all, get, run, iso, uuidOut } from "./db"
|
||||
import { userFromAccessToken, type User } from "./auth"
|
||||
import {
|
||||
canAccessTask,
|
||||
challengeBrief,
|
||||
codeAsMarkdown,
|
||||
getOrCreateActiveConversation,
|
||||
guidanceContext,
|
||||
@@ -132,7 +133,8 @@ async function handlePrompt(ws: WS, data: any) {
|
||||
}))
|
||||
let full: string
|
||||
try {
|
||||
full = await streamToClient(ws, streamChat(history, model, ws.data.abort.signal))
|
||||
const brief = challengeBrief(ws.data.taskId)
|
||||
full = await streamToClient(ws, streamChat(history, model, ws.data.abort.signal, brief))
|
||||
} catch (e) {
|
||||
// 客户端自己断开导致的中止不算出错
|
||||
if (!ws.data.closed) send(ws, { type: "error", content: userFacingError(e) })
|
||||
|
||||
@@ -192,6 +192,27 @@ describe("WebSocket 代码生成", () => {
|
||||
c.ws.close()
|
||||
})
|
||||
|
||||
test("挑战的题目要求作为对照清单发给 AI,自由创作不发", async () => {
|
||||
const { task, token } = await setup()
|
||||
run(`UPDATE task_task SET content = $c WHERE id = $id`, { c: "做一个计时器,有开始和暂停按钮", id: task })
|
||||
const c = (await connect(`/ws/prompt/${task}/?token=${token}`))!
|
||||
await c.next("init")
|
||||
c.ws.send(JSON.stringify({ type: "message", content: "做个页面" }))
|
||||
await c.next("complete")
|
||||
expect(lastRequest.messages[0].content).toContain("它是对照清单")
|
||||
expect(lastRequest.messages[0].content).toContain("题目要求:做一个计时器,有开始和暂停按钮")
|
||||
c.ws.close()
|
||||
|
||||
const creation = makeTask("我的作品", "creation", 2)
|
||||
run(`UPDATE task_task SET content = '我想做一个相册' WHERE id = $id`, { id: creation })
|
||||
const d = (await connect(`/ws/prompt/${creation}/?token=${token}`))!
|
||||
await d.next("init")
|
||||
d.ws.send(JSON.stringify({ type: "message", content: "做个页面" }))
|
||||
await d.next("complete")
|
||||
expect(lastRequest.messages[0].content).not.toContain("题目要求")
|
||||
d.ws.close()
|
||||
})
|
||||
|
||||
test("token 无效或任务不存在时握手失败", async () => {
|
||||
const { task, token } = await setup()
|
||||
expect(await connect(`/ws/prompt/${task}/?token=bad`)).toBeNull()
|
||||
|
||||
Reference in new issue
Block a user