feat(AI 提示): 两段式提示——先诊断出错误标签,再生成提示
Some checks failed
Deploy / deploy (push) Has been cancelled

AI 时代 OJ 设计的 2b。标准答案能让提示准得多,但不能进生成提示的 prompt
(学生代码里一段注释就能把它套走),所以拆成两段:
- 诊断:看得到标准答案、第一个没过的测试点、带行号的学生代码;出参只允许
  { tag, lines, confidence },safeParse 过闸、多余字段剥掉,没有自由文本通道
- 生成提示:看不到标准答案和测试点原文,只多一句「问题定位:X,大约在第 a 行」
- 诊断失败(20 秒超时、不是 JSON、校验不过)退回单段式;同一条提交复用诊断;
  编译失败不诊断
- 契约新增 HINT_ERROR_TAGS(13 个,落库值,只增不改)与 hintDiagnosisSchema
- 迁移 0018:ai_hint 加 diagnosis / diagnosis_error 两列
- 单段式 prompt 原样搬进 services/hint-diagnosis.ts,记版本 1;两段式记版本 2
- completeChat 支持 JSON 模式和自定义超时,现有调用不受影响
- 开关 AI_HINT_DIAGNOSE 默认关(2a 的基线还在攒),两套生产 compose 透传

实跑(一次性库 + 本地假 LLM):开关关时请求与 2a 逐字节相同;开时正常 /
复用 / 坏 JSON / 非法标签 / 行号越界 / 超时 / 编译失败七种场景符合预期;
12 次模型请求里诊断全都带标准答案和测试点,生成全都不带。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-19 06:56:38 -06:00
parent 47d8f46bdb
commit 06ad6745b7
12 changed files with 4308 additions and 30 deletions

View File

@@ -123,6 +123,49 @@ export const HINT_MIN_FAILURES = 3
export const aiHintRequestSchema = z.object({ submissionId: z.string().min(1) })
/**
* AI 提示第一段「诊断」给错误归的类。**key 是落库的值(`ai_hint.diagnosis.tag`
* 和判题状态码一样只能新增、不能改已有 key 的含义** —— 教师端的学情统计要按它聚合。
* `label` 只是给人看的说明,可以改措辞。
*
* 口径按中职入门的 C / Python 定的。`output_format` 刻意写细:多余的输入提示语、
* 全角冒号、多一个空格、小数位数,是这批学生最常见、也最冤的一类 WA。
*/
export const HINT_ERROR_TAGS = {
syntax: "语法错误",
input_format: "输入读取方式不对(格式、分隔、个数)",
output_format:
"输出格式不对(多余的输入提示语、全角/半角符号、多余空格或换行、小数位数)",
condition: "条件判断写错(比较符、漏了分支)",
loop_bound: "循环次数或边界不对(差一)",
integer_division: "整数除法或取余用错",
type_overflow: "数据类型不对或溢出int 不够、浮点精度)",
uninitialized: "变量没初始化,或累加器没清零",
missing_case: "漏了特殊情况0、负数、边界值",
runtime_error: "运行时错误(下标越界、除以零)",
timeout: "超时(算法太慢或死循环)",
wrong_approach: "思路整体不对",
other: "其他,或者看不出来",
} as const
export type HintErrorTag = keyof typeof HINT_ERROR_TAGS
/**
* 诊断的出参。**只有枚举和数字,不允许任何自由文本** —— 诊断那一段能看到标准答案,
* 学生代码又是它的输入,出参里只要有一段文字就是一条把答案带出去的通道。
* 这样注入最多能左右一个枚举值和两个行号。多出来的字段被 zod 剥掉。
*/
export const hintDiagnosisSchema = z.object({
tag: z.enum(
Object.keys(HINT_ERROR_TAGS) as [HintErrorTag, ...HintErrorTag[]],
),
/** 问题所在的行号区间(从 1 起,含两端);说不准就是 null */
lines: z.tuple([z.number().int().min(1), z.number().int().min(1)]).nullable(),
confidence: z.enum(["high", "low"]),
})
export type HintDiagnosis = z.infer<typeof hintDiagnosisSchema>
/**
* 学生对一条 AI 提示的评价POST /ai/hint/:id/feedback。提示的 id 由 /ai/hint 流的
* `done` 事件带回来。可以改票,以最后一次为准。