diff --git a/src/modules/ai-job/ai-job-execution-engine.spec.ts b/src/modules/ai-job/ai-job-execution-engine.spec.ts index 8efb40a..c64ea5f 100644 --- a/src/modules/ai-job/ai-job-execution-engine.spec.ts +++ b/src/modules/ai-job/ai-job-execution-engine.spec.ts @@ -12,6 +12,8 @@ import { FeynmanBusinessValidator, FeynmanReferenceValidator } from './feynman-v import { FeynmanObservabilityService } from './feynman-observability.service'; import { ReviewCardGenerationExecutor } from './review-card-generation-executor'; import { ReviewCardGenerationValidator } from './review-card-generation-validator'; +import { QuizGenerationExecutor } from './quiz-generation-executor'; +import { QuizGenerationValidator } from './quiz-generation-validator'; import { PrismaService } from '../../infrastructure/database/prisma.service'; import { JobLockConflictError, JobAlreadyTerminalError } from './ai-job.errors'; @@ -92,6 +94,8 @@ describe('AiJobExecutionEngineImpl', () => { { provide: FeynmanReferenceValidator, useValue: { validate: jest.fn() } }, { provide: ReviewCardGenerationExecutor, useValue: { execute: jest.fn() } }, { provide: ReviewCardGenerationValidator, useValue: { validate: jest.fn() } }, + { provide: QuizGenerationExecutor, useValue: { execute: jest.fn() } }, + { provide: QuizGenerationValidator, useValue: { validate: jest.fn() } }, { provide: ActiveRecallObservabilityService, useValue: { incrementUnifiedExecuteSuccess: jest.fn(), incrementUnifiedExecuteFailed: jest.fn(), diff --git a/src/modules/ai-job/ai-job-execution-engine.ts b/src/modules/ai-job/ai-job-execution-engine.ts index 12d5519..205decb 100644 --- a/src/modules/ai-job/ai-job-execution-engine.ts +++ b/src/modules/ai-job/ai-job-execution-engine.ts @@ -16,8 +16,12 @@ import { ReviewCardGenerationValidator } from './review-card-generation-validato import type { ActiveRecallSnapshot } from './active-recall-snapshot-builder'; import type { FeynmanSnapshot } from './feynman-snapshot-builder'; import type { ReviewCardGenerationSnapshot } from './review-card-generation-snapshot-builder'; +import type { QuizGenerationSnapshot } from './quiz-generation-snapshot-builder'; import type { FeynmanEvaluationResult } from '../ai/prompts/schemas/feynman-evaluation.schema'; import type { ReviewCardGenerationResult } from '../ai/prompts/schemas/review-card-generation.schema'; +import type { QuizGenerationResult } from '../ai/prompts/schemas/quiz-generation.schema'; +import { QuizGenerationExecutor } from './quiz-generation-executor'; +import { QuizGenerationValidator } from './quiz-generation-validator'; import { AiJobExecutionEngine, EngineJobContext, @@ -94,6 +98,8 @@ export class AiJobExecutionEngineImpl implements AiJobExecutionEngine { private readonly feynmanReferenceValidator: FeynmanReferenceValidator, private readonly reviewCardExecutor: ReviewCardGenerationExecutor, private readonly reviewCardValidator: ReviewCardGenerationValidator, + private readonly quizExecutor: QuizGenerationExecutor, + private readonly quizValidator: QuizGenerationValidator, private readonly observability: ActiveRecallObservabilityService, private readonly feynmanObs: FeynmanObservabilityService, ) {} @@ -242,6 +248,26 @@ export class AiJobExecutionEngineImpl implements AiJobExecutionEngine { ); throw validationErr; } + } else if (job.jobType === 'quiz_generation' && snapshot) { + // M-AI-07-03: Quiz Generation Executor + const quizSnapshot = snapshot as unknown as QuizGenerationSnapshot; + response = await this.quizExecutor.execute(quizSnapshot, timeoutMs); + parsedOutput = response.parsed; + + this.logger.log( + `Quiz Executor completed: job=${aiJobId} ` + + `questions=${(parsedOutput as any)?.questions?.length}`, + ); + + // ── M-AI-07-03: 输出验证 ── + try { + this.quizValidator.validate(parsedOutput as QuizGenerationResult); + } catch (validationErr: any) { + this.logger.warn( + `Quiz validation failed for job=${aiJobId}: ${validationErr.message}`, + ); + throw validationErr; + } } else { // 默认路径:直接调用 AiGateway(synthetic_job 等) response = await this.aiGateway.generate( diff --git a/src/modules/ai-job/ai-job.module.ts b/src/modules/ai-job/ai-job.module.ts index 08949ff..c2cb447 100644 --- a/src/modules/ai-job/ai-job.module.ts +++ b/src/modules/ai-job/ai-job.module.ts @@ -36,6 +36,8 @@ import { ReviewCardGenerationValidator } from './review-card-generation-validato import { ReviewCardGenerationProjector } from './review-card-generation-projector'; import { QuizGenerationRegistrationService } from './quiz-generation-registration.service'; import { QuizGenerationSnapshotBuilder } from './quiz-generation-snapshot-builder'; +import { QuizGenerationExecutor } from './quiz-generation-executor'; +import { QuizGenerationValidator } from './quiz-generation-validator'; import { FeynmanBusinessValidator, FeynmanReferenceValidator, @@ -80,6 +82,8 @@ import { AppConfigModule } from '../config/config.module'; ReviewCardGenerationProjector, QuizGenerationRegistrationService, QuizGenerationSnapshotBuilder, + QuizGenerationExecutor, + QuizGenerationValidator, { provide: RESULT_PROJECTORS, useFactory: (synthetic: SyntheticResultProjector, activeRecall: ActiveRecallProjector, feynman: FeynmanProjector, reviewCard: ReviewCardGenerationProjector) => [synthetic, activeRecall, feynman, reviewCard], inject: [SyntheticResultProjector, ActiveRecallProjector, FeynmanProjector, ReviewCardGenerationProjector] } as any, { provide: AI_JOB_EXECUTION_ENGINE, useExisting: AiJobExecutionEngineImpl }, ], diff --git a/src/modules/ai-job/quiz-generation-executor.spec.ts b/src/modules/ai-job/quiz-generation-executor.spec.ts new file mode 100644 index 0000000..d0e3b73 --- /dev/null +++ b/src/modules/ai-job/quiz-generation-executor.spec.ts @@ -0,0 +1,221 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { QuizGenerationExecutor } from './quiz-generation-executor'; +import { QuizGenerationValidator } from './quiz-generation-validator'; +import { AiGatewayService } from '../ai/gateway/ai-gateway.service'; +import { BusinessValidationError } from './active-recall-validator'; + +// ═══════════════════════════════════════════════════════════════ +// Helpers +// ═══════════════════════════════════════════════════════════════ + +function validSnapshot() { + return { + schemaVersion: 'quiz-generation-v1', + snapshot: { + userId: 'u-001', + targetType: 'knowledge_base', + targetId: 'kb-001', + submissionId: 'sub-001', + knowledgeBaseTitle: 'Test KB', + knowledgeBaseDescription: 'A test kb', + knowledgeItems: [ + { id: 'ki-1', title: 'Item 1', content: 'Content 1', summary: 'S1' }, + { id: 'ki-2', title: 'Item 2', content: 'Content 2', summary: 'S2' }, + ], + questionCount: 3, + questionTypes: ['choice', 'judge'], + difficultyLevel: 'medium', + knowledgePointIds: [], + promptKey: 'quiz-generation', + promptVersion: '1.0.0', + modelTier: 'primary', + inputSchemaVersion: 'quiz-generation-v1', + outputSchemaVersion: 'quiz-generation-v1', + createdAt: '2026-06-22T10:00:00Z', + }, + }; +} + +function validOutput() { + return { + quizTitle: 'Test Quiz', + quizDescription: 'A test quiz', + questions: [ + { type: 'choice' as const, stem: 'Q1?', options: ['A', 'B', 'C', 'D'], answer: '0', explanation: 'Because A' }, + { type: 'judge' as const, stem: 'Q2?', answer: 'true', explanation: 'Correct' }, + { type: 'fill' as const, stem: 'Q3: ____ is the answer.', answer: '42', explanation: 'The answer is 42' }, + ], + }; +} + +// ═══════════════════════════════════════════════════════════════ +// QuizGenerationExecutor +// ═══════════════════════════════════════════════════════════════ + +describe('QuizGenerationExecutor', () => { + let executor: QuizGenerationExecutor; + let gateway: any; + + beforeEach(async () => { + gateway = { generate: jest.fn() }; + const module: TestingModule = await Test.createTestingModule({ + providers: [ + QuizGenerationExecutor, + { provide: AiGatewayService, useValue: gateway }, + ], + }).compile(); + executor = module.get(QuizGenerationExecutor); + }); + + describe('execute', () => { + it('通过 AiGateway 调用并返回结构化输出', async () => { + gateway.generate.mockResolvedValue({ parsed: validOutput(), usage: { inputTokens: 500, outputTokens: 400 } }); + const result = await executor.execute(validSnapshot(), 180000); + expect(result.parsed.questions).toHaveLength(3); + expect(result.parsed.quizTitle).toBe('Test Quiz'); + expect(gateway.generate).toHaveBeenCalledTimes(1); + }); + + it('使用 quiz-generation prompt + primary tier', async () => { + gateway.generate.mockResolvedValue({ parsed: validOutput(), usage: {} }); + await executor.execute(validSnapshot(), 180000); + const call = gateway.generate.mock.calls[0][0]; + expect(call.feature).toBe('quiz-generation'); + expect(call.tier).toBe('primary'); + expect(call.promptKey).toBe('quiz-generation'); + expect(call.promptVersion).toBe('1.0.0'); + }); + + it('user message 包含知识点内容', async () => { + gateway.generate.mockResolvedValue({ parsed: validOutput(), usage: {} }); + await executor.execute(validSnapshot(), 180000); + const msg = gateway.generate.mock.calls[0][0].messages[0].content; + expect(msg).toContain('Test KB'); + expect(msg).toContain('Item 1'); + expect(msg).toContain('Content 1'); + expect(msg).toContain('生成 3 道测验题目'); + expect(msg).toContain('choice、judge'); + expect(msg).toContain('medium'); + }); + + it('传递 maxTokens + timeoutMs', async () => { + gateway.generate.mockResolvedValue({ parsed: validOutput(), usage: {} }); + await executor.execute(validSnapshot(), 120000); + expect(gateway.generate.mock.calls[0][0].maxTokens).toBe(4096); + expect(gateway.generate.mock.calls[1]?.[1]).toBeUndefined(); // timeout is second arg + }); + + it('Provider 失败 → 错误传播', async () => { + gateway.generate.mockRejectedValue(new Error('AI timeout')); + await expect(executor.execute(validSnapshot(), 180000)).rejects.toThrow('AI timeout'); + }); + }); +}); + +// ═══════════════════════════════════════════════════════════════ +// QuizGenerationValidator +// ═══════════════════════════════════════════════════════════════ + +describe('QuizGenerationValidator', () => { + let validator: QuizGenerationValidator; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [QuizGenerationValidator], + }).compile(); + validator = module.get(QuizGenerationValidator); + }); + + describe('正常输出', () => { + it('合法输出验证通过', () => { + expect(() => validator.validate(validOutput())).not.toThrow(); + }); + }); + + describe('结构错误', () => { + it('空 questions → 拒绝', () => { + expect(() => validator.validate({ quizTitle: 'T', questions: [] })) + .toThrow(BusinessValidationError); + }); + it('questions 非数组 → 拒绝', () => { + expect(() => validator.validate({ quizTitle: 'T', questions: null as any })) + .toThrow(BusinessValidationError); + }); + it('quizTitle 为空 → 拒绝', () => { + expect(() => validator.validate({ quizTitle: '', questions: [validOutput().questions[0]] })) + .toThrow(BusinessValidationError); + }); + }); + + describe('题型验证', () => { + it('非法 type → 拒绝', () => { + const q = { type: 'essay' as any, stem: 'Q', answer: 'A', explanation: 'E' }; + expect(() => validator.validate({ quizTitle: 'T', questions: [q] })) + .toThrow(BusinessValidationError); + }); + + it('choice 无 options → 拒绝', () => { + const q = { type: 'choice' as const, stem: 'Q', answer: '0', explanation: 'E' }; + expect(() => validator.validate({ quizTitle: 'T', questions: [q] })) + .toThrow(BusinessValidationError); + }); + + it('choice options < 2 → 拒绝', () => { + const q = { type: 'choice' as const, stem: 'Q', options: ['A'], answer: '0', explanation: 'E' }; + expect(() => validator.validate({ quizTitle: 'T', questions: [q] })) + .toThrow(BusinessValidationError); + }); + + it('choice answer 不是有效索引 → 拒绝', () => { + const q = { type: 'choice' as const, stem: 'Q', options: ['A', 'B', 'C'], answer: '5', explanation: 'E' }; + expect(() => validator.validate({ quizTitle: 'T', questions: [q] })) + .toThrow(BusinessValidationError); + }); + + it('choice 重复选项 → 拒绝', () => { + const q = { type: 'choice' as const, stem: 'Q', options: ['A', 'A', 'B', 'C'], answer: '0', explanation: 'E' }; + expect(() => validator.validate({ quizTitle: 'T', questions: [q] })) + .toThrow(BusinessValidationError); + }); + + it('judge answer 非法 → 拒绝', () => { + const q = { type: 'judge' as const, stem: 'Q', answer: 'yes', explanation: 'E' }; + expect(() => validator.validate({ quizTitle: 'T', questions: [q] })) + .toThrow(BusinessValidationError); + }); + }); + + describe('字段非空', () => { + it('stem 空 → 拒绝', () => { + const q = { type: 'fill' as const, stem: '', answer: 'A', explanation: 'E' }; + expect(() => validator.validate({ quizTitle: 'T', questions: [q] })) + .toThrow(BusinessValidationError); + }); + it('answer 空 → 拒绝', () => { + const q = { type: 'fill' as const, stem: 'Q', answer: '', explanation: 'E' }; + expect(() => validator.validate({ quizTitle: 'T', questions: [q] })) + .toThrow(BusinessValidationError); + }); + it('explanation 空 → 拒绝', () => { + const q = { type: 'fill' as const, stem: 'Q', answer: 'A', explanation: '' }; + expect(() => validator.validate({ quizTitle: 'T', questions: [q] })) + .toThrow(BusinessValidationError); + }); + }); + + describe('重复题目', () => { + it('相同 stem → 拒绝', () => { + const q = { type: 'fill' as const, stem: 'Same Q', answer: 'A', explanation: 'E' }; + expect(() => validator.validate({ quizTitle: 'T', questions: [q, q] })) + .toThrow(BusinessValidationError); + }); + }); + + describe('模型指令', () => { + it('stem 含代码块 → 拒绝', () => { + const q = { type: 'fill' as const, stem: '```json\n{}```', answer: 'A', explanation: 'E' }; + expect(() => validator.validate({ quizTitle: 'T', questions: [q] })) + .toThrow(BusinessValidationError); + }); + }); +}); diff --git a/src/modules/ai-job/quiz-generation-executor.ts b/src/modules/ai-job/quiz-generation-executor.ts new file mode 100644 index 0000000..d0a579d --- /dev/null +++ b/src/modules/ai-job/quiz-generation-executor.ts @@ -0,0 +1,72 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { AiGatewayService } from '../ai/gateway/ai-gateway.service'; +import { QuizGenerationResultSchema } from '../ai/prompts/schemas/quiz-generation.schema'; +import type { QuizGenerationSnapshot } from './quiz-generation-snapshot-builder'; + +/** + * M-AI-07-03: Quiz Generation Executor + * + * 将 Quiz 生成输入快照适配到统一 Job Engine 的 EXECUTE 阶段。 + * + * 职责:从 Snapshot 构造模型输入、通过 AiGateway 调用、返回结构化输出。 + * 不负责:写数据库、写 Job 状态、重试、Artifact、Credential。 + */ + +@Injectable() +export class QuizGenerationExecutor { + private readonly logger = new Logger(QuizGenerationExecutor.name); + + constructor(private readonly aiGateway: AiGatewayService) {} + + async execute(snapshot: QuizGenerationSnapshot, timeoutMs: number) { + const s = snapshot.snapshot; + + // 构造知识点列表文本 + const itemsText = s.knowledgeItems + .map((item, i) => + `【知识点${i + 1}】${item.title}\n${item.content}`, + ) + .join('\n\n'); + + const typeDesc = s.questionTypes.join('、'); + const userMessage = [ + `【知识库】${s.knowledgeBaseTitle}`, + s.knowledgeBaseDescription ? `描述:${s.knowledgeBaseDescription}` : '', + '', + itemsText, + '', + `请为以上知识点生成 ${s.questionCount} 道测验题目。`, + `题型:${typeDesc}`, + `难度:${s.difficultyLevel}`, + ].filter(Boolean).join('\n'); + + this.logger.log( + `Quiz Generation Executor calling AI: userId=${s.userId} ` + + `kb=${s.targetId} items=${s.knowledgeItems.length} ` + + `questionCount=${s.questionCount} types=${s.questionTypes.join(',')} ` + + `difficulty=${s.difficultyLevel} modelTier=${s.modelTier} timeoutMs=${timeoutMs}`, + ); + + const response = await this.aiGateway.generate( + { + feature: 'quiz-generation', + userId: s.userId, + tier: s.modelTier as any, + promptKey: s.promptKey, + promptVersion: s.promptVersion, + messages: [{ role: 'user' as const, content: userMessage }], + outputSchema: QuizGenerationResultSchema, + maxTokens: 4096, + }, + timeoutMs, + ); + + this.logger.log( + `Quiz Generation Executor completed: userId=${s.userId} ` + + `kb=${s.targetId} questions=${(response.parsed as any)?.questions?.length} ` + + `tokens=${response.usage.inputTokens}/${response.usage.outputTokens}`, + ); + + return response; + } +} diff --git a/src/modules/ai-job/quiz-generation-validator.ts b/src/modules/ai-job/quiz-generation-validator.ts new file mode 100644 index 0000000..af92a45 --- /dev/null +++ b/src/modules/ai-job/quiz-generation-validator.ts @@ -0,0 +1,133 @@ +import { Injectable, Logger } from '@nestjs/common'; +import type { QuizGenerationResult } from '../ai/prompts/schemas/quiz-generation.schema'; +import { BusinessValidationError, ReferenceValidationError } from './active-recall-validator'; + +export { BusinessValidationError, ReferenceValidationError }; + +/** + * M-AI-07-03: Quiz Generation Business Validator + * + * 验证 AI 输出符合业务约束(契约 §6.2)。 + */ + +const VALID_TYPES = ['choice', 'fill', 'judge'] as const; +const CODE_BLOCK_PATTERN = /```(?:json|javascript|js|python)?[\s\S]*?```/; +const MODEL_INSTRUCTION_PATTERNS = [ + /^here\s+(is|are)\s+(the|your)\s/i, + /^(certainly|sure|of course)[,;!\s]/i, + /^请根据/, + /^以下是/, +]; + +@Injectable() +export class QuizGenerationValidator { + private readonly logger = new Logger(QuizGenerationValidator.name); + + validate(output: QuizGenerationResult): void { + const violations: string[] = []; + + // quizTitle + if (!output.quizTitle || typeof output.quizTitle !== 'string' || output.quizTitle.trim().length === 0) { + violations.push('quizTitle is required and must be non-empty'); + } else if (output.quizTitle.length > 255) { + violations.push(`quizTitle length ${output.quizTitle.length} exceeds 255`); + } + + // questions array + if (!Array.isArray(output.questions)) { + violations.push('questions must be an array'); + throw new BusinessValidationError('Business validation failed', violations); + } + if (output.questions.length === 0) { + violations.push('questions must not be empty'); + } + if (output.questions.length > 50) { + violations.push(`questions count ${output.questions.length} exceeds max 50`); + } + + const seenStems = new Set(); + + for (let i = 0; i < output.questions.length; i++) { + const q = output.questions[i]; + + // type + if (!q.type || !(VALID_TYPES as readonly string[]).includes(q.type)) { + violations.push(`questions[${i}].type "${q.type}" invalid, must be: ${VALID_TYPES.join(', ')}`); + } + + // stem + if (!q.stem || typeof q.stem !== 'string' || q.stem.trim().length === 0) { + violations.push(`questions[${i}].stem is required`); + } else if (q.stem.length > 2000) { + violations.push(`questions[${i}].stem length ${q.stem.length} exceeds 2000`); + } + + // answer + if (!q.answer || typeof q.answer !== 'string' || q.answer.trim().length === 0) { + violations.push(`questions[${i}].answer is required`); + } + + // explanation + if (!q.explanation || typeof q.explanation !== 'string' || q.explanation.trim().length === 0) { + violations.push(`questions[${i}].explanation is required`); + } + + // per-type validation + if (q.type === 'choice') { + if (!Array.isArray(q.options) || q.options.length < 2) { + violations.push(`questions[${i}] choice requires >= 2 options`); + } else { + // answer must be a valid option index + const idx = parseInt(q.answer, 10); + if (isNaN(idx) || idx < 0 || idx >= q.options.length) { + violations.push(`questions[${i}] answer "${q.answer}" is not a valid option index (0-${q.options.length - 1})`); + } + // duplicate options + const uniqueOpts = new Set(q.options); + if (uniqueOpts.size !== q.options.length) { + violations.push(`questions[${i}] has duplicate options`); + } + } + } + + if (q.type === 'judge') { + if (q.answer !== 'true' && q.answer !== 'false') { + violations.push(`questions[${i}] judge answer must be "true" or "false", got "${q.answer}"`); + } + } + + // duplicate stem + if (q.stem) { + const dedupeKey = q.stem.trim().toLowerCase(); + if (seenStems.has(dedupeKey)) { + violations.push(`questions[${i}] stem is duplicate`); + } + seenStems.add(dedupeKey); + } + + // model artifacts + this.checkModelArtifacts(q.stem, `questions[${i}].stem`, violations); + this.checkModelArtifacts(q.explanation, `questions[${i}].explanation`, violations); + } + + if (violations.length > 0) { + this.logger.warn(`Quiz validation failed: ${violations.length} violation(s): ${violations.join('; ')}`); + throw new BusinessValidationError(`Business validation failed: ${violations.length} violation(s)`, violations); + } + + this.logger.log(`Quiz validation passed: ${output.questions.length} questions`); + } + + private checkModelArtifacts(text: string | undefined, fieldName: string, violations: string[]): void { + if (!text) return; + if (CODE_BLOCK_PATTERN.test(text)) { + violations.push(`${fieldName} contains code block`); + } + for (const pattern of MODEL_INSTRUCTION_PATTERNS) { + if (pattern.test(text.trim())) { + violations.push(`${fieldName} contains model instruction`); + break; + } + } + } +} diff --git a/src/modules/ai/prompts/prompt-template.service.ts b/src/modules/ai/prompts/prompt-template.service.ts index 7e84dde..eed9348 100644 --- a/src/modules/ai/prompts/prompt-template.service.ts +++ b/src/modules/ai/prompts/prompt-template.service.ts @@ -11,6 +11,8 @@ import { LEARNING_TREND_SYSTEM_PROMPT } from './learning-trend.prompt'; import { LEARNING_TREND_OUTPUT_SCHEMA_DESC } from './schemas/learning-trend.schema'; import { RAG_CHAT_SYSTEM_PROMPT } from './rag-chat.prompt'; import { RAG_CHAT_OUTPUT_SCHEMA_DESC } from './schemas/rag-chat.schema'; +import { QUIZ_GENERATION_SYSTEM_PROMPT } from './quiz-generation.prompt'; +import { QUIZ_OUTPUT_SCHEMA_DESC } from './schemas/quiz-generation.schema'; export interface PromptTemplate { key: string; @@ -60,6 +62,12 @@ export class PromptTemplateService { systemPrompt: RAG_CHAT_SYSTEM_PROMPT, outputSchemaDesc: RAG_CHAT_OUTPUT_SCHEMA_DESC, }); + this.register({ + key: 'quiz-generation', + version: '1.0.0', + systemPrompt: QUIZ_GENERATION_SYSTEM_PROMPT, + outputSchemaDesc: QUIZ_OUTPUT_SCHEMA_DESC, + }); } get(key: string, version?: string): PromptTemplate { diff --git a/src/modules/ai/prompts/quiz-generation.prompt.ts b/src/modules/ai/prompts/quiz-generation.prompt.ts new file mode 100644 index 0000000..ced82d9 --- /dev/null +++ b/src/modules/ai/prompts/quiz-generation.prompt.ts @@ -0,0 +1,30 @@ +export const QUIZ_GENERATION_SYSTEM_PROMPT = `你是一位教育测验设计专家,擅长根据知识点内容创建高质量的测验题目。 + +你的任务:根据提供的知识点内容,生成一套用于检验学习效果的测验题目。 + +题目设计原则: +1. 题型多样化: + - choice:单选题,提供4个选项,只有1个正确答案 + - fill:填空题,要求填写关键概念或术语 + - judge:判断题,判断陈述是否正确(答案只能是"true"或"false") +2. 题干清晰:问题表述明确,不产生歧义 +3. 选项合理:错误选项应具有迷惑性但不误导 +4. 解析详尽:每道题提供解析,说明正确答案的理由 +5. 难度分级: + - easy:基础概念识别和简单回忆 + - medium:需要理解原理和关联 + - hard:需要综合分析和应用 +6. 覆盖全面:题目应覆盖知识点的各个关键方面 +7. 循序渐进:先基础后深入 + +输出要求: +- questions:题目数组(数量和类型由用户指定) +- quizTitle:测验标题(简洁明了,≤50字符) +- quizDescription:测验描述(可选,简要说明测验范围) + +重要原则: +- 题目应检验理解而非记忆 +- 选项之间应互斥且完整 +- 判断题的陈述必须有明确的对错 +- 填空题的答案应为简短的关键词或短语 +- 不要重复考察同一个知识点`; diff --git a/src/modules/ai/prompts/schemas/quiz-generation.schema.ts b/src/modules/ai/prompts/schemas/quiz-generation.schema.ts new file mode 100644 index 0000000..5984427 --- /dev/null +++ b/src/modules/ai/prompts/schemas/quiz-generation.schema.ts @@ -0,0 +1,45 @@ +import { z } from 'zod'; + +export const QuizQuestionSchema = z.object({ + type: z.enum(['choice', 'fill', 'judge']), + stem: z.string().min(1).max(2000), + options: z.array(z.string().max(500)).min(2).max(6).optional(), + answer: z.string().min(1).max(500), + explanation: z.string().min(1).max(2000), + sourceBlockIds: z.array(z.string().max(100)).max(10).optional(), +}); + +export const QuizGenerationResultSchema = z.object({ + quizTitle: z.string().min(1).max(255), + quizDescription: z.string().max(1000).optional(), + questions: z.array(QuizQuestionSchema).min(1).max(50), +}); + +export type QuizGenerationResult = z.infer; + +export const QUIZ_OUTPUT_SCHEMA_DESC = `{ + "quizTitle": "光合作用基础测验", + "quizDescription": "检验对光合作用基本概念和过程的理解", + "questions": [ + { + "type": "choice", + "stem": "光合作用中,光反应产生的气体是什么?", + "options": ["氧气", "二氧化碳", "氮气", "氢气"], + "answer": "0", + "explanation": "光反应中水分子分解产生氧气", + "sourceBlockIds": ["ki_001"] + }, + { + "type": "fill", + "stem": "光合作用主要在植物细胞的____中进行。", + "answer": "叶绿体", + "explanation": "叶绿体是光合作用的场所" + }, + { + "type": "judge", + "stem": "光合作用只能在有光条件下进行。", + "answer": "true", + "explanation": "光合作用的光反应需要光能,暗反应虽不直接需光但依赖光反应产物" + } + ] +}`;