feat(M-AI-07-03): quiz generation Executor + Validator + prompt/schema
Some checks failed
Deploy API Server / build-and-unit (push) Successful in 37s
Deploy API Server / backward-compat (push) Has been cancelled
Deploy API Server / deploy (push) Has been cancelled
Deploy API Server / current-integration (push) Has been cancelled

Resolves A1/A2: bring quiz prompt + output schema in-house from Runtime.
- quiz-generation.prompt.ts: system prompt with question design principles
- quiz-generation.schema.ts: Zod schema (choice/fill/judge types)
- Registered in PromptTemplateService with key 'quiz-generation' v1.0.0
- Executor: AiGateway call with knowledge items + generation params
- Validator: per-type validation (choice options/index, judge true/false)
- Engine dispatch for quiz_generation job type

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
wangdl 2026-06-22 20:40:12 +08:00
parent 9cac4781bd
commit d368f1a97f
9 changed files with 543 additions and 0 deletions

View File

@ -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(),

View File

@ -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 {
// 默认路径:直接调用 AiGatewaysynthetic_job 等)
response = await this.aiGateway.generate(

View File

@ -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 },
],

View File

@ -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);
});
});
});

View File

@ -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 ArtifactCredential
*/
@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;
}
}

View File

@ -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<string>();
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;
}
}
}
}

View File

@ -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 {

View File

@ -0,0 +1,30 @@
export const QUIZ_GENERATION_SYSTEM_PROMPT = `你是一位教育测验设计专家,擅长根据知识点内容创建高质量的测验题目。
1.
- choice41
- fill
- judge"true""false"
2.
3.
4.
5.
- easy
- medium
- hard
6.
7.
- questions
- quizTitle50
- quizDescription
-
-
-
-
- `;

View File

@ -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<typeof QuizGenerationResultSchema>;
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": "光合作用的光反应需要光能,暗反应虽不直接需光但依赖光反应产物"
}
]
}`;