Audit complete: 2 quiz systems (procedural + AI Runtime), 4 data models, 3 question types (choice/fill/judge), external Runtime polling architecture. Identified 7 anomalies including answer leak (P0) and missing prompt/schema. Co-Authored-By: Claude <noreply@anthropic.com>
586 lines
25 KiB
Markdown
586 lines
25 KiB
Markdown
# M-AI-07 Quiz 生成与题目产物迁移契约
|
||
|
||
> 审计日期:2026-06-22
|
||
> 契约状态:冻结(待 M-AI-07-01 验收)
|
||
> 对应里程碑:M-AI-07 Quiz 生成与题目产物统一引擎迁移
|
||
|
||
---
|
||
|
||
## 目录
|
||
|
||
1. [当前链路审计](#1-当前链路审计)
|
||
2. [目标链路](#2-目标链路)
|
||
3. [数据模型](#3-数据模型)
|
||
4. [题型规则](#4-题型规则)
|
||
5. [Snapshot Schema](#5-snapshot-schema)
|
||
6. [Output Schema](#6-output-schema)
|
||
7. [副作用矩阵](#7-副作用矩阵)
|
||
8. [Artifact 矩阵](#8-artifact-矩阵)
|
||
9. [幂等契约](#9-幂等契约)
|
||
10. [权限模型](#10-权限模型)
|
||
11. [答案隐藏契约](#11-答案隐藏契约)
|
||
12. [Feature Flag](#12-feature-flag)
|
||
13. [回滚流程](#13-回滚流程)
|
||
14. [架构异常与不确定项](#14-架构异常与不确定项)
|
||
|
||
---
|
||
|
||
## 1. 当前链路审计
|
||
|
||
### 1.1 当前时序(Legacy Runtime 轮询路径)
|
||
|
||
```text
|
||
Client API Server External Runtime
|
||
| | |
|
||
| POST /api/ai/jobs | |
|
||
| { jobType:"quiz_generation", | |
|
||
| targetType:"knowledge_base", | |
|
||
| targetId:"kb-1", | |
|
||
| questionCount:10, | |
|
||
| questionTypes:["choice"], | |
|
||
| difficultyLevel:"medium", | |
|
||
| idempotencyKey? } | |
|
||
|-------------------------------->| |
|
||
| | UserAiService.createAnalysisJob() |
|
||
| | `src/modules/ai-runtime/user-ai.service.ts:199` |
|
||
| | 1. Settings check (:201) |
|
||
| | 2. Validate jobType (:210-214) |
|
||
| | 3. Validate targetType=knowledge_base (:216-222) |
|
||
| | 4. Idempotency check (:225-233) |
|
||
| | 5. Resolve credential (:237-249) |
|
||
| | 6. Quota check (:252) |
|
||
| | 7. Platform budget check (:255-267) |
|
||
| | 8. Build snapshot (:270) |
|
||
| | 9. Resolve prompt/schema versions |
|
||
| | → quiz_gen_v1 / quiz_output_v1 |
|
||
| | `user-ai.service.ts:15-17` |
|
||
| | 10. Compute priority (:279) |
|
||
| | 11. Create aiRuntimeJob (pending) |
|
||
| | modelProvider=deepseek |
|
||
| | modelName=deepseek-chat (:294) |
|
||
| | `user-ai.service.ts:282-299` |
|
||
| | 12. Create QuestionGenerationPlan |
|
||
| | `user-ai.service.ts:303-318` |
|
||
| | 13. Increment quota (:338) |
|
||
| | |
|
||
| { jobId, status:"pending", | |
|
||
| planId } | |
|
||
|<--------------------------------| |
|
||
| | |
|
||
| | POST /internal/runtime/jobs/poll |
|
||
| | (InternalAuthGuard) |
|
||
| | `runtime-internal.controller.ts:22`|
|
||
| |<-------------------------------------|
|
||
| | RuntimeInternalService.pollJobs() |
|
||
| | `runtime-internal.service.ts:22` |
|
||
| | → CAS lockJob (:85) |
|
||
| | → getSnapshot (:153) |
|
||
| | → resolveCredential (:201) |
|
||
| | |
|
||
| | ★ Runtime makes AI call directly |
|
||
| | ★ prompt/schema managed externally |
|
||
| | ★ API server has NO quiz prompt |
|
||
| | ★ API server has NO quiz schema |
|
||
| | |
|
||
| | POST /internal/runtime/jobs/:id/result
|
||
| | { validatedOutput: { |
|
||
| | quizTitle, quizDescription, |
|
||
| | questions: [{ |
|
||
| | type, stem/question, |
|
||
| | options, answer/correctAnswer,|
|
||
| | explanation, sourceBlockIds |
|
||
| | }] } } |
|
||
| |<-------------------------------------|
|
||
| | |
|
||
| | submitResult() → persistResult() |
|
||
| | `runtime-internal.service.ts:220` |
|
||
| | → quiz_generation (:367-381) |
|
||
| | update Plan→completed |
|
||
| | → convertQuizCandidates() |
|
||
| | `runtime-internal.service.ts:435`|
|
||
| | Quiz.create (status=ready) |
|
||
| | QuizQuestion.create × N |
|
||
| | dedup by stem (:463-472) |
|
||
| | |
|
||
| POST /api/ai/quizzes/:id/publish | |
|
||
| → status: ready→active | |
|
||
```
|
||
|
||
### 1.2 双系统并存
|
||
|
||
| 特性 | Procedural Quiz | AI Quiz Generation |
|
||
|------|----------------|-------------------|
|
||
| 入口 | `POST /quizzes` | `POST /ai/jobs` (jobType=quiz_generation) |
|
||
| Controller | `quiz.controller.ts` | `user-ai.controller.ts` |
|
||
| Service | `quiz.service.ts` | `user-ai.service.ts` / `runtime-internal.service.ts` |
|
||
| AI 调用 | **无**(程序化生成) | **外部 Runtime**(API 不管理 prompt/schema) |
|
||
| 题目类型 | choice, fill, judge | Runtime 决定,默认 choice |
|
||
| Job 系统 | 无 | `aiRuntimeJob`(非统一 AiJob) |
|
||
| 队列 | 无 | Runtime 轮询(非 BullMQ) |
|
||
| 幂等 | 无 | `idempotencyKey` on aiRuntimeJob |
|
||
| 数据模型 | Quiz + QuizQuestion | 相同 |
|
||
| 状态 | 直接 active | ready → publish → active |
|
||
| 计费 | 无 | quota.incrementJobCount |
|
||
|
||
**本里程碑只迁移 AI Quiz Generation 路径。Procedural Quiz 保持不变。**
|
||
|
||
### 1.3 Quiz 数据模型
|
||
|
||
**文件**: `prisma/schema.prisma:1833-1904`
|
||
|
||
```text
|
||
Quiz
|
||
├─ id String @id @default(cuid())
|
||
├─ userId String
|
||
├─ knowledgeBaseId String
|
||
├─ title String @db.VarChar(255)
|
||
├─ description String? @db.Text
|
||
├─ questionCount Int @default(0)
|
||
├─ sourceType String @default("kb") @db.VarChar(16) // "kb" | "ai"
|
||
├─ sourceId String? @db.VarChar(100) // aiRuntimeJob.id
|
||
├─ status String @default("ready") @db.VarChar(16) // ready|active|archived
|
||
├─ createdAt DateTime @default(now())
|
||
└─ updatedAt DateTime @updatedAt
|
||
@@index([userId]), @@index([knowledgeBaseId])
|
||
|
||
QuizQuestion
|
||
├─ id String @id @default(cuid())
|
||
├─ quizId String
|
||
├─ type String @db.VarChar(16) // "choice"|"fill"|"judge"
|
||
├─ stem String @db.Text // 题干
|
||
├─ options Json? // 选项数组 (choice 题型)
|
||
├─ answer String @db.VarChar(500) // 正确答案
|
||
├─ explanation String? @db.Text // 解析
|
||
├─ sourceBlockIds Json? // 来源文本块引用
|
||
├─ orderIndex Int @default(0)
|
||
└─ createdAt DateTime @default(now())
|
||
@@index([quizId])
|
||
|
||
QuizAttempt
|
||
├─ id String @id @default(cuid())
|
||
├─ quizId String
|
||
├─ userId String
|
||
├─ totalQuestions Int @default(0)
|
||
├─ correctCount Int @default(0)
|
||
├─ score Int @default(0) // 0-100
|
||
├─ startedAt DateTime @default(now())
|
||
└─ finishedAt DateTime?
|
||
@@index([quizId]), @@index([userId])
|
||
|
||
QuizAnswer
|
||
├─ id String @id @default(cuid())
|
||
├─ attemptId String
|
||
├─ questionId String
|
||
├─ userAnswer String @db.VarChar(500)
|
||
├─ isCorrect Boolean @default(false)
|
||
└─ answeredAt DateTime @default(now())
|
||
@@index([attemptId]), @@index([questionId])
|
||
|
||
QuestionGenerationPlan (Legacy — 由 Unified 的 AiJob + Snapshot 替代)
|
||
├─ id String @id @default(cuid())
|
||
├─ userId String
|
||
├─ jobId String // → aiRuntimeJob.id
|
||
├─ snapshotId String?
|
||
├─ targetType String? @db.VarChar(32)
|
||
├─ targetId String? @db.VarChar(255)
|
||
├─ knowledgePointIds Json?
|
||
├─ questionTypes Json?
|
||
├─ difficultyLevel String? @db.VarChar(32) // easy|medium|hard
|
||
├─ count Int @default(5)
|
||
├─ reason String? @db.Text
|
||
├─ status String @default("pending")
|
||
├─ createdAt DateTime
|
||
└─ updatedAt DateTime
|
||
@@index([userId])
|
||
```
|
||
|
||
### 1.4 当前题型
|
||
|
||
| 题型 | 代码值 | 选项结构 | 答案结构 | 来源 |
|
||
|------|--------|---------|---------|------|
|
||
| 选择题 | `choice` | `Json` 数组(选项字符串) | 正确选项的索引(字符串) | `quiz.service.ts:43-51` |
|
||
| 填空题 | `fill` | 无 | 缺失的关键词 | `quiz.service.ts:52-56` |
|
||
| 判断题 | `judge` | 无 | `"true"` 或 `"false"`(字符串) | `quiz.service.ts:57-60` |
|
||
|
||
**AI 生成的题目默认 type=`'choice'`**(`runtime-internal.service.ts:486`),但可包含其他类型(由 Runtime 决定)。
|
||
|
||
### 1.5 输入字段分类
|
||
|
||
| 字段 | 来源 | 分类 |
|
||
|------|------|------|
|
||
| `userId` | JWT | **进入 Snapshot** |
|
||
| `targetType` | DTO | **进入 Snapshot** |
|
||
| `targetId` (knowledgeBaseId) | DTO | **进入 Snapshot** |
|
||
| `questionCount` | DTO (1-50) | **进入 Snapshot** |
|
||
| `questionTypes` | DTO (string[]) | **进入 Snapshot** |
|
||
| `difficultyLevel` | DTO | **进入 Snapshot** |
|
||
| `knowledgePointIds` | DTO | **进入 Snapshot** |
|
||
| `idempotencyKey` | DTO | **进入 Snapshot** |
|
||
| `promptKey` | 配置 `quiz_gen_v1` | **进入 Snapshot** |
|
||
| `promptVersion` | 配置 | **进入 Snapshot** |
|
||
| `modelTier` | 配置 | **进入 Snapshot** |
|
||
| 知识库内容 | DB 查询 | **进入 Snapshot(限制长度)** |
|
||
| 知识点列表 | DB 查询 | **进入 Snapshot(限制数量+内容长度)** |
|
||
| JWT / Authorization | Request Header | **禁止进入 Snapshot** |
|
||
| API Key | DB | **禁止进入 Snapshot** |
|
||
| 用户学习画像(完整) | DB | **禁止进入 Snapshot**(仅取必要配置) |
|
||
| 完整知识库内容(未筛选) | DB | **禁止进入 Snapshot** |
|
||
|
||
### 1.6 副作用矩阵
|
||
|
||
| 副作用 | 当前行为 | Unified 行为 |
|
||
|--------|---------|-------------|
|
||
| 配额消耗 | `quota.incrementJobCount` (:338) | 保持(通过 CreationService 或 Router) |
|
||
| 平台预算检查 | `budget.checkPlatformBudget` (:257) | 保持 |
|
||
| QuestionGenerationPlan | 创建 plan 记录 (:303-318) | **废弃** — 由 AiJob + Snapshot 替代 |
|
||
| 使用日志 | Runtime 记录 | API 侧通过 Engine 的 usage log |
|
||
| 学习记录 | 不创建 | 不创建 |
|
||
| 通知 | 不创建 | 不创建 |
|
||
|
||
### 1.7 答案泄漏风险(审计发现)
|
||
|
||
**P0 风险**:`user-ai.service.ts:546-556` `getQuizQuestions()` 在 SELECT 中包含 `answer` 和 `explanation` 字段。任何已认证用户可在未开始答题前通过此接口获取正确答案。
|
||
|
||
**Procedural quiz**:`quiz.service.ts:83-88` `findOne()` include `questions` 包含完整 `answer` 字段。同样泄漏。
|
||
|
||
**建议**:M-AI-07-06 必须在 Unified 路径修复此问题。Legacy 路径暂不修改(避免影响现有客户端)。
|
||
|
||
---
|
||
|
||
## 2. 目标链路
|
||
|
||
### 2.1 目标时序(Unified 路径)
|
||
|
||
```text
|
||
POST /api/ai/jobs
|
||
{ jobType: "quiz_generation", targetType: "knowledge_base", targetId, ... }
|
||
→ QuizExecutionRouter
|
||
├─ [legacy] → UserAiService.createAnalysisJob() ← 原路径
|
||
└─ [unified] →
|
||
QuizGenerationSnapshotBuilder.build()
|
||
→ AiJobCreationService.createJob()
|
||
→ AiJob + Snapshot + Outbox
|
||
→ BullMQ (ai-interactive)
|
||
→ Worker → AiJobExecutionEngine
|
||
EXECUTE: QuizGenerationExecutor (AiGateway)
|
||
VALIDATE: QuizGenerationValidator
|
||
PROJECT: QuizGenerationProjector
|
||
→ Quiz + QuizQuestion × N + Artifact
|
||
→ markSucceeded
|
||
```
|
||
|
||
### 2.2 关键差异
|
||
|
||
| 维度 | Legacy (Runtime) | Unified |
|
||
|------|-----------------|---------|
|
||
| AI 调用方 | 外部 Runtime 直接调用 Provider | API 内 Executor → AiGateway |
|
||
| Prompt 管理 | Runtime 外部管理 | API 内 PromptTemplateService |
|
||
| Schema 管理 | Runtime 外部管理 | API 内 Zod Schema |
|
||
| Job 系统 | aiRuntimeJob | 统一 AiJob |
|
||
| 队列 | Runtime 轮询 | BullMQ (ai-interactive) |
|
||
| 结果持久化 | runtime-internal.service | QuizGenerationProjector |
|
||
| 事务 | 非原子(Quiz + Questions 分步) | 原子(同一 Prisma 事务) |
|
||
|
||
---
|
||
|
||
## 3. 数据模型(迁移相关约束)
|
||
|
||
### 3.1 保持不变的字段
|
||
|
||
Quiz 和 QuizQuestion 的所有字段保持不变。Unified Projector 写入相同的表和列。
|
||
|
||
### 3.2 ID 策略
|
||
|
||
| 实体 | 当前 | Unified |
|
||
|------|------|---------|
|
||
| Quiz.id | `@default(cuid())` | 如允许显式赋值 → `deterministic(jobId)`;否则 CUID + Artifact 幂等 |
|
||
| QuizQuestion.id | `@default(cuid())` | 如允许显式赋值 → `deterministic(jobId + ordinal)`;否则 CUID + Artifact 幂等 |
|
||
| QuizQuestion.options | `Json?` | 选择题:`string[]`;填空/判断:`null` |
|
||
| QuizQuestion.answer | `String` | **按当前格式保存**(choice=索引字符串,fill=关键词,judge="true"/"false") |
|
||
|
||
### 3.3 QuestionGenerationPlan
|
||
|
||
Unified 路径不再创建 `QuestionGenerationPlan`。由 AiJob + AiJobSnapshot 替代其功能。
|
||
|
||
---
|
||
|
||
## 4. 题型规则
|
||
|
||
### 4.1 支持题型(冻结)
|
||
|
||
```text
|
||
choice — 单选题(1 个正确答案)
|
||
fill — 填空题(缺失关键词)
|
||
judge — 判断题(true/false)
|
||
```
|
||
|
||
不新增题型。如果 AI 输出其他类型,Validator 拒绝。
|
||
|
||
### 4.2 题型约束
|
||
|
||
| 题型 | options | answer | 验证规则 |
|
||
|------|---------|--------|---------|
|
||
| `choice` | `string[]`(≥2 项) | 正确选项索引(字符串) | answer 必须是 options 的有效索引 |
|
||
| `fill` | 不需要 | 缺失关键词 | answer 非空 |
|
||
| `judge` | 不需要 | `"true"` 或 `"false"` | 必须是这两者之一 |
|
||
|
||
---
|
||
|
||
## 5. Snapshot Schema
|
||
|
||
### 5.1 必须包含
|
||
|
||
```typescript
|
||
interface QuizGenerationSnapshot {
|
||
schemaVersion: string; // "quiz-generation-v1"
|
||
snapshot: {
|
||
userId: string;
|
||
targetType: string; // "knowledge_base"
|
||
targetId: string; // knowledgeBaseId
|
||
submissionId: string; // 稳定幂等标识
|
||
|
||
// 知识内容(限制长度)
|
||
knowledgeBaseTitle: string;
|
||
knowledgeBaseDescription: string;
|
||
knowledgeItems: Array<{ // 限制数量 ≤ 50
|
||
id: string;
|
||
title: string;
|
||
content: string; // 截断 ≤ 2000 字符
|
||
summary: string;
|
||
}>;
|
||
|
||
// 生成参数
|
||
questionCount: number; // 1-50
|
||
questionTypes: string[]; // ["choice","fill","judge"]
|
||
difficultyLevel: string; // "easy"|"medium"|"hard"
|
||
knowledgePointIds: string[]; // 空数组表示全部知识点
|
||
|
||
// Prompt/模型策略(从 Definition 读取)
|
||
promptKey: string;
|
||
promptVersion: string;
|
||
modelTier: string;
|
||
inputSchemaVersion: string;
|
||
outputSchemaVersion: string;
|
||
|
||
createdAt: string; // ISO8601 归一化到秒
|
||
};
|
||
}
|
||
```
|
||
|
||
### 5.2 禁止进入 Snapshot
|
||
|
||
- JWT / Authorization Header
|
||
- API Key / Provider Credential
|
||
- 完整知识库(未筛选+未截断)
|
||
- 完整用户学习画像
|
||
- 其他用户内容
|
||
- 数据库连接信息
|
||
|
||
---
|
||
|
||
## 6. Output Schema
|
||
|
||
### 6.1 AI 输出结构(冻结)
|
||
|
||
```typescript
|
||
interface QuizGenerationOutput {
|
||
quizTitle: string; // 测验标题(非空,≤255)
|
||
quizDescription?: string; // 测验描述
|
||
questions: Array<{
|
||
type: 'choice' | 'fill' | 'judge';
|
||
stem: string; // 题干(非空)
|
||
options?: string[]; // 选项(choice 题型必填,≥2)
|
||
answer: string; // 正确答案
|
||
explanation: string; // 解析(非空)
|
||
sourceBlockIds?: string[]; // 来源引用
|
||
}>;
|
||
}
|
||
```
|
||
|
||
### 6.2 验证规则
|
||
|
||
| 规则 | 严重程度 |
|
||
|------|---------|
|
||
| questions 数组非空,≤ 50 | P0 |
|
||
| quizTitle 非空,≤ 255 | P0 |
|
||
| stem 非空 | P0 |
|
||
| type ∈ {choice, fill, judge} | P0 |
|
||
| choice → options 数组 ≥ 2 项 | P0 |
|
||
| choice → answer 是 options 的有效索引 | P0 |
|
||
| fill → answer 非空 | P0 |
|
||
| judge → answer ∈ {"true", "false"} | P0 |
|
||
| explanation 非空 | P1 |
|
||
| 题目不重复(相同 stem) | P1 |
|
||
| 选项不重复 | P1 |
|
||
| 无 Markdown 代码围栏 | P0 |
|
||
| 无模型指令文本 | P0 |
|
||
|
||
---
|
||
|
||
## 7. 副作用矩阵
|
||
|
||
| 副作用 | Legacy | Unified |
|
||
|--------|--------|---------|
|
||
| AiJob 创建 | aiRuntimeJob | 统一 AiJob |
|
||
| Snapshot 创建 | snapshot_builder | AiJobSnapshot |
|
||
| 队列入队 | Runtime 轮询 | OutboxEvent → BullMQ |
|
||
| 配额消耗 | quota.incrementJobCount | 保持(Router 层) |
|
||
| 平台预算检查 | budget.checkPlatformBudget | 保持(Router 层) |
|
||
| QuestionGenerationPlan | 创建 | **不创建**(由 AiJob+Snapshot 替代) |
|
||
| 使用日志 | Runtime 记录 | Engine 记录 |
|
||
| 学习记录 | 无 | 无 |
|
||
| 通知 | 无 | 无 |
|
||
|
||
---
|
||
|
||
## 8. Artifact 矩阵
|
||
|
||
| 实体 | artifactType | artifactId | ordinal |
|
||
|------|-------------|-----------|---------|
|
||
| Quiz | `"quiz"` | Quiz.id | 0 |
|
||
| (可选) QuizQuestion | metadata 记录 | — | — |
|
||
|
||
不为每个 QuizQuestion 创建独立 Artifact(避免 Artifact 膨胀)。
|
||
|
||
---
|
||
|
||
## 9. 幂等契约
|
||
|
||
### 9.1 请求级幂等
|
||
|
||
```text
|
||
idempotencyKey = "quiz-generation:<submissionId>"
|
||
```
|
||
|
||
- `submissionId` 优先使用客户端传入的 `idempotencyKey`
|
||
- 回退:`<targetType>:<targetId>:<contentHash>`
|
||
- Db 级唯一约束:`AiJob.@@unique([userId, jobType, idempotencyKey])`
|
||
|
||
### 9.2 投影级幂等
|
||
|
||
```text
|
||
Quiz: deterministic ID = "qz_<jobId>"
|
||
Question: deterministic ID = "qq_<jobId>_<ordinal>"
|
||
```
|
||
|
||
如 Prisma `@default(cuid())` 不允许显式 ID 赋值,使用入口 Artifact 检查 + P2002 catch 作为替代(与 M-AI-06 ReviewCard 一致)。
|
||
|
||
### 9.3 禁止的幂等方式
|
||
|
||
- ❌ `findFirst({ stem }) → create` — 不同 Quiz 可能有相同题干
|
||
- ❌ 全局 stem 去重 — 当前 `convertQuizCandidates` 使用此方式 (`runtime-internal.service.ts:463-472`),迁移后应改为确定性 ID
|
||
|
||
---
|
||
|
||
## 10. 权限模型
|
||
|
||
| 操作 | 权限 |
|
||
|------|------|
|
||
| 生成 Quiz | 用户只能为自己的 knowledgeBase 生成 |
|
||
| 查询 Quiz | 只能查询自己的 Quiz (`userId` 过滤) |
|
||
| 查询 Question | 只能查询自己 Quiz 的 Question |
|
||
| 发布 Quiz | 只能发布自己的 Quiz |
|
||
| Admin 查询 | Admin 可查看所有(按现有 Admin 权限) |
|
||
|
||
---
|
||
|
||
## 11. 答案隐藏契约
|
||
|
||
**当前缺陷**:`getQuizQuestions()` 直接返回 `answer` + `explanation` 字段(`user-ai.service.ts:553`)。`findOne()` include questions 同样泄漏(`quiz.service.ts:85`)。
|
||
|
||
**Unified 要求**:
|
||
- Quiz 列表 DTO:不返回 questions 数组(或不含 answer/explanation)
|
||
- Quiz 详情 DTO(未作答前):不返回 answer/explanation
|
||
- 开始答题后:不通过查询接口返回答案(答案仅在 submit 时服务端比对)
|
||
- 提交后:QuizAttempt.getResults 可以返回 question.answer(当前行为保留)
|
||
- Admin:可以查看答案
|
||
|
||
**M-AI-07-06 必须修复此问题。Legacy 路径暂不修改。**
|
||
|
||
---
|
||
|
||
## 12. Feature Flag
|
||
|
||
```text
|
||
Flag Name: QUIZ_GENERATION_ENGINE_MODE
|
||
Values: legacy | unified
|
||
Default: legacy
|
||
```
|
||
|
||
| 模式 | 行为 |
|
||
|------|------|
|
||
| `legacy` | UserAiService.createAnalysisJob() → aiRuntimeJob → Runtime 轮询 |
|
||
| `unified` | QuizGenerationSnapshotBuilder → AiJobCreationService → BullMQ → Engine |
|
||
|
||
---
|
||
|
||
## 13. 回滚流程
|
||
|
||
```text
|
||
unified → legacy:
|
||
1. 修改 FeatureFlag: QUIZ_GENERATION_ENGINE_MODE = legacy
|
||
2. 新请求立即走 Legacy 路径
|
||
3. 已创建的 Unified Job 继续完成
|
||
4. 相同 submission 不重新进入 Legacy
|
||
5. 已生成 Quiz 保留
|
||
6. 无需数据库回滚
|
||
```
|
||
|
||
---
|
||
|
||
## 14. 架构异常与不确定项
|
||
|
||
### 14.1 架构异常
|
||
|
||
| # | 异常 | 严重程度 | 处理方式 |
|
||
|---|------|---------|---------|
|
||
| A1 | API 服务器无 Quiz Prompt 模板 | **阻塞** | M-AI-07-03 需新增 `quiz-generation.prompt.ts` 和 `quiz-generation.schema.ts`(从 Runtime 获取或重新设计) |
|
||
| A2 | API 服务器无 Quiz Output Schema | **阻塞** | 同上 |
|
||
| A3 | `getQuizQuestions()` 泄漏答案 | P0 | M-AI-07-06 修复 |
|
||
| A4 | `findOne()` include questions 泄漏答案 | P0 | M-AI-07-06 修复 |
|
||
| A5 | `deepseek-chat` 硬编码(非 ModelRouter) | P2 | Unified 使用 Definition + ModelRouter |
|
||
| A6 | 当前题目去重依赖 `stem` 全局匹配 | P2 | Unified 使用确定性 ID |
|
||
| A7 | Quiz 状态 `ready` → `publish` → `active` 两阶段 | P2 | Unified 可简化为直接 `active`,或保留两阶段 |
|
||
|
||
### 14.2 不确定项
|
||
|
||
| 项 | 问题 | 建议 |
|
||
|----|------|------|
|
||
| Prompt 来源 | 当前 prompt 由外部 Runtime 管理 | 从 Runtime 团队获取 `quiz_gen_v1` 的 system prompt 和 output schema,或基于业务需求重新设计 |
|
||
| 模型 Tier | 当前硬编码 `deepseek-chat`,不经过 ModelRouter | Unified Definition 使用 `modelTier`(建议 `primary`),通过 ModelRouter 解析 |
|
||
| questionTypes 扩展 | 当前仅 choice/fill/judge,是否需支持 multi_choice/short_answer | 本批不扩展,严格按现有题型 |
|
||
| QuestionGenerationPlan | 是否保留此表供旧数据查询 | Unified 不创建新记录,旧记录保留 |
|
||
|
||
---
|
||
|
||
## 附录 A:关键文件索引
|
||
|
||
| 用途 | 绝对路径 | 关键行号 |
|
||
|------|---------|---------|
|
||
| AI Quiz 入口 Controller | `src/modules/ai-runtime/user-ai.controller.ts` | `:63` createAnalysisJob, `:85` publishQuiz, `:134` getQuiz, `:139` getQuizQuestions |
|
||
| AI Quiz 入口 Service | `src/modules/ai-runtime/user-ai.service.ts` | `:199` createAnalysisJob, `:374` publishQuiz, `:533` getQuiz, `:546` getQuizQuestions |
|
||
| AI Job DTO | `src/modules/ai-runtime/user-ai.dto.ts` | `:67` VALID_JOB_TYPES, `:74` CreateAnalysisJobDto |
|
||
| Runtime Internal Controller | `src/modules/ai-runtime/internal/runtime-internal.controller.ts` | `:22` pollJobs, `:58` submitResult |
|
||
| Runtime Internal Service | `src/modules/ai-runtime/internal/runtime-internal.service.ts` | `:22` pollJobs, `:220` submitResult, `:367` quiz persisting, `:435` convertQuizCandidates |
|
||
| Job Config | `src/modules/ai-runtime/user-ai.service.ts` | `:15-17` JOB_TYPE_CONFIG (quiz_gen_v1/quiz_output_v1) |
|
||
| Procedural Quiz Controller | `src/modules/quiz/quiz.controller.ts` | `:12` create, `:24` findOne |
|
||
| Procedural Quiz Service | `src/modules/quiz/quiz.service.ts` | `:8` create, `:38` 题型 logic |
|
||
| Quiz Prisma Model | `prisma/schema.prisma` | `:1833-1853` Quiz, `:1855-1871` QuizQuestion |
|
||
| QuizAttempt Model | `prisma/schema.prisma` | `:1873-1889` |
|
||
| QuizAnswer Model | `prisma/schema.prisma` | `:1891-1904` |
|
||
| QuestionGenerationPlan | `prisma/schema.prisma` | `:2243-2260` |
|
||
| Snapshot Builder | `src/modules/ai-runtime/snapshot-builder.service.ts` | `:379` quizAttempt data |
|
||
| Model Router | `src/modules/ai/model-router.ts` | `:18-22` cheap tier, `:24-28` primary tier |
|
||
| Prompt Template Service | `src/modules/ai/prompts/prompt-template.service.ts` | 无 quiz prompt(需新增) |
|
||
|
||
## 附录 B:题型对比
|
||
|
||
| 维度 | Procedural Quiz | AI Quiz (Legacy) | AI Quiz (Unified) |
|
||
|------|----------------|-----------------|-------------------|
|
||
| 生成方式 | 随机抽取+交替类型 | Runtime AI 调用 | AiGateway AI 调用 |
|
||
| 题型 | choice/fill/judge | Runtime 决定 | choice/fill/judge(Validator 限制) |
|
||
| 选项 | 其他 item 内容截断 | AI 生成 | AI 生成 |
|
||
| 答案 | 正确选项索引/关键词 | AI 生成 | AI 生成 |
|
||
| 解析 | item.content 截断 | AI 生成 | AI 生成 |
|
||
| 状态 | 直接可用 | ready→publish→active | 同 Legacy 或直接 active |
|