/** * V5-17 A1/A2:AI 反馈闭环 + 解释性展示。 * * 调用 AI 对课案文档进行教学评一致性反馈,返回结构化建议。 * 反馈包含: * - strengths:课案优点 * - improvements:改进建议 * - alignment:教学评一致性评估 * - differentiation:差异化教学建议 * * 每条建议附带 reason(解释性展示),帮助教师理解 AI 判断依据。 */ import "server-only"; import { env } from "@/env.mjs"; import { createAiChatCompletion } from "@/shared/lib/ai"; import { isRecord } from "@/shared/lib/type-guards"; import { z } from "zod"; import type { LessonPlanDocument, LessonPlanNode } from "../types"; /** AI 反馈单条建议 */ export interface AiFeedbackItem { /** i18n 键后缀(feedback.* 命名空间下) */ category: "strengths" | "improvements" | "alignment" | "differentiation"; /** 建议标题 */ title: string; /** 解释性理由(A2:解释性展示) */ reason: string; /** 关联节点 ID(如适用) */ nodeId?: string; } /** AI 反馈结果 */ export interface AiFeedbackResult { items: AiFeedbackItem[]; /** 整体评分(0-100) */ overallScore: number; /** 摘要 */ summary: string; } const FeedbackItemSchema = z.object({ category: z.enum(["strengths", "improvements", "alignment", "differentiation"]), title: z.string().min(1), reason: z.string(), nodeId: z.string().optional(), }); const FeedbackResultSchema = z.object({ items: z.array(FeedbackItemSchema), overallScore: z.number().min(0).max(100), summary: z.string(), }); const AI_FEEDBACK_PROMPT_TEMPLATE = `你是资深教学设计专家。请对以下课案文档进行教学评一致性评估,给出结构化反馈。 课案文档(JSON): --- {doc} --- 请从四个维度评估: 1. strengths:课案优点 2. improvements:改进建议 3. alignment:教学评一致性(目标-教学-评价是否对齐) 4. differentiation:差异化教学建议 返回 JSON 对象,含: - items:数组,每项含 category(维度)/title(建议标题)/reason(解释性理由,说明为何给出此建议)/nodeId(关联节点 ID,可选) - overallScore:整体评分 0-100 - summary:一句话摘要 注意:reason 字段必须解释判断依据,帮助教师理解。`; /** 安全提取节点文本用于 AI prompt */ function extractNodeText(node: LessonPlanNode): string { const data = node.data as unknown; if (!isRecord(data)) return ""; const html = typeof data.html === "string" ? data.html : ""; const sourceText = typeof data.sourceText === "string" ? data.sourceText : ""; return html || sourceText || ""; } /** * 调用 AI 对课案文档生成结构化反馈。 * * @param doc 课案文档 * @returns AI 反馈结果;AI 不可用时返回空结果 */ export async function generateLessonPlanFeedback( doc: LessonPlanDocument, ): Promise { // 提取教学节点摘要(排除正文节点,控制 token 用量) const teachingNodes = doc.nodes.filter( (n): n is LessonPlanNode => n.type !== "textbook_content", ); if (teachingNodes.length === 0) { return { items: [], overallScore: 0, summary: "" }; } const docSummary = teachingNodes.slice(0, 20).map((n) => ({ id: n.id, type: n.type, title: n.title, stage: n.stage, differentiation: n.differentiation, text: extractNodeText(n).slice(0, 200), })); const prompt = AI_FEEDBACK_PROMPT_TEMPLATE.replace( "{doc}", JSON.stringify(docSummary), ); try { const { content } = await createAiChatCompletion({ messages: [{ role: "user", content: prompt }], model: env.AI_MODEL ?? "gpt-4o-mini", temperature: 0.4, }); // 从返回内容中提取 JSON 对象 const jsonMatch = content.match(/\{[\s\S]*\}/); if (!jsonMatch) return { items: [], overallScore: 0, summary: "" }; const parsed: unknown = JSON.parse(jsonMatch[0]); const validated = FeedbackResultSchema.safeParse(parsed); if (!validated.success) { return { items: [], overallScore: 0, summary: "" }; } return validated.data; } catch { return { items: [], overallScore: 0, summary: "" }; } }