feat(M-AI-08-03): learning analysis prompt + schema + executor + engine dispatch
Some checks failed
Deploy API Server / build-and-unit (push) Failing after 27s
Deploy API Server / current-integration (push) Has been skipped
Deploy API Server / backward-compat (push) Has been skipped
Deploy API Server / deploy (push) Has been skipped

Resolves A1/A2: learning-analysis prompt and Zod output schema created.
- Prompt: 6 analysis dimensions, evidence-required, no medical diagnosis
- Schema: strengths/weaknesses/trends/risks/recommendations with evidenceRefs
- Executor: AiGateway only, constructs user message from snapshot metrics
- Engine dispatch for learning_analysis job type

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
wangdl 2026-06-22 21:41:25 +08:00
parent d537ff3bd0
commit 1bb131a1bc
7 changed files with 235 additions and 3 deletions

View File

@ -14,6 +14,7 @@ import { ReviewCardGenerationExecutor } from './review-card-generation-executor'
import { ReviewCardGenerationValidator } from './review-card-generation-validator'; import { ReviewCardGenerationValidator } from './review-card-generation-validator';
import { QuizGenerationExecutor } from './quiz-generation-executor'; import { QuizGenerationExecutor } from './quiz-generation-executor';
import { QuizGenerationValidator } from './quiz-generation-validator'; import { QuizGenerationValidator } from './quiz-generation-validator';
import { LearningAnalysisExecutor } from './learning-analysis-executor';
import { PrismaService } from '../../infrastructure/database/prisma.service'; import { PrismaService } from '../../infrastructure/database/prisma.service';
import { JobLockConflictError, JobAlreadyTerminalError } from './ai-job.errors'; import { JobLockConflictError, JobAlreadyTerminalError } from './ai-job.errors';
@ -96,6 +97,7 @@ describe('AiJobExecutionEngineImpl', () => {
{ provide: ReviewCardGenerationValidator, useValue: { validate: jest.fn() } }, { provide: ReviewCardGenerationValidator, useValue: { validate: jest.fn() } },
{ provide: QuizGenerationExecutor, useValue: { execute: jest.fn() } }, { provide: QuizGenerationExecutor, useValue: { execute: jest.fn() } },
{ provide: QuizGenerationValidator, useValue: { validate: jest.fn() } }, { provide: QuizGenerationValidator, useValue: { validate: jest.fn() } },
{ provide: LearningAnalysisExecutor, useValue: { execute: jest.fn() } },
{ provide: ActiveRecallObservabilityService, useValue: { { provide: ActiveRecallObservabilityService, useValue: {
incrementUnifiedExecuteSuccess: jest.fn(), incrementUnifiedExecuteSuccess: jest.fn(),
incrementUnifiedExecuteFailed: jest.fn(), incrementUnifiedExecuteFailed: jest.fn(),

View File

@ -20,8 +20,11 @@ import type { QuizGenerationSnapshot } from './quiz-generation-snapshot-builder'
import type { FeynmanEvaluationResult } from '../ai/prompts/schemas/feynman-evaluation.schema'; import type { FeynmanEvaluationResult } from '../ai/prompts/schemas/feynman-evaluation.schema';
import type { ReviewCardGenerationResult } from '../ai/prompts/schemas/review-card-generation.schema'; import type { ReviewCardGenerationResult } from '../ai/prompts/schemas/review-card-generation.schema';
import type { QuizGenerationResult } from '../ai/prompts/schemas/quiz-generation.schema'; import type { QuizGenerationResult } from '../ai/prompts/schemas/quiz-generation.schema';
import type { LearningAnalysisResult } from '../ai/prompts/schemas/learning-analysis.schema';
import { QuizGenerationExecutor } from './quiz-generation-executor'; import { QuizGenerationExecutor } from './quiz-generation-executor';
import { QuizGenerationValidator } from './quiz-generation-validator'; import { QuizGenerationValidator } from './quiz-generation-validator';
import { LearningAnalysisExecutor } from './learning-analysis-executor';
import { LearningAnalysisExecutor } from './learning-analysis-executor';
import { import {
AiJobExecutionEngine, AiJobExecutionEngine,
EngineJobContext, EngineJobContext,
@ -100,6 +103,7 @@ export class AiJobExecutionEngineImpl implements AiJobExecutionEngine {
private readonly reviewCardValidator: ReviewCardGenerationValidator, private readonly reviewCardValidator: ReviewCardGenerationValidator,
private readonly quizExecutor: QuizGenerationExecutor, private readonly quizExecutor: QuizGenerationExecutor,
private readonly quizValidator: QuizGenerationValidator, private readonly quizValidator: QuizGenerationValidator,
private readonly learningAnalysisExecutor: LearningAnalysisExecutor,
private readonly observability: ActiveRecallObservabilityService, private readonly observability: ActiveRecallObservabilityService,
private readonly feynmanObs: FeynmanObservabilityService, private readonly feynmanObs: FeynmanObservabilityService,
) {} ) {}
@ -263,11 +267,17 @@ export class AiJobExecutionEngineImpl implements AiJobExecutionEngine {
try { try {
this.quizValidator.validate(parsedOutput as QuizGenerationResult); this.quizValidator.validate(parsedOutput as QuizGenerationResult);
} catch (validationErr: any) { } catch (validationErr: any) {
this.logger.warn( this.logger.warn(`Quiz validation failed for job=${aiJobId}: ${validationErr.message}`);
`Quiz validation failed for job=${aiJobId}: ${validationErr.message}`,
);
throw validationErr; throw validationErr;
} }
} else if (job.jobType === 'learning_analysis' && snapshot) {
// M-AI-08-03: Learning Analysis Executor
response = await this.learningAnalysisExecutor.execute(snapshot as any, timeoutMs);
parsedOutput = response.parsed;
this.logger.log(
`Learning Analysis Executor completed: job=${aiJobId} ` +
`confidence=${(parsedOutput as any)?.confidence}`,
);
} else { } else {
// 默认路径:直接调用 AiGatewaysynthetic_job 等) // 默认路径:直接调用 AiGatewaysynthetic_job 等)
response = await this.aiGateway.generate( response = await this.aiGateway.generate(

View File

@ -41,6 +41,7 @@ import { QuizGenerationValidator } from './quiz-generation-validator';
import { QuizGenerationProjector } from './quiz-generation-projector'; import { QuizGenerationProjector } from './quiz-generation-projector';
import { LearningAnalysisRegistrationService } from './learning-analysis-registration.service'; import { LearningAnalysisRegistrationService } from './learning-analysis-registration.service';
import { LearningAnalysisSnapshotBuilder } from './learning-analysis-snapshot-builder'; import { LearningAnalysisSnapshotBuilder } from './learning-analysis-snapshot-builder';
import { LearningAnalysisExecutor } from './learning-analysis-executor';
import { import {
FeynmanBusinessValidator, FeynmanBusinessValidator,
FeynmanReferenceValidator, FeynmanReferenceValidator,
@ -90,6 +91,7 @@ import { AppConfigModule } from '../config/config.module';
QuizGenerationProjector, QuizGenerationProjector,
LearningAnalysisRegistrationService, LearningAnalysisRegistrationService,
LearningAnalysisSnapshotBuilder, LearningAnalysisSnapshotBuilder,
LearningAnalysisExecutor,
{ provide: RESULT_PROJECTORS, useFactory: (synthetic: SyntheticResultProjector, activeRecall: ActiveRecallProjector, feynman: FeynmanProjector, reviewCard: ReviewCardGenerationProjector, quiz: QuizGenerationProjector) => [synthetic, activeRecall, feynman, reviewCard, quiz], inject: [SyntheticResultProjector, ActiveRecallProjector, FeynmanProjector, ReviewCardGenerationProjector, QuizGenerationProjector] } as any, { provide: RESULT_PROJECTORS, useFactory: (synthetic: SyntheticResultProjector, activeRecall: ActiveRecallProjector, feynman: FeynmanProjector, reviewCard: ReviewCardGenerationProjector, quiz: QuizGenerationProjector) => [synthetic, activeRecall, feynman, reviewCard, quiz], inject: [SyntheticResultProjector, ActiveRecallProjector, FeynmanProjector, ReviewCardGenerationProjector, QuizGenerationProjector] } as any,
{ provide: AI_JOB_EXECUTION_ENGINE, useExisting: AiJobExecutionEngineImpl }, { provide: AI_JOB_EXECUTION_ENGINE, useExisting: AiJobExecutionEngineImpl },
], ],

View File

@ -0,0 +1,60 @@
import { Injectable, Logger } from '@nestjs/common';
import { AiGatewayService } from '../ai/gateway/ai-gateway.service';
import { LearningAnalysisResultSchema } from '../ai/prompts/schemas/learning-analysis.schema';
import type { LearningAnalysisSnapshot } from './learning-analysis-snapshot-builder';
@Injectable()
export class LearningAnalysisExecutor {
private readonly logger = new Logger(LearningAnalysisExecutor.name);
constructor(private readonly aiGateway: AiGatewayService) {}
async execute(snapshot: LearningAnalysisSnapshot, timeoutMs: number) {
const s = snapshot.snapshot;
const userMessage = [
`【学习数据摘要】`,
`学习时长:${s.studyMetrics.totalStudyDuration}秒,活跃${s.studyMetrics.activeDays}`,
`复习完成:${s.reviewMetrics.reviewCompletedCount}次,准确率:${Math.round(s.reviewMetrics.reviewAccuracy * 100)}%`,
`测验完成:${s.quizMetrics.attemptCount}次,准确率:${Math.round(s.quizMetrics.accuracy * 100)}%`,
`主动回忆:${s.activeRecallMetrics.count}次,平均分:${s.activeRecallMetrics.avgScore}`,
`费曼练习:${s.feynmanMetrics.count}次,薄弱点:${s.feynmanMetrics.weaknessCount}`,
`知识点进度:${s.knowledgeProgress.totalItems}个知识点`,
s.knowledgeProgress.weakItems.length > 0
? `薄弱知识点:${s.knowledgeProgress.weakItems.map(i => i.title).join('、')}`
: '',
'',
`数据质量:${s.dataQuality.availableSources.join(', ')} 可用`,
s.dataQuality.insufficientDataReasons.length > 0
? `注意:${s.dataQuality.insufficientDataReasons.join('')}`
: '',
].filter(Boolean).join('\n');
this.logger.log(
`Learning Analysis Executor calling AI: userId=${s.userId} ` +
`modelTier=${s.modelTier} timeoutMs=${timeoutMs}`,
);
const response = await this.aiGateway.generate(
{
feature: 'learning-analysis',
userId: s.userId,
tier: s.modelTier as any,
promptKey: s.promptKey,
promptVersion: s.promptVersion,
messages: [{ role: 'user' as const, content: userMessage }],
outputSchema: LearningAnalysisResultSchema,
maxTokens: 4096,
},
timeoutMs,
);
this.logger.log(
`Learning Analysis Executor completed: userId=${s.userId} ` +
`strengths=${(response.parsed as any)?.strengths?.length} ` +
`recommendations=${(response.parsed as any)?.recommendations?.length} ` +
`confidence=${(response.parsed as any)?.confidence}`,
);
return response;
}
}

View File

@ -0,0 +1,90 @@
export const LEARNING_ANALYSIS_SYSTEM_PROMPT = `你是一位学习分析专家,擅长根据学习数据提供客观、可执行的分析和建议。
##
-
- studyMetrics
- reviewMetrics
- quizMetrics
- activeRecallMetrics
- feynmanMetrics
- knowledgeProgress
- dataQuality
##
1. ****
2. ****
3. ****
4. ****
5. ****metricKey
##
- summary200
- strengths0-3 title/description/evidenceRefs
- weaknesses0-5 title/description/knowledgePointId/evidenceRefs
- trends0-3 metricKey/direction(improving/declining/stable)/description/evidenceRefs
- risks0-3 title/severity(low/medium/high)/description/evidenceRefs
- recommendations1-5 actionType/title/reason/priority(0-10)/estimatedMinutes/targetType/targetId/evidenceRefs
- confidence0.0-1.0
- insufficientData
- dataQualityNotes
##
- ADHD
- "你是一个懒惰的人"
-
- 访
-
- Markdown代码块或JSON格式包装
- `;
export const LEARNING_ANALYSIS_OUTPUT_DESC = `{
"summary": "过去7天你累计学习3.5小时,复习完成率较高但主动回忆练习偏少...",
"strengths": [
{
"title": "复习完成率高",
"description": "你的复习完成率达到85%,说明间隔重复策略执行良好",
"evidenceRefs": [{"sourceType":"review_metric","metricKey":"reviewAccuracy","windowStart":"...","windowEnd":"..."}]
}
],
"weaknesses": [
{
"title": "主动回忆练习不足",
"description": "近30天仅有2次主动回忆建议增加练习频率",
"knowledgePointId": null,
"evidenceRefs": [{"sourceType":"active_recall","metricKey":"count","windowStart":"...","windowEnd":"..."}]
}
],
"trends": [
{
"metricKey": "totalStudyDuration",
"direction": "declining",
"description": "学习时长呈下降趋势,建议设定每日最低学习目标",
"evidenceRefs": [{"sourceType":"study_metric","metricKey":"totalStudyDuration"}]
}
],
"risks": [
{
"title": "知识点遗忘风险",
"severity": "medium",
"description": "有3个知识点的复习已过期存在遗忘风险",
"evidenceRefs": [{"sourceType":"review_metric","metricKey":"overdueCount"}]
}
],
"recommendations": [
{
"actionType": "active_recall",
"title": "增加主动回忆练习",
"reason": "主动回忆是最有效的学习策略之一",
"priority": 5,
"estimatedMinutes": 15,
"targetType": null,
"targetId": null,
"evidenceRefs": [{"sourceType":"active_recall","metricKey":"count"}]
}
],
"confidence": 0.75,
"insufficientData": false,
"dataQualityNotes": "学习行为和复习数据充足,主动回忆数据偏少"
}`;

View File

@ -13,6 +13,7 @@ import { RAG_CHAT_SYSTEM_PROMPT } from './rag-chat.prompt';
import { RAG_CHAT_OUTPUT_SCHEMA_DESC } from './schemas/rag-chat.schema'; import { RAG_CHAT_OUTPUT_SCHEMA_DESC } from './schemas/rag-chat.schema';
import { QUIZ_GENERATION_SYSTEM_PROMPT } from './quiz-generation.prompt'; import { QUIZ_GENERATION_SYSTEM_PROMPT } from './quiz-generation.prompt';
import { QUIZ_OUTPUT_SCHEMA_DESC } from './schemas/quiz-generation.schema'; import { QUIZ_OUTPUT_SCHEMA_DESC } from './schemas/quiz-generation.schema';
import { LEARNING_ANALYSIS_SYSTEM_PROMPT, LEARNING_ANALYSIS_OUTPUT_DESC } from './learning-analysis.prompt';
export interface PromptTemplate { export interface PromptTemplate {
key: string; key: string;
@ -68,6 +69,12 @@ export class PromptTemplateService {
systemPrompt: QUIZ_GENERATION_SYSTEM_PROMPT, systemPrompt: QUIZ_GENERATION_SYSTEM_PROMPT,
outputSchemaDesc: QUIZ_OUTPUT_SCHEMA_DESC, outputSchemaDesc: QUIZ_OUTPUT_SCHEMA_DESC,
}); });
this.register({
key: 'learning-analysis',
version: '1.0.0',
systemPrompt: LEARNING_ANALYSIS_SYSTEM_PROMPT,
outputSchemaDesc: LEARNING_ANALYSIS_OUTPUT_DESC,
});
} }
get(key: string, version?: string): PromptTemplate { get(key: string, version?: string): PromptTemplate {

View File

@ -0,0 +1,61 @@
import { z } from 'zod';
const EvidenceRefSchema = z.object({
sourceType: z.enum(['study_metric', 'review_metric', 'quiz_metric', 'active_recall', 'feynman', 'knowledge_progress']),
metricKey: z.string().min(1).max(100),
entityId: z.string().max(255).optional(),
windowStart: z.string().optional(),
windowEnd: z.string().optional(),
});
const StrengthSchema = z.object({
title: z.string().min(1).max(255),
description: z.string().min(1).max(1000),
evidenceRefs: z.array(EvidenceRefSchema).min(1).max(5),
});
const WeaknessSchema = z.object({
title: z.string().min(1).max(255),
description: z.string().min(1).max(1000),
knowledgePointId: z.string().max(255).optional().nullable(),
evidenceRefs: z.array(EvidenceRefSchema).min(1).max(5),
});
const TrendSchema = z.object({
metricKey: z.string().min(1).max(100),
direction: z.enum(['improving', 'declining', 'stable']),
description: z.string().min(1).max(500),
evidenceRefs: z.array(EvidenceRefSchema).min(1).max(3),
});
const RiskSchema = z.object({
title: z.string().min(1).max(255),
severity: z.enum(['low', 'medium', 'high']),
description: z.string().min(1).max(1000),
evidenceRefs: z.array(EvidenceRefSchema).min(1).max(3),
});
const RecommendationSchema = z.object({
actionType: z.string().min(1).max(100),
title: z.string().min(1).max(255),
reason: z.string().min(1).max(1000),
priority: z.number().int().min(0).max(10),
estimatedMinutes: z.number().int().min(1).max(480).optional(),
targetType: z.string().max(32).optional().nullable(),
targetId: z.string().max(255).optional().nullable(),
evidenceRefs: z.array(EvidenceRefSchema).min(1).max(3),
});
export const LearningAnalysisResultSchema = z.object({
summary: z.string().min(1).max(500),
strengths: z.array(StrengthSchema).min(0).max(3),
weaknesses: z.array(WeaknessSchema).min(0).max(5),
trends: z.array(TrendSchema).min(0).max(3),
risks: z.array(RiskSchema).min(0).max(3),
recommendations: z.array(RecommendationSchema).min(1).max(5),
confidence: z.number().min(0).max(1),
insufficientData: z.boolean(),
dataQualityNotes: z.string().max(500).optional(),
});
export type LearningAnalysisResult = z.infer<typeof LearningAnalysisResultSchema>;