fix: 提示词链里的引导提问不算轮数,分类器固定结果、跳过过短输入
- 引导对话和 AI 对话存在同一个对话里,提示词链把它当成一轮“AI 对话”,显示“没有网页”。 现在标成“引导”,淡化显示,不计入轮数,也不能选中(↑↓ 跳过) - 分类器 temperature 设为 0:同一句提示词以前约两成会被打成不同层级 - 不到 4 个字的提问(“1”“,”等)不打层级,--force 重标时清掉旧标签 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
bc9da688ba
commit
30630e8125
6 files changed
+73
-10
No files matched your search
@@ -121,14 +121,14 @@
|
|||||||
|
|
||||||
<section>
|
<section>
|
||||||
<n-flex justify="space-between" align="center" class="chain-head">
|
<n-flex justify="space-between" align="center" class="chain-head">
|
||||||
<h4>提示词 <span class="muted" v-if="chain.length">· {{ chain.length }} 轮</span></h4>
|
<h4>提示词 <span class="muted" v-if="chainSteps">· {{ chainSteps }} 轮</span></h4>
|
||||||
<n-button v-if="chain.length" size="tiny" quaternary @click="showChain = true">
|
<n-button v-if="chain.length" size="tiny" quaternary @click="showChain = true">
|
||||||
逐轮看网页
|
逐轮看网页
|
||||||
</n-button>
|
</n-button>
|
||||||
</n-flex>
|
</n-flex>
|
||||||
<div v-if="!chain.length" class="muted">这份提交没有关联的提示词</div>
|
<div v-if="!chain.length" class="muted">这份提交没有关联的提示词</div>
|
||||||
<ol class="chain">
|
<ol class="chain">
|
||||||
<li v-for="(r, i) in chain" :key="i">
|
<li v-for="(r, i) in chain" :key="i" :class="{ guide: r.source === 'guidance' }">
|
||||||
<n-flex :size="6" align="center">
|
<n-flex :size="6" align="center">
|
||||||
<span class="src">{{ sourceLabel(r.source) }}</span>
|
<span class="src">{{ sourceLabel(r.source) }}</span>
|
||||||
<span v-if="r.prompt_level" class="level">{{ levelLabel(r.prompt_level) }}</span>
|
<span v-if="r.prompt_level" class="level">{{ levelLabel(r.prompt_level) }}</span>
|
||||||
@@ -195,6 +195,8 @@ const versions = ref<SubmissionOut[]>([])
|
|||||||
const chain = ref<PromptRound[]>([])
|
const chain = ref<PromptRound[]>([])
|
||||||
const rating = ref(false)
|
const rating = ref(false)
|
||||||
const showChain = ref(false)
|
const showChain = ref(false)
|
||||||
|
// 引导对话不生成网页,不算轮数
|
||||||
|
const chainSteps = computed(() => chain.value.filter((r) => r.source !== "guidance").length)
|
||||||
|
|
||||||
let awardsCache: Promise<AwardManageOut[]> | null = null
|
let awardsCache: Promise<AwardManageOut[]> | null = null
|
||||||
const awards = ref<AwardManageOut[]>([])
|
const awards = ref<AwardManageOut[]>([])
|
||||||
@@ -490,6 +492,12 @@ h4 {
|
|||||||
color: var(--wp-muted);
|
color: var(--wp-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 不是 list-item,就不占 ol 的序号 */
|
||||||
|
.chain .guide {
|
||||||
|
display: block;
|
||||||
|
color: var(--wp-muted);
|
||||||
|
}
|
||||||
|
|
||||||
.level {
|
.level {
|
||||||
font: 600 11px var(--wp-font-mono);
|
font: 600 11px var(--wp-font-mono);
|
||||||
color: var(--wp-brand);
|
color: var(--wp-brand);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<template #header>
|
<template #header>
|
||||||
<div class="head">
|
<div class="head">
|
||||||
<b>提示词</b>
|
<b>提示词</b>
|
||||||
<span v-if="rounds.length" class="muted">{{ rounds.length }} 轮</span>
|
<span v-if="steps" class="muted">{{ steps }} 轮</span>
|
||||||
<span v-if="title" class="muted title">{{ title }}</span>
|
<span v-if="title" class="muted title">{{ title }}</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -41,16 +41,17 @@
|
|||||||
<li
|
<li
|
||||||
v-for="(r, i) in rounds"
|
v-for="(r, i) in rounds"
|
||||||
:key="i"
|
:key="i"
|
||||||
:class="['round', { on: i === selected }]"
|
:class="['round', { on: i === selected, guide: isGuide(r) }]"
|
||||||
@click="selected = i"
|
@click="isGuide(r) || (selected = i)"
|
||||||
>
|
>
|
||||||
<div class="round-head">
|
<div class="round-head">
|
||||||
<span class="num">{{ i + 1 }}</span>
|
<span class="num">{{ nums[i] }}</span>
|
||||||
<span :class="['src', `src-${r.source || 'conversation'}`]">
|
<span :class="['src', `src-${r.source || 'conversation'}`]">
|
||||||
{{ sourceLabel(r.source) }}
|
{{ sourceLabel(r.source) }}
|
||||||
</span>
|
</span>
|
||||||
<span v-if="r.prompt_level" class="level">{{ levelLabel(r.prompt_level) }}</span>
|
<span v-if="r.prompt_level" class="level">{{ levelLabel(r.prompt_level) }}</span>
|
||||||
<span v-if="i === rounds.length - 1" class="final">交的这一版</span>
|
<span v-if="i === rounds.length - 1" class="final">交的这一版</span>
|
||||||
|
<span v-else-if="isGuide(r)" class="muted no-code">问写提示词的建议,不生成网页</span>
|
||||||
<span v-else-if="!r.html" class="muted no-code">没有网页</span>
|
<span v-else-if="!r.html" class="muted no-code">没有网页</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -65,7 +66,7 @@
|
|||||||
|
|
||||||
<div class="stage">
|
<div class="stage">
|
||||||
<div class="stage-head">
|
<div class="stage-head">
|
||||||
<b>第 {{ selected + 1 }} 轮生成的网页</b>
|
<b>第 {{ nums[selected] }} 轮生成的网页</b>
|
||||||
<span class="muted">↑ ↓ 切换</span>
|
<span class="muted">↑ ↓ 切换</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="frame">
|
<div class="frame">
|
||||||
@@ -110,6 +111,14 @@ const rounds = ref<PromptRound[]>([])
|
|||||||
const selected = ref(0)
|
const selected = ref(0)
|
||||||
const expanded = ref(new Set<number>())
|
const expanded = ref(new Set<number>())
|
||||||
|
|
||||||
|
// 引导对话(问怎么写提示词)夹在 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 doc = computed(() => {
|
||||||
const r = rounds.value[selected.value]
|
const r = rounds.value[selected.value]
|
||||||
if (!r?.html) return ""
|
if (!r?.html) return ""
|
||||||
@@ -165,7 +174,9 @@ function onKey(e: KeyboardEvent) {
|
|||||||
const step = e.key === "ArrowDown" ? 1 : e.key === "ArrowUp" ? -1 : 0
|
const step = e.key === "ArrowDown" ? 1 : e.key === "ArrowUp" ? -1 : 0
|
||||||
if (!step || !rounds.value.length) return
|
if (!step || !rounds.value.length) return
|
||||||
e.preventDefault()
|
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(() =>
|
nextTick(() =>
|
||||||
document.querySelector(".rounds .round.on")?.scrollIntoView({ block: "nearest" }),
|
document.querySelector(".rounds .round.on")?.scrollIntoView({ block: "nearest" }),
|
||||||
)
|
)
|
||||||
@@ -291,6 +302,25 @@ onUnmounted(() => window.removeEventListener("keydown", onKey, true))
|
|||||||
background: var(--wp-html-soft);
|
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 {
|
.level {
|
||||||
color: var(--wp-ink);
|
color: var(--wp-ink);
|
||||||
background: var(--wp-paper);
|
background: var(--wp-paper);
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export function goHome(router: any, type: TASK_TYPE, display: number) {
|
|||||||
export function sourceLabel(source: string | null | undefined) {
|
export function sourceLabel(source: string | null | undefined) {
|
||||||
if (source === "edit") return "手改"
|
if (source === "edit") return "手改"
|
||||||
if (source === "manual") return "外部 AI"
|
if (source === "manual") return "外部 AI"
|
||||||
|
if (source === "guidance") return "引导"
|
||||||
return "AI 对话"
|
return "AI 对话"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ const CLASSIFY_SYSTEM_PROMPT = `你是一个教育评估专家。根据布鲁姆
|
|||||||
|
|
||||||
只返回一个数字(1-6),不要解释。`
|
只返回一个数字(1-6),不要解释。`
|
||||||
|
|
||||||
|
const MIN_PROMPT_CHARS = 4
|
||||||
|
|
||||||
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
|
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
|
||||||
|
|
||||||
async function callLlm(content: string): Promise<number | null> {
|
async function callLlm(content: string): Promise<number | null> {
|
||||||
@@ -23,7 +25,8 @@ async function callLlm(content: string): Promise<number | null> {
|
|||||||
{ role: "system", content: CLASSIFY_SYSTEM_PROMPT },
|
{ role: "system", content: CLASSIFY_SYSTEM_PROMPT },
|
||||||
{ role: "user", content },
|
{ role: "user", content },
|
||||||
],
|
],
|
||||||
{ maxTokens: 10, timeoutMs: 30_000 },
|
// 同一句提示词要得到同一个层级
|
||||||
|
{ maxTokens: 10, temperature: 0, timeoutMs: 30_000 },
|
||||||
)
|
)
|
||||||
const match = /[1-6]/.exec(text)
|
const match = /[1-6]/.exec(text)
|
||||||
if (!match) {
|
if (!match) {
|
||||||
@@ -43,6 +46,11 @@ export async function classifyMessage(messageId: number): Promise<number | null>
|
|||||||
{ id: messageId },
|
{ id: messageId },
|
||||||
)
|
)
|
||||||
if (!msg) return null
|
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)
|
const level = await callLlm(msg.content)
|
||||||
if (level !== null) {
|
if (level !== null) {
|
||||||
run(`UPDATE prompt_message SET prompt_level = $level WHERE id = $id`, {
|
run(`UPDATE prompt_message SET prompt_level = $level WHERE id = $id`, {
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ interface CompletionOptions {
|
|||||||
model?: string
|
model?: string
|
||||||
stream: boolean
|
stream: boolean
|
||||||
maxTokens?: number
|
maxTokens?: number
|
||||||
|
temperature?: number
|
||||||
signal?: AbortSignal
|
signal?: AbortSignal
|
||||||
timeoutMs?: number
|
timeoutMs?: number
|
||||||
}
|
}
|
||||||
@@ -82,6 +83,7 @@ function buildRequest(messages: ChatMessage[], opts: CompletionOptions) {
|
|||||||
stream: opts.stream,
|
stream: opts.stream,
|
||||||
}
|
}
|
||||||
if (opts.maxTokens) body.max_tokens = opts.maxTokens
|
if (opts.maxTokens) body.max_tokens = opts.maxTokens
|
||||||
|
if (opts.temperature !== undefined) body.temperature = opts.temperature
|
||||||
const apiKey = ark ? config.arkApiKey : config.llmApiKey
|
const apiKey = ark ? config.arkApiKey : config.llmApiKey
|
||||||
if (!apiKey) throw new Error("未配置模型接口密钥")
|
if (!apiKey) throw new Error("未配置模型接口密钥")
|
||||||
const baseUrl = (ark ? config.arkBaseUrl : config.llmBaseUrl).replace(/\/$/, "")
|
const baseUrl = (ark ? config.arkBaseUrl : config.llmBaseUrl).replace(/\/$/, "")
|
||||||
@@ -203,12 +205,13 @@ export function streamGuidance(history: ChatMessage[], signal?: AbortSignal) {
|
|||||||
|
|
||||||
export async function complete(
|
export async function complete(
|
||||||
messages: ChatMessage[],
|
messages: ChatMessage[],
|
||||||
opts: { model?: string; maxTokens?: number; timeoutMs?: number } = {},
|
opts: { model?: string; maxTokens?: number; temperature?: number; timeoutMs?: number } = {},
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const res = await buildRequest(messages, {
|
const res = await buildRequest(messages, {
|
||||||
model: opts.model ?? config.llmModel,
|
model: opts.model ?? config.llmModel,
|
||||||
stream: false,
|
stream: false,
|
||||||
maxTokens: opts.maxTokens,
|
maxTokens: opts.maxTokens,
|
||||||
|
temperature: opts.temperature,
|
||||||
timeoutMs: opts.timeoutMs,
|
timeoutMs: opts.timeoutMs,
|
||||||
})
|
})
|
||||||
await ensureOk(res)
|
await ensureOk(res)
|
||||||
|
|||||||
@@ -646,3 +646,16 @@ describe("课堂总结", () => {
|
|||||||
expect(all.json.top_scored.map((t: any) => t.username)).toEqual(["weba2小李", "weba1小明"])
|
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()
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in new issue
Block a user