AI 时代 OJ 设计的 2a:先把现有提示记下来,为后面的分级和两段式诊断攒对照数据。
提示本身的行为不变,发给模型的 prompt 一字未改。
- 迁移 0017 建 ai_hint:模型、prompt 版本、内容、错误、耗时、学生评价;
成功和失败都记(失败率是 AI 悄悄变差时最先动的数)。挂在 submission 上 CASCADE
- streamChat 第三个参数改成 { onComplete, onError }:onComplete 的返回值并进
done 事件,提示 id 由此带回前端;班级学情分析那处调用同步改写,行为不变
- 落库失败只记日志,不影响学生拿到提示
- 新增 POST /ai/hint/:id/feedback:只能评自己的提示(别人的一律 404),可以改票
- 前端提示生成完出「有帮助 / 没帮助」,选中的高亮;后端没存上时不出按钮
- 实跑:本地假 LLM 验证成功 / provider 断开 / 缺 AI_KEY 三条路径的落库,
评价接口六种请求,浏览器里按钮出现、改票、重复点不发请求
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a0ef204bd2
commit
47d8f46bdb
9 files changed
+4220
-21
No files matched your search
@@ -0,0 +1,17 @@
|
||||
-- AI 提示的留痕与学生评价(AI 时代 OJ 设计 2a:先记录、不改行为),字段含义见 schema.ts 的 aiHint。
|
||||
-- 纯建表。上线之前的提示从未落库,这张表从空开始。
|
||||
CREATE TABLE "ai_hint" (
|
||||
"id" bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "ai_hint_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1),
|
||||
"submission_id" text NOT NULL,
|
||||
"model" text NOT NULL,
|
||||
"prompt_version" integer NOT NULL,
|
||||
"content" text NOT NULL,
|
||||
"error" text,
|
||||
"duration_ms" integer NOT NULL,
|
||||
"helpful" boolean,
|
||||
"feedback_time" timestamp with time zone,
|
||||
"create_time" timestamp with time zone NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "ai_hint" ADD CONSTRAINT "ai_hint_submission_id_fk_submission_id" FOREIGN KEY ("submission_id") REFERENCES "public"."submission"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
CREATE INDEX "ai_hint_submission_id_idx" ON "ai_hint" USING btree ("submission_id");
|
||||
File diff suppressed because it is too large.
Load diff
@@ -120,6 +120,13 @@
|
||||
"when": 1789817209482,
|
||||
"tag": "0016_add_submission_trace",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 17,
|
||||
"version": "7",
|
||||
"when": 1789818766735,
|
||||
"tag": "0017_add_ai_hint",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -991,6 +991,60 @@ export const submissionTrace = pgTable(
|
||||
],
|
||||
)
|
||||
|
||||
/**
|
||||
* 每一次「让 AI 分析我的代码」(POST /ai/hint)的留痕,连同学生的评价。
|
||||
*
|
||||
* 在此之前提示一条都没落库,用了多少次、在哪些题上用、用完有没有做出来都无从知道;
|
||||
* 之后要做的提示分级、错误归因都得拿这张表做对照。**生成失败的也记**(content 为空、
|
||||
* error 有值),失败率是 AI 功能悄悄变差时最先动的那个数。
|
||||
*
|
||||
* - 不存 prompt 原文:学生代码在 submission 里、题面在 problem 里,重复存一遍没有意义。
|
||||
* 存的是 `prompt_version` —— 改 system / prompt 的拼法时在 routes/ai.ts 里加一,
|
||||
* 事后才分得清哪批提示是按哪版生成的。
|
||||
* - 人和题顺着 submission 查(submission_id 上有索引)。和 submission_trace 一样
|
||||
* CASCADE 挂在提交上:提交没了,这条提示也就没有上下文了。
|
||||
* - 同一条提交可以有多条:刷新页面之后按钮会重新出现。
|
||||
*/
|
||||
export const aiHint = pgTable(
|
||||
"ai_hint",
|
||||
{
|
||||
id: bigint({ mode: "number" }).primaryKey().generatedByDefaultAsIdentity({
|
||||
name: "ai_hint_id_seq",
|
||||
startWith: 1,
|
||||
increment: 1,
|
||||
minValue: 1,
|
||||
maxValue: "9223372036854775807",
|
||||
cache: 1,
|
||||
}),
|
||||
submissionId: text("submission_id").notNull(),
|
||||
model: text().notNull(),
|
||||
promptVersion: integer("prompt_version").notNull(),
|
||||
// 生成失败时为空串,失败原因在 error
|
||||
content: text().notNull(),
|
||||
error: text(),
|
||||
// 从收到请求到生成结束(或失败)的毫秒数
|
||||
durationMs: integer("duration_ms").notNull(),
|
||||
// 学生的评价:null = 没评
|
||||
helpful: boolean(),
|
||||
feedbackTime: timestamp("feedback_time", {
|
||||
withTimezone: true,
|
||||
mode: "string",
|
||||
}),
|
||||
createTime: timestamp("create_time", {
|
||||
withTimezone: true,
|
||||
mode: "string",
|
||||
}).notNull(),
|
||||
},
|
||||
(table) => [
|
||||
index("ai_hint_submission_id_idx").on(table.submissionId),
|
||||
foreignKey({
|
||||
columns: [table.submissionId],
|
||||
foreignColumns: [submission.id],
|
||||
name: "ai_hint_submission_id_fk_submission_id",
|
||||
}).onDelete("cascade"),
|
||||
],
|
||||
)
|
||||
|
||||
export const tutorial = pgTable(
|
||||
"tutorial",
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user