智学文枢:课堂教学引擎

本文介绍‘课堂教学引擎’的设计与实现,聚焦将AI分析结果、学生VARK学习风格、教师语气策略及LaTeX公式规范,逐层翻译为可直接朗读的自然语言讲解词。引擎摒弃JSON解析,采用配置驱动的三层翻译机制,在System消息中硬注入LaTeX书写约束,确保输出可渲染、适配多认知通道,并通过姓名注入、双键兼容、摘要截取等工程设计提升鲁棒性与教学人格化水平。

课堂教学引擎:把分析结果翻译成"讲给人听"的话

一句话摘要:TeachingEngine 不走 JSON 解析路线,而是把 AI 分析结果、学生姓名、VARK 学习风格、讲解语气逐层翻译成自然语言讲解词,并在 System 消息中强制注入 LaTeX 书写规范,解决"模型会做题但不会讲题"的问题。

一、引言

分析引擎回答"学生错在哪",讲解引擎回答"老师该怎么讲"——这是两个完全不同的任务。

第一性原理:"会做题"和"会讲题"是两种能力。一个模型可以把一道函数题算得明明白白,但让它"像老师一样讲给学生听",它可能会:开头就来一句"显然可得"(学生一脸懵)、全程抽象术语轰炸(视觉型学生完全抓不住)、公式写得七零八落(前端 LaTeX 渲染直接崩)。

「智学文枢」的课堂教学引擎 services/ai/teaching_engine.py 是五个引擎里最"人格化"的一个——它的输入不仅是题目,还包括学生是谁(姓名)、学生怎么学(VARK 学习风格)、老师怎么讲(语气策略、举例偏好、私教备注),输出则是一段可以照着读的讲解词。它有三点与其他引擎的显著分野:

  1. 不解析 JSON:讲解词是给人看的,chat 直接返回文本,无需 extract_json;
  2. 配置即人设:语气、举例、学习风格三套策略表,把离散配置值翻译成连续的自然语言指令;
  3. 公式规范硬注入:System 消息里追加一段"公式书写规范",从源头约束模型输出可渲染的 LaTeX。

二、核心原理

2.1 三层翻译:从配置到讲解策略

_translate_preferences(config) 是讲解引擎的"人设翻译器":配置字典里的离散值(如 teaching_tone="encouraging")经三张策略表映射为具体的讲解指令:

encouraging

strict

humorous

daily_life

past_exam

tech_frontier

visual

auditory

kinesthetic

mixed

合并配置 dict

tone?

多肯定:这一步你想得很对…

严谨:注意、关键点在于…

幽默:轻松比喻俏皮话

example?

生活类比:函数图像=过山车轨道

真题视角:与某年高考题思路一致

科技前沿举例

learning_style?

视觉型:多用图像/图示/画图引导

听觉型:口头复述/像讲故事一样

动觉型:草稿纸上跟着写

混合型:图像+复述+演练结合

custom_notes 私教备注
teaching_custom 学科专属要求

拼接为讲解策略文本

注意 _TONE_STRATEGY / _EXAMPLE_STRATEGY / _LEARNING_STYLE_STRATEGY 三张表是模块级常量(写死在代码里),而 teaching_tone 等值来自第 30 篇合并的配置——配置决定选哪套策略,代码决定策略长什么样,两者解耦。

2.2 VARK 学习风格的注入

learning_style 字段来自学生分科配置(第 30 篇 L2 层把 teaching_learning_style 合并为 learning_style),取值范围 visual / auditory / kinesthetic / mixed 对应 VARK 模型。它解决了教学法的核心问题:同一个知识点,给不同认知通道的学生讲,方式必须不同——视觉型要"想象这个图形",动觉型要"在草稿纸上跟着写"。

2.3 LaTeX 规范硬注入

