api-server/docs/architecture/m-ai-07-quiz-generation-migration-contract.md
wangdl a17aba972b
All checks were successful
Deploy API Server / build-and-unit (push) Successful in 36s
Deploy API Server / current-integration (push) Successful in 30s
Deploy API Server / backward-compat (push) Successful in 0s
Deploy API Server / deploy (push) Successful in 1m5s
docs: M-AI-07-01 quiz generation migration contract
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>
2026-06-22 20:29:57 +08:00

586 lines
25 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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/judgeValidator 限制) |
| 选项 | 其他 item 内容截断 | AI 生成 | AI 生成 |
| 答案 | 正确选项索引/关键词 | AI 生成 | AI 生成 |
| 解析 | item.content 截断 | AI 生成 | AI 生成 |
| 状态 | 直接可用 | ready→publish→active | 同 Legacy 或直接 active |