判分与错误分类:答错之后系统认为你错在哪

2026-08-10

一个带练习的学习系统里,最容易被想象成”很智能”的环节其实是判分。多数人默认它是模型在读你的答案、揣摩你的意思。DeepTutor 的做法不是这样:判分这件事被单独抽出来放在 deeptutor/learning/grading.py,整个文件 64 行,两个函数,全是确定性字符串比较,一次模型调用都不发。

下文所有行号与数值都对应我们采集的仓库快照:HEAD 456f9c2,采集日 2026-08-10,deeptutor/__version__.py 里的版本号是 1.5.11。你自己 clone 后行号可能已经漂了,函数名与常量名是稳的。

先把一条边界写在前面:这些判对阈值、状态取值、流水线顺序,都是该项目在代码里做出的实现选择,不是经过验证的教学结论。本文只讲它们写在哪一行、怎么运作,不评价它们在学习科学上是否有效,也不承诺任何学习效果。

一、期望答案存在服务端,判分不经模型往返

要读懂判分,得先知道被比对的那个”标准答案”从哪来。

deeptutor/learning/models.py:164-182 定义了 PendingQuestion,也就是导师刚抛出、正等着你作答的那道题。这个类的注释写明了一件关键的事:期望答案存在服务端,从不经模型往返。与之配套的是 mastery 能力自带的 5 个工具,其中含 mastery_quizmastery_gradedeeptutor/capabilities/mastery/tools.py:61-67);期望答案由服务端持久化在 PendingQuestion 里(models.py:164-182),至于这两个工具之间的具体调用顺序,我们没有逐行核实。

这决定了判分的性质:模型负责出题和讲解,但”对不对”这一步不归它管。相应地,模型也就没法在评分环节改口——它拿不到重新解释标准答案的机会。

配套的还有选项这一侧。deeptutor/learning/pending.py:20,23-37 有一条解析选项标签的正则 ^\s*([A-Z])\s*[.::、))-]\s*(.+)$(忽略大小写),负责从选项文本里剥出 A、B、C 这类标签;超过 26 个选项时改用序号。另外 deeptutor/capabilities/mastery/loop.py:16-43 记录了一个动作:mastery loop 在 ask_user 调用时,会用服务端持久化的挂起问题替换模型自拟的展示数据,防止模型改题号或重排 A/B/C。选择题的判分要靠标签对齐,这一步在链路上是前置条件。

二、三种题型,三条规则

grade_answer 的签名在 deeptutor/learning/grading.py:13-47,支持的题型只有三种:choice / short / open。三条规则各自是:

题型判对规则位置
choice去空格后全等比较grading.py:30-33
short先全等;不等且期望答案长度 ≤ 30 时用 difflib.SequenceMatcher 比率 ≥ 0.85 判对;期望答案更长则直接判错grading.py:35-40
open[,;,;。\n]+ 切成关键词,命中比例 ≥ 0.6 判对grading.py:42-47

这张表要这么读:三条规则的宽松度不是递增的,它们各自解决不同形状的答案。choice 最严,因为它比的是一个标签;open 最宽,因为它比的是一组关键词里命中了多少;夹在中间的 short 才是最需要留神的那一条。

另外还有一条兜底:期望答案为空一律判错grading.py:20-21)。服务层有同款保护,deeptutor/learning/service.py:182-184 在调 grade_answer 之前先要求 expected_answer 非空。这是 fail-closed 的写法——没有存下标准答案时,这次作答被记成错,而不是被记成对。这一点在排查”我明明答对了却算错”时值得先确认:先看服务端有没有把期望答案存进那道挂起问题里。

三、本篇最反直觉的一处:short 的答案越长,判分越严

大多数人对模糊匹配的直觉是”越长的答案越容易蒙到相似度”。grading.py:35-40 恰好反过来。

它的分支顺序是:先做全等比较,相同就判对;不同的话,看期望答案的长度——只有期望答案长度 ≤ 30 时才启用 SequenceMatcher,比率达到 0.85 判对;期望答案长度超过 30,直接判错,连相似度都不算

也就是说,模糊匹配的开关不挂在你的答案上,挂在标准答案上。同样一道简答题,标准答案写成一个短词组,你写岔一两个字还可能过;标准答案写成一整句话,就退化成一字不差的全等比较,差一个标点也是错。

这条规则带来两个具体后果,都能自己去核:

其一,题目怎么出,直接决定判分的松紧。同一个知识点,期望答案定得长一点还是短一点,走的是两条完全不同的判分路径。这两个数字是写在 grading.py:35-40 分支里的字面常量;我们没有在事实卡覆盖的配置项里看到对应的可调开关,是否另有别处可配我们没有核实。

其二,长答案不该用 short 题型open 是按分隔符切关键词再算命中比例的,它对长答案更适配。至于某道题到底该用哪种题型,取决于你自己的出题方式,项目没有给通用建议,本文也不替它补。

顺带一句,题型白名单在 deeptutor/capabilities/mastery/tools.py:69 也有一份:_QUESTION_TYPES = ("choice", "short", "open"),与 grade_answer 支持的三种一致。再往下有一层题库类型映射(tools.py:93-99):choicechoiceopenwritten,其余→short_answer。判分用的名字和题库里存的名字不是同一套,做对账时别把两边的字符串直接等同。

四、错误分类:确定性代码只分两类,四类细分留给后面的 LLM

标题里那半句”系统认为你错在哪”,答案要拆成两段看。

deeptutor/learning/models.py:16-45 定义的 ErrorType 确实有四类:structural / deviation / application / metacognitive(同时带一套旧中文值映射,知识结构性 / 理解偏差型 / 应用错误 / 元认知型)。