_LATEX_FORMATTING_RULES 是一段专门约束公式书写的规则文本,追加到 System 消息末尾(若已有则不重复追加)。它针对实战中反复出现的四类公式病:开标签损坏(\( 写成 \2)、括号错乱((2-x) 漏右括号)、公式内换行、公式内混中文标点。把渲染层的约束前置到生成层,比前端容错更治本。

三、代码实现

3.1 主流程:组装与调用

# 源码位置:services/ai/teaching_engine.py L45-L91
def generate(self, question, analysis, grade: str = "",
             student_id: str = None, config: dict = None) -> str:
    """生成讲解词

    Args:
        question: Question ORM对象
        analysis: AIAnalysis 对象(用于提取分析摘要)
        grade: 学生年级
        student_id: 学生ID(用于取姓名与偏好)
        config: 已合并的Prompt配置(含 tone/example_preference/custom_notes/custom_prompt)
    """
    config = config or {}
    student_name = self._get_student_name(student_id)

    strategy = self._translate_preferences(config)
    summary = self._extract_summary(analysis)

    # 讲解配置来源:config(全局→学生·学科 合并) 优先,空则回退代码常量
    user_template = config.get("teaching_user") or TEACHING_USER_DEFAULT
    user_prompt = self._safe_format(
        user_template,
        grade=grade or "未知",
        subject=question.subject or "",
        question_text=question.question_text or "",
        correct_answer=question.correct_answer or "",
        student_preferences=strategy,
        analysis_summary=summary,
    )
    if student_name:
        user_prompt = f"【学生姓名】{student_name}\n" + user_prompt

    system_prompt = config.get("teaching_system") or TEACHING_SYSTEM_DEFAULT
    if _LATEX_FORMATTING_RULES not in system_prompt:
        system_prompt = (system_prompt.rstrip() + "\n\n" + _LATEX_FORMATTING_RULES).strip()

    messages = [
        {"role": "system", "content": system_prompt},
        {"role": "user", "content": user_prompt},
    ]
    # 调用AI前,记录完整提示词供核对组成
    log_prompts("AI讲解", messages, temperature=TEACHING_TEMPERATURE)
    return self.client.chat(
        messages, temperature=TEACHING_TEMPERATURE
    ).strip()

五个细节:_get_student_name 查库拿姓名(讲解词里"小明,这一步你想得很对"比冷冰冰的"你"更有温度);strategy 是偏好翻译结果、summary 是分析摘要(两者都插进 user 模板的占位符);if student_name: 把姓名拼在 user 消息最前面;_LATEX_FORMATTING_RULES not in system_prompt 防重复注入;最后 .strip() 裁掉模型爱加的前后空行。

3.2 策略翻译器与摘要提取

# 源码位置:services/ai/teaching_engine.py L112-L157
def _translate_preferences(self, config: dict) -> str:
    """把偏好翻译为可执行的讲解策略"""
    lines = []
    tone = config.get("teaching_tone") or config.get("tone", "")
    if tone in _TONE_STRATEGY:
        lines.append(f"【讲解语气】{_TONE_STRATEGY[tone]}")

    example = config.get("teaching_example") or config.get("example_preference", "")
    if example in _EXAMPLE_STRATEGY:
        lines.append(f"【举例方式】{_EXAMPLE_STRATEGY[example]}")

    style = config.get("learning_style", "")
    if style in _LEARNING_STYLE_STRATEGY:
        lines.append(f"【学习风格】{_LEARNING_STYLE_STRATEGY[style]}")

    notes = (config.get("custom_notes") or "").strip()
    if notes:
        lines.append(f"【私教备注】{notes}")

    teaching_custom = (config.get("teaching_custom") or "").strip()
    if teaching_custom:
        lines.append(f"【学生·学科专属要求】{teaching_custom}")

    return "\n".join(lines) if lines else "按标准方式讲解即可。"

config.get("teaching_tone") or config.get("tone", "") 是"新键优先、旧键兜底"的双向兼容:新版本配置走 teaching_tone,老数据只有 tone,两者都能生效。if tone in _TONE_STRATEGY 用 in 而非直接取值——配置里若是未知值(如用户手改配置),直接跳过该策略而不是报错。

3.3 摘要提取器

# 源码位置:services/ai/teaching_engine.py L137-L157
def _extract_summary(self, analysis) -> str:
    """从分析结果提取对讲解有用的摘要"""
    parts = []
    core = getattr(analysis, "core_concept", None)
    if core:
        parts.append(f"核心概念/公式:{core}")

    gap = getattr(analysis, "knowledge_gap", None)
    if gap:
        parts.append(f"学生可能的知识断层:{gap}")

    steps = getattr(analysis, "step_by_step", None) or []
    if isinstance(steps, list) and steps:
        preview = " → ".join(str(s) for s in steps[:3])
        parts.append(f"解题关键步骤:{preview}")

    remediation = getattr(analysis, "remediation_suggestion", None)
    if remediation:
        parts.append(f"补漏建议:{remediation}")

    return "\n".join(parts) if parts else "(无分析摘要)"

getattr(analysis, ..., None) 而非直接 analysis.core_concept——analysis 可能为 None(没分析过就点讲解),getattr 保证空对象也不崩;steps[:3] 只摘前 3 步,用 → 连接成一行预览,避免把整段分析原文塞给模型(控制 token、聚焦重点)。

四、设计权衡 / 踩坑记录

1. 为什么讲解引擎不解析 JSON? 分析/变式输出的是"数据"(要落库、要结构化展示),讲解输出的是"话语"(要朗读、要感受语气)。若强行让讲解也输出 JSON,模型会把注意力分给格式而牺牲自然度——实测讲解词质量明显下降。面向人的输出用 chat,面向机器的输出用 chat_json,这是引擎选型的根本依据。

2. _LATEX_FORMATTING_RULES 为什么注入 System 而不是 User? System 消息决定"模型如何表现",公式规范是跨题目、跨学生的全局约束,放 System 最合适;User 消息承载具体题目内容,若把规则放 User,每次组装都要重复拼接、且用户自定义 user 模板时会弄丢规则。not in system_prompt 的判断防止用户自己写了类似规则时重复注入。

3. teaching_tone 与 tone 的双键兼容是过渡期产物。 配置字段从 tone 演进到 teaching_tone(为了区分"分析语气"与"讲解语气"),存量数据库里还有旧键。用 or 兼容两代,等数据迁移完成后再删旧键——兼容代码可以丑,但不能让用户数据失效。

4. 姓名注入的时机。 把 【学生姓名】xxx 拼在 user 消息最前面而不是放进模板,是为了兼容用户自定义的 teaching_user 模板——自定义模板里没有 {student_name} 占位符,若依赖模板注入会直接丢失;前置拼接保证"无论模板长什么样,姓名都在场"。

五、小结

机制实现解决的问题
策略翻译三张策略表 + if in 判断配置值 → 自然语言指令
VARK 注入learning_style → 学习风格段落按认知通道定制讲解方式
公式约束System 注入 _LATEX_FORMATTING_RULES模型输出可渲染的 LaTeX
摘要提取getattr + steps[:3] 拼预览把分析结果压缩成讲解上下文
输出通道chat 而非 chat_json自然语言讲解词,不解析

TeachingEngine 展示了 AI 服务层的另一种形态:没有流水线式的"组装-调用-解析-落库",只有"翻译人设 → 拼装上下文 → 生成话语"。它证明了统一架构的弹性——五条引擎共享客户端与配置底座,但各自按业务形态生长出完全不同的内部结构。最后一篇的追问引擎(第 36 篇)将把"上下文拼装"推向极致:让模型带着整段对话历史回答学生的追问。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

在世修行

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值