diff --git a/client/src/components/review/ReviewPanel.vue b/client/src/components/review/ReviewPanel.vue index 6097660..cb30c6d 100644 --- a/client/src/components/review/ReviewPanel.vue +++ b/client/src/components/review/ReviewPanel.vue @@ -121,14 +121,14 @@
-

提示词 · {{ chain.length }} 轮

+

提示词 · {{ chainSteps }} 轮

逐轮看网页
这份提交没有关联的提示词
    -
  1. +
  2. {{ sourceLabel(r.source) }} {{ levelLabel(r.prompt_level) }} @@ -195,6 +195,8 @@ const versions = ref([]) const chain = ref([]) const rating = ref(false) const showChain = ref(false) +// 引导对话不生成网页,不算轮数 +const chainSteps = computed(() => chain.value.filter((r) => r.source !== "guidance").length) let awardsCache: Promise | null = null const awards = ref([]) @@ -490,6 +492,12 @@ h4 { color: var(--wp-muted); } +/* 不是 list-item,就不占 ol 的序号 */ +.chain .guide { + display: block; + color: var(--wp-muted); +} + .level { font: 600 11px var(--wp-font-mono); color: var(--wp-brand); diff --git a/client/src/components/submissions/ChainModal.vue b/client/src/components/submissions/ChainModal.vue index ec1b88f..91e0b74 100644 --- a/client/src/components/submissions/ChainModal.vue +++ b/client/src/components/submissions/ChainModal.vue @@ -9,7 +9,7 @@ @@ -41,16 +41,17 @@
  3. - {{ i + 1 }} + {{ nums[i] }} {{ sourceLabel(r.source) }} {{ levelLabel(r.prompt_level) }} 交的这一版 + 问写提示词的建议,不生成网页 没有网页
    - 第 {{ selected + 1 }} 轮生成的网页 + 第 {{ nums[selected] }} 轮生成的网页 ↑ ↓ 切换
    @@ -110,6 +111,14 @@ const rounds = ref([]) const selected = ref(0) const expanded = ref(new Set()) +// 引导对话(问怎么写提示词)夹在 AI 对话中间,不算轮数,也不能选中 +const isGuide = (r: PromptRound) => r.source === "guidance" +const steps = computed(() => rounds.value.filter((r) => !isGuide(r)).length) +const nums = computed(() => { + let n = 0 + return rounds.value.map((r) => (isGuide(r) ? "" : String(++n))) +}) + const doc = computed(() => { const r = rounds.value[selected.value] if (!r?.html) return "" @@ -165,7 +174,9 @@ function onKey(e: KeyboardEvent) { const step = e.key === "ArrowDown" ? 1 : e.key === "ArrowUp" ? -1 : 0 if (!step || !rounds.value.length) return e.preventDefault() - selected.value = Math.min(rounds.value.length - 1, Math.max(0, selected.value + step)) + let i = selected.value + step + while (rounds.value[i] && isGuide(rounds.value[i]!)) i += step + if (rounds.value[i]) selected.value = i nextTick(() => document.querySelector(".rounds .round.on")?.scrollIntoView({ block: "nearest" }), ) @@ -291,6 +302,25 @@ onUnmounted(() => window.removeEventListener("keydown", onKey, true)) background: var(--wp-html-soft); } +.src-guidance { + color: var(--wp-muted); + background: var(--wp-paper); +} + +.round.guide { + border-style: dashed; + background: transparent; + cursor: default; +} + +.round.guide:hover { + border-color: var(--wp-line); +} + +.round.guide .q { + color: var(--wp-muted); +} + .level { color: var(--wp-ink); background: var(--wp-paper); diff --git a/client/src/utils/helper.ts b/client/src/utils/helper.ts index c81e0a0..b0fd0d2 100644 --- a/client/src/utils/helper.ts +++ b/client/src/utils/helper.ts @@ -21,6 +21,7 @@ export function goHome(router: any, type: TASK_TYPE, display: number) { export function sourceLabel(source: string | null | undefined) { if (source === "edit") return "手改" if (source === "manual") return "外部 AI" + if (source === "guidance") return "引导" return "AI 对话" } diff --git a/server/src/services/classifier.ts b/server/src/services/classifier.ts index 8f866f2..a313b21 100644 --- a/server/src/services/classifier.ts +++ b/server/src/services/classifier.ts @@ -13,6 +13,8 @@ const CLASSIFY_SYSTEM_PROMPT = `你是一个教育评估专家。根据布鲁姆 只返回一个数字(1-6),不要解释。` +const MIN_PROMPT_CHARS = 4 + const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)) async function callLlm(content: string): Promise { @@ -23,7 +25,8 @@ async function callLlm(content: string): Promise { { role: "system", content: CLASSIFY_SYSTEM_PROMPT }, { role: "user", content }, ], - { maxTokens: 10, timeoutMs: 30_000 }, + // 同一句提示词要得到同一个层级 + { maxTokens: 10, temperature: 0, timeoutMs: 30_000 }, ) const match = /[1-6]/.exec(text) if (!match) { @@ -43,6 +46,11 @@ export async function classifyMessage(messageId: number): Promise { 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(msg.content) if (level !== null) { run(`UPDATE prompt_message SET prompt_level = $level WHERE id = $id`, { diff --git a/server/src/services/llm.ts b/server/src/services/llm.ts index b9980f1..053e449 100644 --- a/server/src/services/llm.ts +++ b/server/src/services/llm.ts @@ -63,6 +63,7 @@ interface CompletionOptions { model?: string stream: boolean maxTokens?: number + temperature?: number signal?: AbortSignal timeoutMs?: number } @@ -82,6 +83,7 @@ function buildRequest(messages: ChatMessage[], opts: CompletionOptions) { stream: opts.stream, } if (opts.maxTokens) body.max_tokens = opts.maxTokens + if (opts.temperature !== undefined) body.temperature = opts.temperature const apiKey = ark ? config.arkApiKey : config.llmApiKey if (!apiKey) throw new Error("未配置模型接口密钥") const baseUrl = (ark ? config.arkBaseUrl : config.llmBaseUrl).replace(/\/$/, "") @@ -203,12 +205,13 @@ export function streamGuidance(history: ChatMessage[], signal?: AbortSignal) { export async function complete( messages: ChatMessage[], - opts: { model?: string; maxTokens?: number; timeoutMs?: number } = {}, + opts: { model?: string; maxTokens?: number; temperature?: number; timeoutMs?: number } = {}, ): Promise { const res = await buildRequest(messages, { model: opts.model ?? config.llmModel, stream: false, maxTokens: opts.maxTokens, + temperature: opts.temperature, timeoutMs: opts.timeoutMs, }) await ensureOk(res) diff --git a/server/tests/api.test.ts b/server/tests/api.test.ts index eaf27ef..58ec7a3 100644 --- a/server/tests/api.test.ts +++ b/server/tests/api.test.ts @@ -646,3 +646,16 @@ describe("课堂总结", () => { expect(all.json.top_scored.map((t: any) => t.username)).toEqual(["weba2小李", "weba1小明"]) }) }) + +describe("提示词分类", () => { + test("太短的提问不打层级,以前打的也清掉", async () => { + const { classifyMessage } = await import("../src/services/classifier") + const { createConversation, insertMessage } = await import("../src/services/prompt") + const conv = createConversation(makeUser("stu"), makeTask("T")) + const id = insertMessage({ conversation_id: conv, role: "user", content: " 1 " }) + run(`UPDATE prompt_message SET prompt_level = 6 WHERE id = $id`, { id }) + expect(await classifyMessage(id)).toBeNull() + const row = get<{ prompt_level: number | null }>(`SELECT prompt_level FROM prompt_message WHERE id = $id`, { id }) + expect(row!.prompt_level).toBeNull() + }) +})