刷题软件用多了,你会发现一个尴尬事实:同一道错题,今天推、明天推、后天还推,结果学生把答案背了下来,知识点却还是没学会。传统题库的“根据错题推荐”本质上是拿静态规则代替诊断,它根本不理解你。而真正的自适应学习引擎,必须回答三个问题:你现在真正会什么、你会在什么时候忘掉、你下一题最该做什么。Project Akari 就是我围绕“数学与CS”这两个学科从零开始搭建的现代自适应学习引擎,目标是让每一道推荐题目都有据可循,让每次学习路径都不再是“题海”的堆叠,而是一套可解释、可迭代、可验证的教学决策循环。这篇文章不准备讲空泛的理念,只讲我在设计Akari时的核心算法选型、工程架构、内容工程以及在真实试点中踩过的坑,适合正在做教育产品、自适应学习系统,或者说想改造传统题库算法的同行参考。
1. 为什么“数学+CS”的自适应学习值得单独做一套引擎
1.1 自适应学习引擎解决的是什么问题
先把我理解的自适应学习引擎边界说清楚。它不是一个智能组卷工具,也不是一个错题本,而是一个学习状态的实时估计器加上学习策略的决策器。它会不断根据你的每次点击、每次提交、每次犹豫,更新对你知识掌握程度的判断,再根据这个判断,从题库里选择一条最适合当前学习的路径。传统做法里,用户答对一题,系统就给这个知识点加上一分;答错就扣一分,或者干脆把题目里错误的知识点标记成“未掌握”。这样的逻辑在数据量小的时候勉强能用,但一旦题目变多、知识点变细,它的误差会被迅速放大:学生今天状态不好,一道题错三次,系统就认定这个知识点完蛋了;或者学生靠排除法蒙对了一道选择题,系统就认为他已经会了。
自适应学习引擎的真正价值,是把这种“二元判定”升级成“概率估计”。系统不再说“你掌握了”,而是说“你大概有73%的可能是掌握了函数单调性”,并且随着新数据不断修正这个信念。Project Akari 的核心工作,就是把这个概率估计做得足够准,再把估计结果翻译成下一道题的选择。这件事听着很学术,但落到工程上,它决定了整个推荐系统的数据流、特征设计、模型选型和评测方式。我下文要讲的每一层设计,都是从“概率估计”这个原点长出来的。
1.2 现有方案为何在数学与CS上水土不服
市面上已经有不少做自适应学习的商业产品和开源方案,但大多是从通识题库出发,学科粒度很粗。数学和CS天然适合自适应学习,因为两者的知识结构都有非常明显的层次性和前置依赖:学不会导数,定积分基本无从谈起;不理解递归的调用栈,就很难真正掌握分治算法。但它们又有截然不同的难点——数学重推理过程和严谨证明,学生提交的往往不是简单选项,而是一串步骤、一个表达式,甚至一张草稿;CS重代码实操,学生在编辑器里的每个编译错误、每轮测试失败,都是比“对错按钮”丰富得多的行为信号。
通用引擎在这两个学科上往往会出同一个毛病:只看结果不看过程。一道数学证明题,学生过程中间走了三次弯路,最后写对了答案,系统判定满分掌握;一道CS编程题,学生抄了别人的答案然后运行通过,系统也判定掌握。结果学生到综合题面前全线崩溃。这也是我决定做 Akari 的起点:必须让引擎理解“数学的推理链路”和“CS的调试过程”,而不能只是把选择题换成编程题来打分。
1.3 Akari 的定位与目标:不是“智能题库”,而是“学习状态机”
所以 Akari 的定位从来不是“更智能的题库”,而是“学习状态机”。它把每个知识点当作一个可能迁移、可能遗忘、也可能被干扰的状态,把每一次作答当作一次观测,通过观测不断推断真实状态,再根据状态计算出学习收益最高的下一步动作。这个定位带来三个直接影响:第一,推荐不能只看正确率,要看知识点的掌握概率变化幅度;第二,推荐必须考虑时间因素,因为记忆会遗忘,昨天会不等于今天会;第三,必须把解题过程结构化,让过程性信号进入模型,而不只是最终答案。
这个定位也决定了 Akari 的整体架构:底层是内容工程,包含知识点图谱和题目标注;中间是推理引擎,包含知识追踪和遗忘模型;上层是推荐策略,包含召回、精排和规则兜底;外围是评测模块,用来判断每次改动是否真的让学生学会更多。本章先把“为什么做”讲透,后面逐一展开每个模块怎么设计、怎么实现。
2. Akari 的推理大脑:知识追踪、遗忘曲线与推荐策略的设计
2.1 知识追踪选型:IRT、BKT 与深度模型的取舍
知识追踪是整个引擎最核心的一环。我最初面对的是一个三选一的局面:项目反应理论IRT、贝叶斯知识追踪BKT、深度知识追踪DKT。三者我都试过,收集了各自的表现,也逐步摸清了它们的使用边界。
| 算法 | 核心思想 | 数据要求 | 在Akari里的定位 |
|---|---|---|---|
| IRT | 用能力值、题目难度、区分度建模答对概率 | 每个题目样本量要足够,否则参数漂移严重 | 用于题目校准、能力估计的“基线层” |
| BKT | 每个知识点视为隐状态(掌握/未掌握),用观测正确率迭代更新 | 对单知识点内数据要求适中,解释性强 | 主干推理模型,支持遗忘扩展 |
| DKT | 用循环神经网络从序列中隐式学习知识状态 | 需要大规模作答序列,黑盒,难解释 | 离线实验备用,作为特征补充 |
我最终选择 BKT 作为主干,不是因为 DKT 不准,而是因为 Akari 的早期场景里,解释性比精度更紧急。老师要问“为什么推荐这道题”,家长要问“孩子到底哪里不会”,学生自己也要知道“系统为什么说我忘了”。BKT 的状态更新逻辑是一步一步可回放的,能天然给出这样的解释。DKT 先放在离线通道里做特征比较,等数据集足够大了再考虑融合。
2.2 BKT 的主干更新:从先验概率到后验概率的滚动估计
BKT 的核心参数每个知识点一组:初始掌握概率(这个知识点学生一开始就会的可能性)、学习概率(做一题后从未掌握转移到掌握的概率)、猜测概率(未掌握却蒙对的概率)、失误概率(掌握了却写错的概率)。每做一道题,系统根据这道题的对错,把当前掌握概率更新成后验概率,再叠加一个学习概率,进入下一回合。
我用 Python 写了一个轻量实现,核心只有几十行:
def bkt_update(prior, p_slip, p_guess, p_transit, correct): if correct: # 答对:可能来自“已掌握且没失误”,也可能来自“未掌握但猜对” posterior = prior * (1 - p_slip) posterior /= prior * (1 - p_slip) + (1 - prior) * p_guess else: # 答错:可能来自“已掌握但失误”,也可能来自“未掌握且没猜对” posterior = prior * p_slip posterior /= prior * p_slip + (1 - prior) * (1 - p_guess) # 学完这一题,状态可能发生迁移 return posterior + (1 - posterior) * p_transit真正让这套逻辑“活”起来的,是参数估计。你不可能靠拍脑袋定猜测概率。Akari 的离线管线会用历史作答日志,用极大似然估计拟合每个知识点的四参数。拟合时不只看最后一步对错,还结合学生作答用时作为辅助信号——用时过短的“秒对”要下调观测置信度,用时过长的“磨对”也要下调,防止把蒙对当成掌握。这个细节是我们实测后加上的,不加之前,BKT 会把大量“蒙对”判成“掌握”,推荐难度一直不敢拉上去,学生进步很慢。
2.3 遗忘曲线的加入:时间间隔不该是推荐的黑盒
BKT 原版有个明显短板:它假设学生一旦掌握就不会遗忘。但在数学和CS的日常练习里,遗忘恰恰是最常见的现象。我给 Akari 加了一层遗忘衰减:每次作答间隔超过一定阈值,掌握概率会按时间指数衰减,衰减参数随知识点类型调整。概念型知识点衰减快,操作型知识点衰减慢,比如“二次函数求顶点坐标”衰减就比“因式分解”快一些。
初始衰减系数来自课程教师的经验标注,后续用真实数据回归校正。形式上,每次用户重新进入系统时,Akari 会根据距离上次作答的小时数重新计算一轮先验概率。这一步让推荐引擎产生了一个关键变化:系统开始主动给那些“看似掌握但已经三天没练”的知识点安排复习题,而不是永远推新知识点。实测下来,学生的周遗忘率明显下降,而且学生反馈“感觉系统好像记得我昨天会那个,今天忘了一点”——这种真切体验是提升信任感的最大利器。
2.4 下一题推荐:从“查漏补缺”到“学习收益最大化”
掌握了知识状态,剩下的问题是:给学生推荐哪道题?最简单的做法是“推荐最短板”,也就是掌握概率最低的知识点。但这样很快就会让学生崩溃,因为最短板通常是最难的地方,连续受挫会直接导致弃学。Akari 的推荐策略把目标定义为“学习收益最大化”,而不是“最短补缺最大”。
我给每道题算四个维度:掌握增益的期望、知识与当前薄弱点的关联度、难度和当前能力的匹配度、以及题目的新鲜度衰减。精排打分用加权求和,权重初值来自回归实验,上线后再根据A/B结果调:
score = 0.45 * expected_gain + 0.25 * weakness_overlap + 0.20 * difficulty_fit - 0.10 * exposure_decayexpected_gain 是用当前知识点掌握概率代入 BKT 更新公式,分别计算答对和答错后的后验概率,再取期望。difficulty_fit 讲究的是“心流区间”:题目正确率在40%到70%之间时,学习效率通常最高。低于30%则说明太难,学生容易放弃;高于80%则说明太简单,学习收益有限。这一条规则看起来朴素,但实际推荐效果比重排引擎调参显著得多。后面我会在评测章节再展开这套策略如何验证。
3. 工程架构与数据闭环:延迟、日志与离线训练怎么配合
3.1 系统分层与各层的职责划分
算法再漂亮,落不到低延迟的在线服务上也是白搭。Akari 的系统架构分为内容层、状态层、推荐层和交互层。内容层维护题目库、知识点图谱和标注数据;状态层维护每个用户每个知识点的实时掌握概率和遗忘计时器;推荐层做召回、精排、规则过滤;交互层负责对接前端做题界面,并把用户行为写回日志。四层之间通过内部API通信,各层职责互不越界。
这个分层最大的好处是可以独立迭代。某一天内容层新增了一百道题,不需要改推荐层代码;推荐算法要改版,也不影响交互层。实际开发里,早期版本最容易犯的错就是把推荐逻辑写在用户答题的请求处理函数里,结果每次改策略都要处理线上请求的兼容性。我后来感受到,教育系统后端代码再“小”,也建议按模块拆干净,因为推荐策略的迭代频率远比你预期的高。
3.2 在线推荐的低延迟实现:召回、精排与规则兜底
在线请求路径分三步。第一步是召回:基于当前知识状态,从全题库里筛出候选题目集合。召回条件包括知识点范围、难度区间、最近是否做过、上一次推荐时间。Akari 用 Redis 缓存每个学生的近期作答记录和知识点状态,避免每次请求都查库。第二步是精排,也就是上一章说的打分公式,逐题计算后取 Top N。第三步是规则兜底,这些规则直接用代码硬写:同一道题60天内不重复推荐、连续推荐的知识点不能超过三个、CS 判断题之后尽量接一道代码题等等。
延迟方面,实测通常在 120ms 到 220ms 之间。这个速度对在线学习场景完全够用。需要注意的是,不要把深度模型放在在线请求里跑。Akari 的做法是离线训练完成所有模型更新,在线只读取已经算好的知识点参数和用户状态快照。DKT 的特征计算也在离线做,在线只查结果。这样既兼顾了模型复杂度,又保证了响应速度。
3.3 日志事件设计:一切训练数据都来自作答过程
没有日志,自适应学习就是无源之水。Akari 从第一天起就强制统一答题事件的 Schema,每个事件包含用户、科目、知识点、题目编号、事件类型、事件结果、时间戳和客户端会话。事件类型不止“提交答案”,还包括“查看提示”“多次编辑”“切换题目”“放弃作答”“查看解析”。我把这些统称为过程性行为日志。
| 字段名 | 类型 | 说明 |
|---|---|---|
| user_id | string | 用户唯一标识 |
| item_id | string | 题目唯一标识 |
| knowledge_id | string | 关联知识点 |
| event_type | enum | 进入/提交/提示/放弃/切题等 |
| result | string | 正确/部分正确/错误/超时 |
| duration_ms | int | 作答时长 |
| attempt_count | int | 本题尝试次数 |
| hint_used | boolean | 是否使用过提示 |
| ts | string | 事件发生时间 |
这套日志规范看起来繁琐,但它是后面所有模型训练的基石。BKT 的参数拟合、DKT 的序列生成、遗忘曲线的回归,全都吃这同一套日志。如果你正在从零搭类似系统,我建议你在产品上线前就把日志埋好,后补的数据往往缺这缺那,清洗成本远大于想象中的量。
4. 内容工程才是真正的门槛:知识点图谱、题目标注与学科信号
4.1 知识图谱的粒度:太粗推不准,太细没数据
算法可以复用开源项目,内容只能自己砌。Akari 花时间最多的,不是模型,而是知识点图谱和题目标注。粒度问题尤其折磨人:如果把“函数”当一个知识点,推荐策略等于没有;如果把“二次函数在给定区间内的值域求解”当成叶子知识点,数据量又会被稀释得根本拟合不出参数。
我们最后的折中方案是三层结构:大专题、中知识点、微技能。大专题是章节级别,比如“函数与导数”;中知识点是可以单独安排教学单元的程度,比如“复合函数单调性”;微技能则是具体到某类题型的解题动作,比如“利用定义法判断单调性”。推荐引擎的操作单位是中知识点,但会在微技能上记录掌握状态。这样一来,推荐时既有足够的颗粒度做差异判断,又不会因为微技能太细导致每个点的训练数据撑不起来。
4.2 题目元数据与作答信号:数学和CS各自要单独设计
一道题进入 Akari 内容库,至少带下面这些元数据:所属中知识点、关联微技能、认知层级(记忆、理解、应用、分析、创造)、预期难度、预期用时、是否使用提示词、干扰项对应的典型错误类型。CS 题还要额外标注语言类型、输入输出约束、是否涉及算法复杂度分析、以及评测用例覆盖的知识点。
真正让 Akari 区别于通用引擎的,是作答信号设计。数学题我们接入了过程逐步提交:学生可以先提交第一步化简,再提交第二步构造辅助线,系统可以定位第一个出错步骤。CS 题我们采集三类信号:编译阶段错误(未过编译)、运行阶段错误(超时或崩溃)、测试用例级错误(哪些用例失败)。这些信号的价值在于,它们反映了具体的认知障碍模式。CS 里一个学生反复在“边界条件”上丢用例,和反复在“算法超时”上丢用例,是两个完全不同的问题属性,推荐策略也应该不同。
4.3 内容审核与迭代:标注错误比算法偏差更致命
自适应学习引擎有一个信息论层面的对称性:模型再好,标注错了也是白搭。一道题目被标错知识点,模型的所有推断都会被带偏。实测中我们发现,最影响在线体验的问题往往是两道题共用了一个文本模板、或者 CS 题被误标为数学题、又或者同一知识点下题目难度排序标反了。
因此 Akari 的内容管线里专门有一道“校准回测”:每周用真实作答数据对所有题目的标注做一遍一致性检查。看看每道题的实测正确率是否符合标注时的预期难度,若某道题标注难度与真实作答难度偏差超过阈值,自动进入人工复审队列。这个环节听起来不“高级”,但它带来的体验改善经常比算法调整更直接。作为工程师出身的实践者,我最大的体会是:自适应学习的内容质量是硬门槛,含糊不得。
5. 冷启动、解释性与实测:从“能跑”到“能用”的距离
5.1 冷启动的三种情况与对应的降级策略
冷启动问题在真实项目里不是一种,而是三种:新用户、新题、新知识点。新用户没有任何行为日志,系统连他当前水平在哪个位置都不知道。Akari 的处理是用一套前置探查题,由内容组专门设计,覆盖基础中知识点,且难度从低到高递增,同时结合学生自评和学校年级基准线做先验。新用户进入后,先做6到8道探查题,系统就有了第一版能力画像。
新题没有作答统计,不知道真实难度和区分度。我们用三个数值拼出一个先验难度:该题的文本复杂度、所属知识点历史平均难度、以及试题编写时的心智标号。上线后随着作答量积累,这个先验会被真实统计逐步替换。新知识点则更麻烦——整个知识点都没有历史数据,推荐引擎无法判断是否该推荐这个知识点。Akari 的做法是做“相似知识迁移”:找到结构上最接近的已有知识点,把参数估计结果作为新知识点的初始值,上线后可快速校准。
5.2 稀疏反馈下的参数估计:别想着给每个人单独建模
自适应领域一个很容易踩的坑,是希望给每个用户单独建立一套模型,愈精细愈好。但数学与CS的实际作答数据是非常稀疏的,一个用户一个学期也就几百道题,单独拟合个人 IRT 参数大概率过拟合。Akari 采用分层贝叶斯的结构路径:先学习全局先验分布,再让每个用户在后验更新中逐渐拉开差异。
实际操作中,我们把新用户的前20次作答作为“校准期”,这期间系统还是会推荐题目,但是推荐的排序关闭个性化,只按知识点的基础逻辑来。校准期结束后,用户的状态向量才参与个性化精排。这个做法避免了一个让人头疼的现象:冷启动阶段因为数据噪声太大,推荐结果时好时坏,导致学生失去信任。逻辑上更平稳地过渡,反而让留存数据更健康。
5.3 可解释性:学生、老师、家长分别想看什么
算法黑盒在新闻资讯场景也许可以容忍,但在教育场景里不行。学生希望你告诉他为什么做这道题;老师希望你告诉他班级整体弱在哪;家长只想知道孩子的知识漏洞长什么样。Akari 给每个推荐生成一条简短的理由卡片,内容包含:当前知识点掌握度、最近同类题的作答趋势、距离上次练习的间隔、以及这次推荐的学习目标。
实践下来,最好的理由卡片是一条融合了数据和教学判断的句子,比如“你最近三次同类题正确率从40%升到80%,但已隔26小时没练,推荐一题巩固并加深难度。”这类解释不需要把 BKT 的公式展示出来,但要把“概率变化”翻译成人和人之间能理解的因果表达。在线反馈里,学生对这样理由的接受度明显高于“系统根据算法向你推荐本题”。记住一点:可解释性不是给算法专家看的,是给真实使用者看的。
6. 评测体系与上线迭代:算法改得好不好,不能靠感觉
6.1 离线评测:预测准确率只是最低门槛
开发第一个版本时,我最关心的指标是预测准确率:模型预测答对的概率和真实作答结果是否一致。这个指标当然要有,但不能唯它马首是瞻。Akari 在离线评测里同时跟踪三组指标:预测类指标(AUC、RMSE、LogLoss)、教育目标类指标(掌握的稳固性、遗忘召回率、后测知识得分相关性)、以及推荐体验类指标(推荐知识点覆盖度、题目重复率、难度分布、连续挫败率)。
| 指标组 | 具体指标 | 作用 |
|---|---|---|
| 预测准确度 | AUC / RMSE / LogLoss | 判断状态估计是否靠谱 |
| 教学效果 | 遗忘召回率 / 后测相关 | 判断推荐是否促进长期记忆 |
| 体验健康 | 重复率 / 挫败率 / 难度分布 | 判断推荐是否导致学生不适 |
这套三维评测的引入,让不少自认为有效的改动露出了真实面貌。比如有版本预测准确率上升,但续题率下降,说明系统虽然更准地在做题判断,却变得过于保守,所有题目都往学生最熟练的知识点靠——准确率好看了,学习收益没了。只看单一指标的话,很容易做出“做题很舒服但没什么进步”的伪自适应。
6.2 在线上线:A/B 实验之外还要盯住健康度指标
离线评测通过后,Akari 会进入小流量线上实验。实验中我正在意的三件事:样本量的最小周期、可能混杂的因素、以及长期效果的迟滞效应。一个推荐改动至少要观察两周,均摊到用户和场景里去判断,避免被一次考试或一个假期带偏。线上指标除了学习时长和答题量,还要细分正确率之外的情绪维度:学生平均连续答错两次的次数、放弃率、以及主动查看解析的比例。
这几个“健康度指标”往往比总挑战次数更早暴露问题。曾有一次我们调整了遗忘系数,导致复习题占比大幅上升,虽然正确率看起来稳定,但学生主动学习时长下降了不少,当时就是靠连续失败率和切题率的变化抓住了这件事。要记住,在线实验里你可能永远拿不到统计意义上的完美证据,所以总要保留一些“经验判断”的空间,但是判断要基于数据,不是凭感觉。
6.3 一个迭代序列:先内容,再算法,最后做交互
项目组总结出了一条稳妥的迭代节奏:先修内容,再改算法,最后动交互。内容层的错误最容易被忽视,但也最影响信任;算法改动在内容和数据可靠的基础上才有意义的;交互层的变动只有在算法效果稳定后,才有足够的解释空间去归因。Akari 每次发布都拆成三个独立版本:纯内容校准版、纯推荐策略版、纯UI交互版。各方相互不干扰,出了问题也能秒级回滚。
这套流程看起来很保守,但教育产品犯错成本太高。有一次我们几乎同时对难度模型和题目默认顺序做了修改,结果推荐合理性数据下降,怎么排查都说不清是哪一步造成的,最后只能整体回滚。后来我们就立了规矩:一次只动一个变量。在自适应学习这类多因子系统里,这条规矩省下的时间是惊人的。
做 Akari 的时间里,我最真切的感受是:自适应学习引擎的复杂度不在一道题,也不在一个公式,而在于如何把数据、算法、内容、反馈在一个真实系统里长期协同跑起来。你可能花了很大力气写的模型,最终被一个标注错误打败;你也可能因为一个遗忘因子,让整个推荐节奏焕然一新。如果你也要做类似系统,我的建议只有一条:尽早定好日志规范和标注规范,然后在一个足够宽的时间窗口里,老老实实地看数据、跑实验、解释每一次变化。教育引擎没有灵丹妙药,只有持续校准之后的螺旋上升。