· engineering· 约 24 分钟精读

端侧离线教育沙盒的概念问答交互契约:两套作答面、单次提交的边界与只记正确的记录缺口

核心要点速览 · TL;DR 概要

同一套题库在 QubitLab 里长出了两张面孔:沙盒实验室的整卷测验与奥德赛关卡的前置概念题。本文逐个函数量出这两条作答链路的判定、锁卷、揭晓与记录,给出十道题与九十六道题的正确项位置分布,并说明为什么“答错”在这套系统里几乎不留痕迹。

端侧离线教育沙盒的概念问答交互契约:两套作答面、单次提交的边界与只记正确的记录缺口

一、一个学生按下第二个选项之后

点开任意一关的导读页,十六比九的课程卡播完,往下是一道概念题,四个选项,没有提交按钮。手指落在第二个选项上,那一行变成青色描边,右侧出现一枚空心对勾,页面底部原本灰着的按钮亮起。整个过程里,学生不知道自己这一下是对是错。

另一处,沙盒实验室的双缝实验做完,点开“概念测验”,同样是四选一,选好之后还要按一次“确认并核对答案”;按下去的那一帧,正确项转绿、错选项转红,下面是物理解析和一条趣味拓展。

两处界面看起来是同一个功能,代码里却是两套不同的契约:判定时机不同、揭晓策略不同、写盘字段不同、重测语义不同。本文把这两条链路逐个函数翻一遍,把能量化的部分量化,最后说明这套设计在教学上留下了什么缺口,以及缺口该怎么补。

二、两套作答面:入口、题量与结算

QubitLab 的概念题分成两组。一组挂在沙盒实验室下,由 QuizSet.allQuizzes 提供,六个实验室共十个 QuizQuestion;一组挂在奥德赛关卡的导读页,九十六个关卡各带一道,字段直接写在 PuzzleLevel 上(quizQuestion、quizOptions、quizCorrectIndex、quizExplanation,四个都是可选类型)。

这两组的差别不止在入口,而在整条契约上。

维度沙盒实验室测验奥德赛关卡概念题
入口实验室详情页的测验卡关卡导读页的课程卡下方
题量六实验室共十题,一次走完整卷九十六关各一题,一次只做一题
选项固定四项,顺序不洗牌固定四项,顺序不洗牌
提交选好再按“确认并核对答案”点选即提交,没有确认动作
锁卷本卷内提交后不可改选当次视图内点过即不可改选
揭晓正确项转绿、错选转红,解析展开只给选中项描边与空心对勾,答案不出现
记录quizScore 写一个 0 到 100 的整数levelQuizMastered 插入关号,再重算星级
失败留痕只在结算时体现为较低百分比无,答错不写任何字段
重测可无限重测,每次覆盖上一次每次重新进入关卡可再答,直到答对
后续影响证书逐实验室显示百分比决定该关第三颗星,进而影响章节解锁

前五行是界面差异,后五行是契约差异。真正决定这套题教学效果的,是后五行。

三、沙盒测验的四段链路

概念问答的两套面:一次点选之后的判定与记录沙盒实验测验整卷结算,奥德赛关卡单题闸门;同样四选一,记录方式并不同步沙盒实验室测验六个实验室 · 十道题 · 整卷结算一、四选一作答每题四个选项,顺序固定不洗牌二、确认后锁卷本卷之内改不了选中的那一项三、当场揭晓正确项转绿,解析与拓展同时展开四、末题结算写整数百分比,低分会覆盖高分奥德赛关卡概念题九十六关 · 每关一题 · 进入实验的闸门一、点选即提交没有确认键,一次点击就是一次作答二、不揭晓对错只给选中项一枚空心对勾,答案不露出三、只记正确答对写入掌握集合,答错不留任何记录四、星数重算请求值先截到两颗,已掌握才补足三颗一百零六道题里八十五道的正确答案落在第二项,第四项一道也没有

图左是沙盒测验的四段,图右是奥德赛关卡对应的四段。先看左边。

3.1 状态机只有五个变量

LabQuizView 的作答状态全部是本视图的 @State:currentIndex、selectedAnswer、hasSubmittedAnswer、correctAnswersCount、isCompleted。选项按钮的第一行守卫是 guard !hasSubmittedAnswer else return,也就是提交之后本卷内不能再改选;submitAnswer() 只做三件事——置位、按对错给分并触发对应触觉、把选中项留在状态里。推进逻辑在 advanceQuestion(),最后一题走结算分支。

