Files
OJ2/apps/api/src/worker.ts
yuetsh c27c9fdbf9 fix(流程图评分): 队列重试从没生效过、AI 调用不设超时、等级由模型自报
## 重试是摆设

`flowchartQueue` 配了 `attempts: 3` + 指数退避,但任务开头有一道守卫:

    if (!row || ![0, 1].includes(row.flowchart.status)) return

而 catch 里第一件事就是把 status 写成 3(FAILED)。于是第 2、3 次尝试进来一看
状态是 3,直接 return、算作成功 —— **实际只跑了一次**。AI 侧的偶发失败(限流、
超时、网络抖动)永远等不到重试,学生看到「评分失败」只能自己重新提交。

改成只有最后一次尝试才落 FAILED,中间几次把状态留在 PROCESSING(1) 让守卫放行。
「是不是最后一次」由 worker 算好传进来:`attemptsMade` 是「此前已失败几次」,
当前这次还没计入,所以判据是 `attemptsMade + 1 >= attempts`。

实测(用没配 AI_KEY 这条必然失败的路径):修复前 t+1s 就落 FAILED、只评一次;
修复后评满 3 次,状态到 t+7s 才落 FAILED。

## fetch 不设超时

`completeChat` 直接 `fetch`,而 fetch 默认不超时。AI 侧一挂就把 worker 的并发位
(只有 2 个)一直占着,学生那边的按钮也就一直转。加 60 秒超时。

流式调用**不加**:那边超时会把正在推的长回答直接掐断,而客户端断开本来就能收尾。

## 等级不该由模型说了算

提示词里写死了 S/A/B/C 四档分数区间,但模型偶尔会给出「88 分配 S 级」这种自相
矛盾的结果,甚至直接吐「优秀」。脏值会一路串到等级分布图、等级筛选,以及
「A/S 才把流程图展示给学生」的判断里。改成一律由分数推出等级,模型自报的 grade
不再采信;score 本来就已经 clamp 到 0-100。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 06:14:21 -06:00

50 lines
1.7 KiB
TypeScript

import { Worker } from "bullmq"
import { config } from "./config"
import { judgeQueueName, type JudgeJobData } from "./judge/job"
import { judgeSubmission } from "./judge/run"
import { flowchartQueueName, type FlowchartJobData } from "./flowchart/job"
import { evaluateFlowchart } from "./flowchart/run"
import { createBlockingRedis } from "./redis"
const worker = new Worker<JudgeJobData>(
judgeQueueName,
async (job) => judgeSubmission(job.data),
{
connection: createBlockingRedis(),
concurrency: config.judgeConcurrency,
},
)
const flowchartWorker = new Worker<FlowchartJobData>(
flowchartQueueName,
// attemptsMade 是「此前已经失败过几次」,当前这次还没计进去,
// 所以最后一次尝试的判据是 attemptsMade + 1 >= attempts
async (job) => evaluateFlowchart(job.data, {
isFinalAttempt: job.attemptsMade + 1 >= (job.opts.attempts ?? 1),
}),
{ connection: createBlockingRedis(), concurrency: 2 },
)
worker.on("ready", () => {
console.log(`Judge worker ready (concurrency=${config.judgeConcurrency})`)
})
worker.on("failed", (job, error) => {
console.error(`Judge job ${job?.id ?? "unknown"} failed`, error)
})
worker.on("error", (error) => {
console.error("Judge worker error", error)
})
flowchartWorker.on("ready", () => console.log("Flowchart worker ready (concurrency=2)"))
flowchartWorker.on("failed", (job, error) => console.error(`Flowchart job ${job?.id ?? "unknown"} failed`, error))
flowchartWorker.on("error", (error) => console.error("Flowchart worker error", error))
async function shutdown() {
await worker.close()
await flowchartWorker.close()
process.exit(0)
}
process.on("SIGINT", shutdown)
process.on("SIGTERM", shutdown)