grading.py:52-58classify_error() 只做粗分类,而且只有两个出口:

  • 答案是空的 → METACOGNITIVE
  • 其余一律 → APPLICATION_ERROR

源码把理由写在 docstring 里:空答案意味着学生不知道,其他一律先按”用错了”处理,更细的四类分类留给错误诊断阶段的 LLM

这处分工是本篇第二个值得记的点。判错的那一瞬间,系统给你贴的标签只有两种可能,它跟”你到底错在概念还是错在步骤”没有关系——那是后面的诊断阶段才做的事。所以如果你在数据里看到大量 application,先别急着解读成”学生普遍是应用型错误”:这个值有可能只是 classify_error() 的默认出口。要区分,得看这条记录后来有没有被诊断阶段改写过。LearningStage 的七个阶段里确实有独立的 error_diagnosismodels.py:61-72),但诊断阶段具体怎么改写 error_type,我们没有读到那段实现,不做推断。

五、判错之后,哪些状态跟着动

判分本身只返回一个布尔值,真正的连锁反应在服务层。deeptutor/learning/service.py:174-210grade_and_record() 写死了一条固定流水线:

记录作答 → 重算掌握度 → 推进间隔重复状态 → 重建复习队列 → 持久化。

卡里记录的是这条固定顺序(service.py:174-210),是否可跳过其中某一步我们没有核实。按这条顺序,一次作答会同时改动掌握度、复习排期和落盘文件——掌握度公式与间隔序列各自另有一篇专门讲,这里只说与判分直接相关的三处:

其一,错题记录的四个状态。 ErrorRecord.status 的取值被限定在 active / retrying / review / graduated,默认 activemodels.py:140)。答错时新建的记录进 active

其二,答对会”毕业”掉旧错题。 service.py:129-145:答对时,系统会找同一题目、同一知识点、状态为 activeretrying 的错题记录,把它改成 graduated。注意匹配条件是题目与知识点两者都对得上——换一道题考同一个知识点,不走这条分支。

其三,未毕业的错题会插到复习队列最前面。 deeptutor/learning/scheduler.py:80-87:构建复习队列时,处于 active / retrying 状态的错题所在的知识点,priority 被强制设为 1,高于所有按知识类型排的优先级。也就是说错题的优先级不是靠间隔算出来的,是硬插的。

六、还有一条根本不走 grade_answer 的路

deeptutor/learning/models.py:195-198 的注释把分工写得很清楚:qualitative_mastery 管 CONCEPT / DESIGN 两类知识点,mastery_levels 的定量闸门管 MEMORY / PROCEDURE。

对应到判分上,deeptutor/learning/policy.py:40-45QUALITATIVE_TYPES = frozenset({CONCEPT, DESIGN}) 这两类由导师用 mastery_assess 判定费曼式讲解,不做字符串评分。它们的落库走另一个入口 record_qualitative()service.py:239-241):通过则 mastery_levelsmax(current, 1.0),不通过则取 min(current, 0.4)

所以”答错之后系统认为你错在哪”这个问题,对定性类型是不适用的——那条路上压根没有 grade_answer,也没有 classify_error(),自然也不会产生 ErrorType。做数据分析时如果发现某些知识点从来没有错题记录,先确认它的 KnowledgeType 是不是 CONCEPT 或 DESIGN,而不是先怀疑数据丢了。

七、一处可核实的清单不一致

deeptutor/learning/__init__.py:3-10 的模块清单只列了 models / storage / scheduler / mastery / grading / service / prompts 七个,没有列 policy.py(295 行)与 pending.py(166 行)——而这两个文件确实存在,并且被 mastery 工具直接依赖。

按纪律,这里只陈述差异并标明位置,以我们实读的仓库状态为准,不推断原因,也不据此评价项目。说完就停。

八、你可以照着核的五步

  1. 打开 deeptutor/learning/grading.py:35-40,确认 short 那条分支里长度 30 的判断挂在 expected 上,不是挂在用户答案上。
  2. 打开 grading.py:52-58,确认 classify_error() 只有两个返回值,四类细分不在这个函数里产生。
  3. 打开 grading.py:20-21,再对照 deeptutor/learning/service.py:182-184,确认两层都做了”期望答案为空判错”的 fail-closed 保护。
  4. 打开 service.py:174-210,把那条五步流水线的顺序抄下来,与你自己系统里”作答之后要更新什么”逐条比一遍。
  5. 打开 deeptutor/learning/scheduler.py:80-87,确认错题优先级是硬置为 1 的,不是算出来的。

最后交代边界。本文只读了 grading.py 全文、service.pymodels.py 中与判分直接相关的几段,error_diagnosis 阶段如何改写错误类型、mastery_grade 工具的参数校验与返回结构我们都没有逐行核实。deeptutor/learning/tests/ 下有 233 个测试用例(按 def test_ 计数),我们只统计了数量,没有读断言内容,因此本文中的规则与常量是从实现读出来的,未经测试交叉验证。

再重复一次开头那条边界:以上全部是代码里的默认实现,不是运行结果的保证,更不是对学习效果的任何承诺。


本文依据 DeepTutor 官方仓库(github.com/HKUDS/DeepTutor)的 README、AGENTS.mdpyproject.tomldeeptutor/ 下的源码整理,核对日 2026-08-10,对应仓库快照 456f9c2(版本 1.5.11)。 本文内容为仓库源码与文档口径,我们没有安装、部署或运行过该项目,也没有调用过其中任何一个模型 API, 因此不涉及生成质量、响应速度与教学效果的任何描述。 参数与默认值随版本变动,请以仓库最新代码与 --help 的实际输出为准。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。