这套状态简短、好读,代价是所有东西都在视图里,进程一退就没了。

3.2 得分行的分母取错了量

题头那一行是这么写的:得分取 correctAnswersCount,分母取 currentIndex。而 currentIndex 是当前题号(从零起),提交后、推进前不变。于是真机上会看到这样一串:

  • 进入第一题:0 / 0(审计截图里这一帧就是 0 / 0);
  • 答对第一题那一下:1 / 0;
  • 推进到第二题:1 / 1;
  • 答对第二题:2 / 1(一直显示到按下“查看测试总结”);
  • 随后切到结算页。

分母始终是“已经翻过去的题数”,与总题数无关。两题卷面上“二比一”这样的分数会真实出现在屏幕上。

结算页另算一次 Int(Double(correctAnswersCount) / Double(questions.count) * 100.0),这一处是对的。所以写进本机的分数没有问题,错的只是学生在答题途中读到的那一行自我评估。这类错误的麻烦之处在于它不崩、不报错,只是安静地给出一个错误的分母。

3.3 进度条把“位置”当成了“进度”

ProgressView(value: Double(currentIndex), total: Double(questions.count)) 的分子是题号,不是已答数。两题卷停在第 1 题时是百分之五十,答完立刻切页,进度条永远到不了一百。进度条在这里表达的是“翻到第几页”,而学生读到的是“做了多少”。

3.4 中途退出等于没做

写盘只有一处:advanceQuestion() 的最后分支 progress.quizScore[lab.rawValue] = scorePercent。此前所有答案都在 @State 里。右上角“关闭”直接 dismiss(),sheet 也没有禁用下滑关闭,两条退出路径都不弹确认。答完第一题就退出,回到实验室详情页,卡片上仍然是“开始测验”,刚才那次作答不存在。

这件事实在不必如此:离线应用本可以把这份状态写进本机存档,不需要网络、不涉及同步、也没有隐私成本。这里不是不能,是没做。

3.5 覆盖写与“只留最好”的两种态度

同一个 LabProgress 里,星级用 max 保留历史最好(completeLevel 里 if masteryStars > currentBest),沙盒测验分则是直接赋值。于是重测一次答错三题,六十分会盖掉此前的满分。两种策略并存不算矛盾,但界面上“重测”这个按钮在两种语义下含义不同,用户无从预判。

顺带一处:模型层写了 recordQuizScore(labId:score:),全工程零调用,视图绕过它直接改字典。方法里以后想加的校验、留痕、审计,也就都没有落点。

3.6 六个实验室的题量分布

实验室题数正确项位置是否免费
波粒二象性2第二项、第二项免费
叠加与比特2第三项、第三项免费
量子隧穿2第二项、第一项付费
幽灵纠缠1第二项付费
电路设计2第二项、第二项付费
密码通信1第二项付费

十道题里有六道落在四个付费实验室,免费侧只有四道;题量与实验室的概念密度也没有对应关系——纠缠是整个沙盒里概念最重的一块,只配了一道题。

四、奥德赛关卡:单题、不揭晓、只记正确

4.1 点选即提交

quizOptionButton 里没有确认动作:先 guard selectedQuizOption == nil else return,然后一次触觉、一次赋值,紧接着 if idx == correct 就写掌握。误触即一次作答,没有撤销。

4.2 对错都不说

选项的视觉状态只取决于“是否被选中”:背景转青、描边转青、右侧一枚未填充的对勾。正确索引只在判定时用一次,从不参与渲染。学生拿到的第一条反馈是结算页的星数:三颗表示答对,两颗表示答错;两颗那一页还会写“物理装置已全部点亮!返回导读页答对概念题可获得第 3 颗金星!”——用一句话事后否定了刚才那次作答。

另外,进入实验的闸门只检查“有没有答过”,不检查“答得对不对”:只要选中过一项,底部按钮就亮。题目在这里的作用是强制一次思考,不是筛选。

4.3 答错不留任何痕迹

recordLevelQuizMastery 的第一行是 guard isCorrect else return。答错:不插入、不计数、不落盘。承载它的字段是 levelQuizMastered: Set<Int>?,一个可选集合,只能表达“这一关已掌握”。它无法表达“答过、但错了”“错了几次”“上次选了哪一项”。

所以 refreshStateForCurrentLevel() 只有两态:已掌握就把选中项预置成正确项,未掌握就清空。想让界面说一句“上次你选了第三项”,现有模型做不到。

4.4 星数由模型层说了算

triggerWin 先算“答对给三星、答错给两星”,再调 completeLevel。而 completeLevel 里会把请求值截到零到二之间,再按掌握集合决定是否补到三:

S=max⁡(Sprev, min⁡(max⁡(s,0),2)+1q∈Q)S=\max\left(S_{\mathrm{prev}},\ \min(\max(s,0),2)+\mathbf{1}_{q\in Q}\right)

其中 s 是调用方请求的星数,Q 是已掌握的关号集合。任何调用方传一百颗星,最多也只拿到两颗——这是正确的防御位置,规则不该由视图决定。代价是同一套规则在两处各写了一遍,读代码的人得先确认哪一份有约束力。

4.5 “只能选择一次”的三条失效路径

界面标题写着“科学概念问答(只能选择一次)”,选项中也有注释说只能选一次。实际有三条路径可以再答:

  1. 结算页的“返回导读页补答题目”按钮,里面显式把选中项置空,注释原文就是重置以便再选;
  2. 退出关卡再进,onAppear 触发 refreshStateForCurrentLevel(),未掌握时同样置空;
  3. 同一关可以反复进出,没有次数上限,也没有冷却。

所以真实语义是“每次进入只能提交一次”,而不是“这一关只能答一次”。前者的作用是防止在一屏之内反复试错,后者才是门槛。两者在教学上相差很远:一个允许想想再来,一个要求一次想对。

4.6 前置题允许后补

题目的定位写在模型注释里:实验前的概念检查。但 recordLevelQuizMastery 的后半段是,若该关已有两颗星,直接把星级提到三颗。也就是说实验做完再回来补答概念题,第三颗星照样到手,不用重做实验。

设计上这不算错,它避免惩罚先动手的学生;但它让“先想清楚再做”的顺序约束失效——学生可以先把装置点亮,再对着四个选项慢慢试。

五、题库的统计面

把两组题的正确项位置数一遍,得到一张很集中的分布:

选项位置沙盒十题奥德赛九十六题合计占比
第一项1898.5%
第二项7788580.2%
第三项2101211.3%
第四项0000%

p=85106≈0.802p=\frac{85}{106}\approx 0.802

三个可以当场算出来的结论。

第一,固定选第二项的盲选策略,沙盒卷面拿七十分,奥德赛有百分之八十一点二五的关卡直接得到第三颗星。若四个位置等概率,盲选期望是四分之一;当前位置分布把期望抬到约八成,等于把四选一压成了一点二五选一。

第二,一百零六道题的正确答案没有一道在第四项。按一二三四顺序轮换,任何一题最多三次必然命中;沙盒两题卷和奥德赛单题关都成立。

第三,选项顺序不洗牌。同一道题每次呈现完全相同,“上次第三项是对的”这种位置记忆长期有效,与第一条叠加。

三条叠起来,第三颗星在当前题库上无法区分“想过”与“试过”。这不是安全漏洞,是一道测量学问题:区分度被位置分布吃掉了。

5.1 干扰项的语义距离

题目要点正确项典型干扰项排除所需知识
哪种日常技术依赖隧穿闪存与固态硬盘木质铅笔、机械钟表发条、自行车链条齿轮常识即可排除
CNOT 的作用控制位为一则翻转目标位关闭量子计算机、强制清零、加快时钟频率知道门操作即可排除
在缝上观测光子波函数坍缩成两条粒子条纹干涉纹更亮、光子超光速、探测器起火基本物理常识即可排除

干扰项与正确项的距离过大,排除法可以完全绕过概念本身。这对启蒙阶段不算错,荒谬干扰项能降低挫败感;但它决定了这套题测的是“有没有听说过”,而不是“有没有想明白”。若目标是后者,干扰项应当来自同一概念族内的常见误解,例如把“观测导致坍缩”与“观测导致相位退相干”并列,逼学生分辨两者。

六、写了却不显示的解析

九十六道关卡题每题都写了物理解析,并且通过 currentQuizExplanation 把七种语言的译文接了出来:日语、法语、西班牙语、德语、简体中文、繁体中文各九十六条,韩语六十四条(其余三十二关回落英语原文)。文本本身涉及 656.3 纳米的 Hα 跃迁、位置与动量的不确定性乘积下限、CHSH 参数的二倍根号二这类具体结论,不是随手写的安慰话。

然后视图层对解析字段的引用数是零。

也就是说,为九十六道题准备了解析,翻成了七种语言,最后没有任何界面把它显示出来。它现在只是包体里一段死数据:占体积、需要维护、会随题库改动同步过期,却不产生任何教学作用。

两种收尾都合理:要么答完之后显示解析,与沙盒侧对齐;要么明确“本题不提供解析,解析在导读视频里”,并把字段删掉,免得后来的人以为有界面。现状是第三种——字段在、译文在、界面没有。

同一屏上还有一处语言覆盖的不对称。题干与选项的译文覆盖简繁日韩四种,而这一屏的界面框架文字只分简繁两档,其余语言一律英文。结果是日语用户读到日文题干、配英文的“得分”“确认并核对答案”,德语用户整屏英文。这不是翻译质量问题,是两套字符串表各自覆盖了不同的语言集合。

七、记录缺口的后果

三处消费方读到的口径各不相同:

消费方读什么显示什么
实验室详情页单实验室分数“N% 得分”,并决定按钮是开始还是重测
设置页已测实验室个数“已测 / 六”
证书页逐实验室分数“测验 N%”
家长报告关卡星级与总星数完全没有测验成绩

沙盒侧只存一个整数百分比:两题卷的五十分,可能是第一题错,也可能是第二题错。奥德赛侧只存一个集合:只有掌握与未掌握。想在端侧回答“这个学生在观测与坍缩这一族概念上是否反复出错”,现有字段一个都不够。

这一点要说清楚:缺口不是隐私设计换来的。百分之百离线的取舍是“数据不出设备”,不是“数据不存在”。本机完全可以存一份逐题作答记录——题号、选项、时间序、连续次数——不上传、不同步、不进任何遥测,仍然满足数据不出设备这条底线。缺的是记录面,不是隐私面。

八、如果要改:五条工程线

  1. 选项洗牌并映射回去。呈现层打乱顺序,正确索引随置换映射回原位置;记录里存选项的稳定标识而不是位置,否则洗牌之后历史数据无法解释。
  2. 逐题作答落盘。给进度模型增加作答记录(题标识、选项标识、时间序、连续次数),答错也写。判定仍可以只看掌握集合,但诊断从此有据。
  3. 分数取最大值。测验分与星级对齐,重测不降级;若坚持覆盖,就在重测按钮上明示“本次将覆盖上次成绩”。
  4. 断点续答。把当前题号与已答结果写进进度,关闭面板前给一次确认,进度条分子改成已答数。
  5. 语义统一。把“只能选择一次”改成“本次进入只能提交一次”,或者真的做成一次;实验前与实验后二选一,在文案和模型里同时落地。

这五条都不需要联网,也不改变数据不出设备的边界。

九、能力边界

  1. 四选一的一次命中不等于理解,位置偏差让它更弱。
  2. 三次内必中,意味着第三颗星在这个题库上无法区分想过与试过。
  3. 答错零留痕,等于把最有价值的教学信号丢在本机之外——不是丢在云端,是根本没记。
  4. 解析不显示本身不构成缺陷;写了不显示才会让后续维护重复劳动。
  5. 全部判定在本机完成,没有行为数据回流,题库参数只能靠人工评审调整。这是离线架构的固有代价,不是疏漏。

十、结语

把作答当成一个状态机来看,两套面的代码都很短:一个守卫、一次置位、一个分支、一次写盘。短到容易被当成同一个功能。

真正决定教学效果的,不是四选一这个形式,而是四件事的组合:什么时候锁、锁了告不告诉对错、答错记不记、重测算不算数。QubitLab 在沙盒侧选择了锁后揭晓、整卷记录、重测覆盖,在关卡侧选择了点选即锁、全程不揭晓、只记正确、可以重来。两种选择各有理由,但它们不该由两份独立的代码各自决定——一旦要回答“这个学生到底学会了没有”,两份记录必须能对上同一道题。

参考文献

[1] Apple Inc. ProgressView — Apple Developer Documentation. https://developer.apple.com/documentation/swiftui/progressview

[2] Apple Inc. Managing model data in your app — Apple Developer Documentation. https://developer.apple.com/documentation/swiftui/managing-model-data-in-your-app

[3] Apple Inc. Human Interface Guidelines — Providing feedback. https://developer.apple.com/design/human-interface-guidelines/providing-feedback

[4] Next Generation Science Standards. https://www.nextgenscience.org/

[5] W3C. Web Content Accessibility Guidelines (WCAG) 2.2. https://www.w3.org/TR/WCAG22/

[6] ISO/IEC 25010:2023 Systems and software Quality Requirements and Evaluation — Product quality model. https://www.iso.org/standard/78176.html

Share:
Back to Blog